Seitenfenster mit Berliner Wetter (DWD-Daten) und Strommarkt Deutschland (SMARD)

Ein Mod für Claude Code. Er öffnet neben dem Chat ein Seitenfenster mit dem aktuellen Wetter in Berlin und dem Strommarkt in Deutschland und aktualisiert es alle 15 Minuten.
Wetter Berlin: Temperatur, Himmel, Wind und Böen, Regen der letzten Stunde, Luftfeuchte und die amtlichen Warnungen des Deutschen Wetterdienstes für Berlin-Mitte. Die Daten stammen vom DWD und werden über Bright Sky abgefragt.
Strommarkt Deutschland: Börsenpreis der laufenden Stunde in ct/kWh, die günstigste und die teuerste Stunde des Tages, der Anteil der Erneuerbaren an der Erzeugung und die fünf größten Quellen. Die Daten stammen von SMARD, der Plattform der Bundesnetzagentur. Der Preis ist der Großhandelspreis am Day-Ahead-Markt, nicht der Haushaltstarif. Die Erzeugung erscheint dort mit etwa zwei Stunden Verzug.
Beide Quellen sind kostenlos und brauchen keinen Schlüssel.
In einer Claude-Code-Sitzung im Terminal:
/plugin install wetter-strom --marketplace ronnyschumann1983-boop/claude-mod-wetter-strom
Danach y, um den Marketplace hinzuzufügen, und mit Enter den Bereich wählen.
Aus einer Shell:
claude plugin install wetter-strom --marketplace ronnyschumann1983-boop/claude-mod-wetter-strom
In der Desktop-App, wo /plugin nicht läuft: auf + neben dem Eingabefeld klicken, dann Plugins, Add marketplace mit ronnyschumann1983-boop/claude-mod-wetter-strom, dann Add plugin.
In einem Terminal ab 144 Zeichen Breite öffnet sich das Fenster beim Start von selbst. Sonst mit /wetter-strom. Der Knopf „Aktualisieren“ lädt die Daten sofort neu.
hooks/register.tsx 264 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { Hour, Power, Snapshot, Weather } from '../types'
5
6const PANE = 'wetter-strom'
7const TITLE = 'Wetter & Strom Berlin'
8const GEO = 'lat=52.52&lon=13.40' // Berlin-Mitte
9const EVERY = 15 * 60 * 1000
10const HOUR = 3600 * 1000
11const SMARD = 'https://www.smard.de/app/chart_data'
12
13// SMARD-Filter fuer die Erzeugung (MWh je Stunde), am 24.10.2026 einzeln geprueft
14const SOURCES = [
15 { id: 4067, name: 'Wind an Land', ee: true },
16 { id: 1225, name: 'Wind auf See', ee: true },
17 { id: 4068, name: 'Photovoltaik', ee: true },
18 { id: 4066, name: 'Biomasse', ee: true },
19 { id: 1226, name: 'Wasserkraft', ee: true },
20 { id: 1228, name: 'Sonst. Erneuerbare', ee: true },
21 { id: 1223, name: 'Braunkohle', ee: false },
22 { id: 4069, name: 'Steinkohle', ee: false },
23 { id: 4071, name: 'Erdgas', ee: false },
24 { id: 4070, name: 'Pumpspeicher', ee: false },
25 { id: 1227, name: 'Sonst. Konventionelle', ee: false },
26]
27
28const SKY: Record<string, string> = {
29 'clear-day': 'klar',
30 'clear-night': 'klar',
31 'partly-cloudy-day': 'teils bewölkt',
32 'partly-cloudy-night': 'teils bewölkt',
33 cloudy: 'bedeckt',
34 fog: 'Nebel',
35 wind: 'windig',
36 rain: 'Regen',
37 sleet: 'Schneeregen',
38 snow: 'Schnee',
39 hail: 'Hagel',
40 thunderstorm: 'Gewitter',
41}
42
43const snap = atom({ plugin: 'wetter-strom', key: 'snap' } as const, null, {
44 shape: 'snap-1',
45})
46
47// Berliner Ortszeit ohne Intl: Sommerzeit vom letzten Sonntag im Maerz bis zum
48// letzten Sonntag im Oktober, jeweils 01:00 UTC
49const lastSunday = (year: number, month: number) => {
50 const d = new Date(Date.UTC(year, month + 1, 0, 1))
51 d.setUTCDate(d.getUTCDate() - d.getUTCDay())
52 return d.getTime()
53}
54const berlin = (ms: number) => {
55 const y = new Date(ms).getUTCFullYear()
56 const summer = ms >= lastSunday(y, 2) && ms < lastSunday(y, 9)
57 return new Date(ms + (summer ? 2 : 1) * HOUR)
58}
59const two = (n: number) => String(n).padStart(2, '0')
60const hhmm = (ms: number) => {
61 const d = berlin(ms)
62 return `${two(d.getUTCHours())}:${two(d.getUTCMinutes())}`
63}
64const day = (ms: number) => berlin(ms).toISOString().slice(0, 10)
65const de = (n: number, digits = 1) => n.toFixed(digits).replace('.', ',')
66const or = (n: number | null | undefined, digits: number, unit: string) =>
67 n === null || n === undefined ? '–' : `${de(n, digits)} ${unit}`
68const ct = (h: Hour) => `${de(h.value / 10)} ct`
69
70const json = async ($: EngineInterface, url: string) => {
71 const r = await $.http.fetch(url)
72 if (!r.ok) throw new Error(`HTTP ${r.status} bei ${url.split('/')[2]}`)
73 return JSON.parse(r.text)
74}
75
76type Point = [number, number | null]
77const filled = (s: Point[]) => s.filter((p): p is [number, number] => p[1] !== null)
78const chunk = async ($: EngineInterface, id: number, region: string, ts: number) =>
79 filled((await json($, `${SMARD}/${id}/${region}/${id}_${region}_hour_${ts}.json`)).series)
80
81// Neuester Wochenblock mit Werten; montags frueh ist der neue noch leer
82const latest = async ($: EngineInterface, id: number, index: number[]) => {
83 for (const ts of index.slice(-2).reverse()) {
84 const s = await chunk($, id, 'DE', ts)
85 if (s.length) return s
86 }
87 return []
88}
89
90const loadWeather = async ($: EngineInterface): Promise<Weather> => {
91 const [now, warn] = await Promise.all([
92 json($, `https://api.brightsky.dev/current_weather?${GEO}`),
93 json($, `https://api.brightsky.dev/alerts?${GEO}`),
94 ])
95 const w = now.weather
96 return {
97 at: Date.parse(w.timestamp),
98 station: now.sources?.[0]?.station_name ?? '?',
99 temp: w.temperature ?? null,
100 sky: SKY[w.icon] ?? w.icon ?? '?',
101 wind: w.wind_speed_10 ?? null,
102 gust: w.wind_gust_speed_10 ?? null,
103 rain: w.precipitation_60 ?? null,
104 humidity: w.relative_humidity ?? null,
105 alerts: (warn.alerts ?? []).map(
106 (a: { headline_de: string; expires: string }) =>
107 `${a.headline_de} bis ${hhmm(Date.parse(a.expires))}`,
108 ),
109 }
110}
111
112const loadPower = async ($: EngineInterface, now: number): Promise<Power> => {
113 // Die Wochenblock-Zeitstempel sind fuer Preis (DE-LU) und Erzeugung (DE) gleich
114 const index: number[] = (await json($, `${SMARD}/4169/DE-LU/index_hour.json`)).timestamps
115 // Preis aus beiden Bloecken: sonntags nachmittags steht Montag schon im neuen
116 const prices = (
117 await Promise.all(index.slice(-2).map(ts => chunk($, 4169, 'DE-LU', ts)))
118 ).flat()
119 const hour = ([at, value]: [number, number]): Hour => ({ at, value })
120 const current = prices.find(([at]) => at <= now && now < at + HOUR)
121 const today = prices.filter(([at]) => day(at) === day(now)).sort((a, b) => a[1] - b[1])
122
123 const gen = await Promise.all(SOURCES.map(s => latest($, s.id, index)))
124 // Juengste Stunde, die alle liefernden Quellen schon haben
125 const ends = gen.map(s => s.at(-1)?.[0]).filter((t): t is number => t !== undefined)
126 const mixAt = ends.length ? Math.min(...ends) : 0
127 const mix = SOURCES.map((s, i) => ({
128 ...s,
129 mw: gen[i]?.find(([at]) => at === mixAt)?.[1] ?? 0,
130 }))
131 const total = mix.reduce((n, m) => n + m.mw, 0)
132 const ee = mix.reduce((n, m) => n + (m.ee ? m.mw : 0), 0)
133
134 return {
135 price: current ? hour(current) : null,
136 cheapest: today[0] ? hour(today[0]) : null,
137 dearest: today.at(-1) ? hour(today.at(-1)!) : null,
138 mixAt,
139 eeShare: total > 0 ? ee / total : null,
140 mix: mix
141 .filter(m => m.mw > 0)
142 .sort((a, b) => b.mw - a.mw)
143 .map(({ name, mw }) => ({ name, mw })),
144 }
145}
146
147let isBusy = false
148const refresh = async ($: EngineInterface) => {
149 if (isBusy) return
150 isBusy = true
151 try {
152 const now = await $.clock.now()
153 const [w, p] = await Promise.allSettled([loadWeather($), loadPower($, now)])
154 const errors = [
155 w.status === 'rejected' ? `Wetter: ${w.reason?.message ?? w.reason}` : '',
156 p.status === 'rejected' ? `Strom: ${p.reason?.message ?? p.reason}` : '',
157 ].filter(Boolean)
158 await update($, snap, (old): Snapshot => ({
159 // Ein Ausfall behaelt den letzten guten Stand
160 weather: w.status === 'fulfilled' ? w.value : old?.weather ?? null,
161 power: p.status === 'fulfilled' ? p.value : old?.power ?? null,
162 fetchedAt: now,
163 errors,
164 }))
165 } finally {
166 isBusy = false
167 }
168}
169
170export const register: Register = on => {
171 on('session.start', async ($, e, next) => {
172 await $.command.register({
173 name: 'wetter-strom',
174 description: 'Wetter Berlin und Strommarkt im Seitenfenster',
175 })
176 void $.ui.open({ id: PANE, title: TITLE })
177 void refresh($)
178 $.clock.every(EVERY, () => void refresh($))
179
180 return next(e)
181 })
182
183 on('command.run', { command: 'wetter-strom' }, async $ => {
184 await $.ui.open({ id: PANE, title: TITLE })
185 await refresh($)
186
187 return { text: 'Fenster „Wetter & Strom Berlin“ geöffnet.' }
188 })
189
190 on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
191 const { Box, Button, Text } = $.ui.resolve(e)
192 const s = await read($, snap)
193
194 if (!s) {
195 return <Text dimColor>Lade Daten …</Text>
196 }
197
198 const { weather: w, power: p } = s
199
200 return (
201 <Box flexDirection="column">
202 <Text weight="bold">Wetter Berlin</Text>
203 {w ? (
204 <Box flexDirection="column">
205 <Text dimColor>
206 DWD-Station {w.station}, {hhmm(w.at)} Uhr
207 </Text>
208 <Text>
209 {or(w.temp, 1, '°C')} · {w.sky}
210 </Text>
211 <Text>
212 Wind {or(w.wind, 0, 'km/h')}, Böen {or(w.gust, 0, 'km/h')}
213 </Text>
214 <Text>
215 Regen 1 h {or(w.rain, 1, 'mm')} · Feuchte {or(w.humidity, 0, '%')}
216 </Text>
217 {w.alerts.map(a => (
218 <Text color="warning">⚠ {a}</Text>
219 ))}
220 </Box>
221 ) : (
222 <Text dimColor>keine Wetterdaten</Text>
223 )}
224 <Text> </Text>
225 <Text weight="bold">Strommarkt Deutschland</Text>
226 {p ? (
227 <Box flexDirection="column">
228 <Text>
229 Börsenpreis jetzt{' '}
230 {p.price ? `${ct(p.price)}/kWh (${de(p.price.value, 0)} €/MWh)` : '–'}
231 </Text>
232 {p.cheapest && p.dearest && (
233 <Text>
234 Heute am günstigsten {hhmm(p.cheapest.at)} ({ct(p.cheapest)}), am teuersten{' '}
235 {hhmm(p.dearest.at)} ({ct(p.dearest)})
236 </Text>
237 )}
238 {p.eeShare !== null && (
239 <Text color={p.eeShare >= 0.5 ? 'success' : 'text'}>
240 Erneuerbare {de(p.eeShare * 100, 0)} % der Erzeugung ({hhmm(p.mixAt)} Uhr)
241 </Text>
242 )}
243 {p.mix.slice(0, 5).map(m => (
244 <Text dimColor>
245 {m.name} {de(m.mw / 1000)} GW
246 </Text>
247 ))}
248 </Box>
249 ) : (
250 <Text dimColor>keine Strommarktdaten</Text>
251 )}
252 <Text> </Text>
253 {s.errors.map(err => (
254 <Text color="error">{err}</Text>
255 ))}
256 <Text dimColor>
257 Stand {hhmm(s.fetchedAt)} · alle 15 Min. · DWD-Daten über Bright Sky, SMARD
258 </Text>
259 <Button key="reload" label="Aktualisieren" onPress={() => refresh($)} />
260 </Box>
261 )
262 })
263}
264types/index.d.ts 36 lines1export type Weather = {
2 at: number
3 station: string
4 temp: number | null
5 sky: string
6 wind: number | null
7 gust: number | null
8 rain: number | null
9 humidity: number | null
10 alerts: string[]
11}
12
13export type Hour = { at: number; value: number }
14
15export type Power = {
16 price: Hour | null
17 cheapest: Hour | null
18 dearest: Hour | null
19 mixAt: number
20 eeShare: number | null
21 mix: { name: string; mw: number }[]
22}
23
24export type Snapshot = {
25 weather: Weather | null
26 power: Power | null
27 fetchedAt: number
28 errors: string[]
29}
30
31declare module 'claude-code' {
32 interface PluginState {
33 'wetter-strom': { snap: Shaped<Snapshot | null> }
34 }
35}
36