Compare commits

...

7 Commits

Author SHA1 Message Date
admin 8aa528ff5b deploy: Typos 2026-06-02 06:44:54 +02:00
admin 9754ffabaa V 1.6.1 fix: X-Achsen-Beschriftung verbessert (24h/7d/30d/365d)
24h: Mitternacht als DD.MM (blau), sonst HH:MM per tickPositioner.
7d: nur bei 0-Uhr beschriftet, waagerecht. 30d: alle 5 Tage.
365d: nur Monatsnummer (MM), monatliche Ticks.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-01 18:26:16 +02:00
admin 9c2855fa98 V 1.6.0 fix: Tagesregen per MAX (kumulierter Tageszähler, Reset um Mitternacht)
Wochenwerte als Summe täglicher Maxima; /weather/stats mit Subquery über tägliche Maxima.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-01 17:31:07 +02:00
admin 4f89db49b6 V 1.5.9 feat: Spalte 'source' (loop/archive/wview) in weather_data
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 14:09:59 +02:00
admin dfdd4943e1 V 1.5.8 fix: Tagesregen wieder per SUM (Daten sind inkrementell, nicht kumuliert)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 16:45:13 +02:00
admin 4abaf5ee17 fix: pgAdmin nur intern erreichbar (kein öffentlicher Traefik-Zugang)
Zugriff nur noch per SSH-Tunnel, nicht mehr über stwwetter.fuerst-stuttgart.de/pgadmin

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 15:09:48 +02:00
admin 779a76dd92 V 1.5.7 fix: Tagesregen per MAX statt SUM (Server kumuliert selbst)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 09:53:00 +02:00
10 changed files with 217 additions and 51 deletions
+1
View File
@@ -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:**
+28 -14
View File
@@ -270,11 +270,17 @@ 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,
SUM(rain) as total_rain, (SELECT COALESCE(SUM(daily_max), 0)
FROM (
SELECT MAX(rain) as daily_max
FROM weather_data d2
WHERE d2.datetime >= NOW() - make_interval(hours => %s)
GROUP BY DATE(d2.datetime)
) sub) as total_rain,
COUNT(*) as data_points COUNT(*) as data_points
FROM weather_data FROM weather_data
WHERE datetime >= NOW() - make_interval(hours => %s) WHERE datetime >= NOW() - make_interval(hours => %s)
""", (hours,)) """, (hours, hours))
result = cursor.fetchone() result = cursor.fetchone()
if not result or result['data_points'] == 0: if not result or result['data_points'] == 0:
@@ -300,7 +306,7 @@ async def get_daily_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,
SUM(rain) as total_rain, MAX(rain) as total_rain,
COUNT(*) as data_points COUNT(*) as data_points
FROM weather_data FROM weather_data
WHERE datetime >= NOW() - make_interval(days => %s) WHERE datetime >= NOW() - make_interval(days => %s)
@@ -387,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
@@ -429,7 +435,7 @@ async def get_daily_aggregated_data(
MAX(wind_gust * 1.60934)::float as wind_gust, MAX(wind_gust * 1.60934)::float as wind_gust,
(array_agg(datetime ORDER BY wind_gust DESC NULLS LAST))[1] as max_wind_gust_time, (array_agg(datetime ORDER BY wind_gust DESC NULLS LAST))[1] as max_wind_gust_time,
AVG(wind_dir)::float as wind_dir, AVG(wind_dir)::float as wind_dir,
SUM(rain)::float as total_rain MAX(rain)::float as total_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('day', datetime) GROUP BY date_trunc('day', datetime)
@@ -469,7 +475,7 @@ async def get_daily_with_minmax_data(
MAX(wind_gust * 1.60934)::float as wind_gust, MAX(wind_gust * 1.60934)::float as wind_gust,
(array_agg(datetime ORDER BY wind_gust DESC NULLS LAST))[1] as max_wind_gust_time, (array_agg(datetime ORDER BY wind_gust DESC NULLS LAST))[1] as max_wind_gust_time,
AVG(wind_dir)::float as wind_dir, AVG(wind_dir)::float as wind_dir,
SUM(rain)::float as total_rain MAX(rain)::float as total_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('day', datetime) GROUP BY date_trunc('day', datetime)
@@ -490,7 +496,7 @@ async def get_daily_rain_data(
cursor.execute(""" cursor.execute("""
SELECT SELECT
date_trunc('day', datetime) as date, date_trunc('day', datetime) as date,
SUM(rain) as total_rain MAX(rain) as total_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('day', datetime) GROUP BY date_trunc('day', datetime)
@@ -512,20 +518,28 @@ async def get_weekly_rain_data(
if days >= 365: if days >= 365:
cursor.execute(""" cursor.execute("""
SELECT SELECT
date_trunc('week', datetime) as week_start, date_trunc('week', day) as week_start,
SUM(rain) as total_rain SUM(daily_rain) as total_rain
FROM (
SELECT DATE(datetime) as day, MAX(rain) as daily_rain
FROM weather_data FROM weather_data
GROUP BY date_trunc('week', datetime) GROUP BY DATE(datetime)
) sub
GROUP BY date_trunc('week', day)
ORDER BY week_start ASC ORDER BY week_start ASC
""") """)
else: else:
cursor.execute(""" cursor.execute("""
SELECT SELECT
date_trunc('week', datetime) as week_start, date_trunc('week', day) as week_start,
SUM(rain) as total_rain SUM(daily_rain) as total_rain
FROM (
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) GROUP BY DATE(datetime)
) sub
GROUP BY date_trunc('week', day)
ORDER BY week_start ASC ORDER BY week_start ASC
""", (days,)) """, (days,))
results = cursor.fetchall() results = cursor.fetchall()
@@ -596,7 +610,7 @@ async def get_daily_aggregated_range(
MAX(wind_gust * 1.60934)::float as wind_gust, MAX(wind_gust * 1.60934)::float as wind_gust,
(array_agg(datetime ORDER BY wind_gust DESC NULLS LAST))[1] as max_wind_gust_time, (array_agg(datetime ORDER BY wind_gust DESC NULLS LAST))[1] as max_wind_gust_time,
AVG(wind_dir)::float as wind_dir, AVG(wind_dir)::float as wind_dir,
SUM(rain)::float as total_rain MAX(rain)::float as total_rain
FROM weather_data FROM weather_data
WHERE datetime BETWEEN %s AND %s WHERE datetime BETWEEN %s AND %s
GROUP BY date_trunc('day', datetime) GROUP BY date_trunc('day', datetime)
+18 -3
View File
@@ -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 -1
View File
@@ -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
+19
View File
@@ -92,6 +92,23 @@ services:
- "traefik.http.routers.wetterstation.tls.certresolver=letsencrypt" - "traefik.http.routers.wetterstation.tls.certresolver=letsencrypt"
- "traefik.http.services.wetterstation.loadbalancer.server.port=80" - "traefik.http.services.wetterstation.loadbalancer.server.port=80"
pgadmin:
image: dpage/pgadmin4:latest
container_name: wetterstation_pgadmin_prod
restart: unless-stopped
environment:
PGADMIN_DEFAULT_EMAIL: ${PGADMIN_EMAIL}
PGADMIN_DEFAULT_PASSWORD: ${PGADMIN_PASSWORD}
PGADMIN_CONFIG_SERVER_MODE: 'True'
volumes:
- pgadmin_data:/var/lib/pgadmin
depends_on:
- postgres
networks:
- internal
labels:
- "traefik.enable=false"
monitor: monitor:
image: docker.citysensor.de/wetterstation-monitor:latest image: docker.citysensor.de/wetterstation-monitor:latest
container_name: wetterstation_monitor_prod container_name: wetterstation_monitor_prod
@@ -102,6 +119,8 @@ services:
volumes: volumes:
postgres_data: postgres_data:
name: wetterstation_postgres_data_prod name: wetterstation_postgres_data_prod
pgadmin_data:
name: wetterstation_pgadmin_data_prod
networks: networks:
internal: internal:
-2
View File
@@ -1,5 +1,3 @@
version: '3.8'
services: services:
postgres: postgres:
image: postgres:16-alpine image: postgres:16-alpine
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "wetterstation-frontend", "name": "wetterstation-frontend",
"private": true, "private": true,
"version": "1.5.6", "version": "1.6.1",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
+1 -1
View File
@@ -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,
} }
} }
+98 -14
View File
@@ -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)',
+39 -4
View File
@@ -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