From 107978b2657b6f6664ec280f7d515dad716307e6 Mon Sep 17 00:00:00 2001 From: Theo Weijmer Date: Sun, 13 Sep 2026 11:50:40 +0200 Subject: [PATCH] feat: klein grafiekje van het temperatuurverloop van vandaag MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In de lege ruimte rechts van de grote buitentemperatuur staat nu het verloop van vandaag: onderschrift "Vandaag", de lijn, en tijdlabels op hele uren met "nu" rechts. Geen y-as, want de laagste en hoogste waarde van vandaag staan al onder de balk en dat is precies de schaal. Het getal en het grafiekje staan in een flexrij, zodat het grafiekje de resterende ruimte pakt en meebeweegt als het getal smaller of breder wordt. Vlak na middernacht zijn er nog geen twee meetpunten; dan blijft het blokje verborgen in plaats van een leeg kader te tonen. Data uit een extra getmeasure (scale=30min, type=Temperature, op de buitenmodule) vanaf middernacht lokale tijd, cache 15 minuten, in de payload als outdoor_history: {points, stale} zodat een storing daar de rest niet raakt. PressureHistory is generiek gemaakt tot MeasureSeries met de naam van het waardeveld als parameter, in plaats van er een bijna identieke tweede klasse naast te zetten. StationMapper geeft nu ook het id van de buitenmodule door. Onderweg gevonden: FixtureSource deed een modulo op een float, wat een PHP-deprecation vóór de JSON in de response zette. Dat verklaart de kapotte JSON die eerder in de browserconsole langskwam. Opgelost met intdiv. Servicewerker naar v10. Co-Authored-By: Claude Opus 5 --- config.example.php | 1 + docs/weerscherm-spec.md | 7 ++ public/app.js | 22 +++++ public/index.html | 12 ++- public/styles.css | 9 +- public/sw.js | 2 +- src/Config.php | 1 + src/DashboardService.php | 31 +++++- src/FixtureSource.php | 20 +++- src/LiveSource.php | 5 + src/MeasureSeries.php | 38 ++++++++ src/NetatmoClient.php | 24 ++++- src/PressureHistory.php | 33 ------- src/StationMapper.php | 5 +- src/WeatherSource.php | 3 + tests/ConfigTest.php | 4 +- tests/DashboardServiceTest.php | 34 ++++++- tests/FakeSource.php | 8 +- ...eHistoryTest.php => MeasureSeriesTest.php} | 19 +++- tests/fixtures/getmeasure-temperature.json | 95 +++++++++++++++++++ 20 files changed, 318 insertions(+), 55 deletions(-) create mode 100644 src/MeasureSeries.php delete mode 100644 src/PressureHistory.php rename tests/{PressureHistoryTest.php => MeasureSeriesTest.php} (52%) create mode 100644 tests/fixtures/getmeasure-temperature.json diff --git a/config.example.php b/config.example.php index 2ac2ee9..9a40dc0 100644 --- a/config.example.php +++ b/config.example.php @@ -31,6 +31,7 @@ return [ 'stations' => 300, 'measure' => 900, 'pressure' => 900, + 'temperature' => 900, 'forecast' => 3600, 'radar' => 240, ], diff --git a/docs/weerscherm-spec.md b/docs/weerscherm-spec.md index 6ee268c..d352a7a 100644 --- a/docs/weerscherm-spec.md +++ b/docs/weerscherm-spec.md @@ -251,6 +251,13 @@ De iconen `sun`, `partly`, `rain` en `showers` staan in de mockup. Maak `cloud`, - De temperatuur staat groot, met 1 decimaal ("16,4°"). Het label heeft een trend-aanduiding: `up` → "↗ stijgend", `down` → "↘ dalend", `stable` → "→ stabiel". - Daaronder de gradiëntbalk van `--cold` naar `--warm`. De marker staat op `(temp − min) / (max − min)`, begrensd op 0–100%. Bij `max == min` staat hij op 50%. - Onder de balk: "9,2° laagst om 06:10" en "19,8° hoogst om 13:05". + - Rechts van het grote getal een klein grafiekje van het temperatuurverloop van vandaag. + _Toegevoegd 2026-09-13: onderschrift "Vandaag", de lijn, en tijdlabels op hele uren met "nu" rechts + (zelfde `hourTicks` als de luchtdruk). Geen y-as: de laagste en hoogste waarde van vandaag staan al + onder de balk en dat is precies de schaal van de lijn. Data uit een extra `getmeasure` + (`scale=30min`, `type=Temperature`, buitenmodule) vanaf middernacht lokale tijd, in de payload als + `outdoor_history: {points, stale}`. Vlak na middernacht zijn er nog geen twee punten; dan blijft het + blokje verborgen in plaats van een leeg kader te tonen._ - Daaronder de luchtdruk: "Luchtdruk 1014 hPa ↗ stijgend" met een grafiek van de afgelopen 24 uur. _Assen toegevoegd 2026-09-13: links de hoogste en laagste waarde van het venster in hele hPa, onder de lijn tijdlabels op hele uren die op de klok zijn uitgelijnd (elke 6 uur vanaf middernacht, dus 00:00, diff --git a/public/app.js b/public/app.js index 25e417a..87679b0 100644 --- a/public/app.js +++ b/public/app.js @@ -72,6 +72,27 @@ function renderOutdoor(o) { setText('max-at', `hoogst om ${formatTime(o.max_at, state.tz)}`); } +/** Verloop van vandaag naast het grote getal. Met minder dan twee punten valt er niets te tekenen. */ +function renderTodayChart(history) { + const block = $('today-chart'); + const points = history?.points || []; + if (points.length < 2) { + block.hidden = true; + return; + } + block.hidden = false; + $('today-path').setAttribute('d', sparklinePath(points.map((p) => p.temp), 200, 96, 4)); + + const ticks = hourTicks(points[0].at, points[points.length - 1].at, state.tz); + $('today-hours').innerHTML = ticks + .map((t) => { + const style = t.percent < 6 ? 'left:0' : `left:${t.percent.toFixed(1)}%;transform:translateX(-50%)`; + return `${formatTime(t.at, state.tz)}`; + }) + .concat(['nu']) + .join(''); +} + function renderRooms(rooms) { const ordered = pinRoom(rooms, state.pinnedRoomId); const html = ordered.map((r) => { @@ -271,6 +292,7 @@ function render() { if (d) { renderMeasurement(now); renderOutdoor(d.outdoor); + renderTodayChart(d.outdoor_history); renderRooms(d.indoor); renderRain(d.rain, now); renderPressure(d.pressure || { current: null, trend: null, history: [] }); diff --git a/public/index.html b/public/index.html index f6e98b7..3a3f844 100644 --- a/public/index.html +++ b/public/index.html @@ -27,7 +27,17 @@

Buiten

-

–

+
+

–

+ +
diff --git a/public/styles.css b/public/styles.css index bd16263..3409307 100644 --- a/public/styles.css +++ b/public/styles.css @@ -80,7 +80,14 @@ html, body { .a-hero { align-self: center; } .a-label { font-size: 22px; color: var(--muted); font-weight: 500; display: flex; gap: 14px; align-items: baseline; } .a-trend { font-size: 17px; font-weight: 400; } -.a-temp { font-size: 200px; font-weight: 200; letter-spacing: -.045em; line-height: .95; margin: 4px 0 10px -8px; } +.a-temprow { display: flex; align-items: center; gap: 28px; margin: 4px 0 10px 0; } +.a-temp { font-size: 200px; font-weight: 200; letter-spacing: -.045em; line-height: .95; margin-left: -8px; } + +/* Temperatuurverloop van vandaag, in de ruimte rechts van het grote getal */ +.a-today { flex: 1; min-width: 0; } +.a-today[hidden] { display: none; } +.a-tcap { font-size: 15px; color: var(--muted); margin-bottom: 4px; } +.a-tspark { display: block; width: 100%; height: 96px; overflow: visible; } .a-hum { font-size: 17px; font-weight: 400; } .a-hum:not(:empty)::before { content: "·"; margin-right: 14px; } .a-range { width: 560px; } diff --git a/public/sw.js b/public/sw.js index d7f1f75..dc4c1f4 100644 --- a/public/sw.js +++ b/public/sw.js @@ -1,6 +1,6 @@ /* Service worker: app-shell cache-first, API network-first met timeout en fallback op de laatste response. */ -const VERSION = 'weerscherm-v9'; +const VERSION = 'weerscherm-v10'; const SHELL = [ './', './index.html', diff --git a/src/Config.php b/src/Config.php index 5ceaccf..5fe563f 100644 --- a/src/Config.php +++ b/src/Config.php @@ -19,6 +19,7 @@ final class Config 'stations' => 300, 'measure' => 900, 'pressure' => 900, + 'temperature' => 900, 'forecast' => 3600, 'radar' => 240, ], diff --git a/src/DashboardService.php b/src/DashboardService.php index 6df5f62..b021b5b 100644 --- a/src/DashboardService.php +++ b/src/DashboardService.php @@ -10,6 +10,8 @@ namespace Weerscherm; */ final class DashboardService { + private const PRESSURE_HOURS = 24; + private const ERR_NETATMO_AUTH = 'netatmo_auth'; private const ERR_NETATMO = 'netatmo_unavailable'; private const ERR_FORECAST = 'forecast_unavailable'; @@ -48,6 +50,7 @@ final class DashboardService $rain = $this->rain($station, $now); $pressure = $this->pressure($station, $now); + $outdoorHistory = $this->outdoorHistory($station, $now); $forecast = $this->forecast($station, $tz); return [ @@ -66,6 +69,7 @@ final class DashboardService 'max_at' => $this->iso($station['outdoor']['max_at'], $tz), 'humidity' => $station['outdoor']['humidity'], ], + 'outdoor_history' => $outdoorHistory, 'indoor' => $station['indoor'], 'rain' => $rain, 'pressure' => $pressure, @@ -101,14 +105,37 @@ final class DashboardService if ($station['device_id'] === null) { return $base + ['history' => [], 'stale' => false]; } - $begin = $now->getTimestamp() - PressureHistory::HOURS * 3600; + $begin = $now->getTimestamp() - self::PRESSURE_HOURS * 3600; $measure = $this->fetch( 'pressure', fn() => $this->source->pressureHistory($station['device_id'], $begin, $now->getTimestamp()), self::ERR_NETATMO, ); return $base + [ - 'history' => $measure['data'] === null ? [] : PressureHistory::map($measure['data'], $now->getTimezone()), + 'history' => $measure['data'] === null ? [] : MeasureSeries::map($measure['data'], $now->getTimezone(), 'hpa'), + 'stale' => $measure['stale'], + ]; + } + + /** + * Temperatuurverloop van vandaag: vanaf middernacht lokale tijd tot nu. + * + * @return array{points: list, stale: bool} + */ + private function outdoorHistory(array $station, \DateTimeImmutable $now): array + { + $moduleId = $station['outdoor_module_id']; + if ($station['device_id'] === null || $moduleId === null) { + return ['points' => [], 'stale' => false]; + } + $begin = $now->setTime(0, 0, 0)->getTimestamp(); + $measure = $this->fetch( + 'temperature', + fn() => $this->source->temperatureHistory($station['device_id'], $moduleId, $begin, $now->getTimestamp()), + self::ERR_NETATMO, + ); + return [ + 'points' => $measure['data'] === null ? [] : MeasureSeries::map($measure['data'], $now->getTimezone(), 'temp'), 'stale' => $measure['stale'], ]; } diff --git a/src/FixtureSource.php b/src/FixtureSource.php index 304aee7..9fa5bdf 100644 --- a/src/FixtureSource.php +++ b/src/FixtureSource.php @@ -61,12 +61,30 @@ final class FixtureSource implements WeatherSource { $body = []; for ($ts = $begin - ($begin % 1800); $ts <= $end; $ts += 1800) { - $i = ($end - $ts) / 1800; + // Hele halfuren sinds het einde; intdiv houdt $i een int, zodat de modulo hieronder geen + // deprecation geeft (en de waarschuwing niet vóór de JSON in de response belandt). + $i = intdiv($end - $ts, 1800); $body[(string) $ts] = [round(1012 + 3 * sin($i / 8) + ($i % 5) * 0.1, 1)]; } return $body; } + public function temperatureHistory(string $deviceId, string $moduleId, int $begin, int $end): array + { + // Dagcurve tussen de min en max uit stationsData(): koudst rond 06:00, warmst rond 15:00. + $body = []; + for ($ts = $begin - ($begin % 1800); $ts <= $end; $ts += 1800) { + if ($ts < $begin) { + continue; + } + $at = (new \DateTimeImmutable('@' . $ts))->setTimezone($this->now->getTimezone()); + $hour = (int) $at->format('G') + (int) $at->format('i') / 60; + $t = 9.2 + (19.8 - 9.2) * (0.5 + 0.5 * sin((($hour - 10) / 24) * 2 * M_PI)); + $body[(string) $ts] = [round($t, 1)]; + } + return $body; + } + public function forecast(float $lat, float $lon, string $timezone): array { $tmin = [11.0, 10.1, 11.3, 12.0, 9.4, 8.1]; diff --git a/src/LiveSource.php b/src/LiveSource.php index 5c23249..9699f1d 100644 --- a/src/LiveSource.php +++ b/src/LiveSource.php @@ -39,6 +39,11 @@ final class LiveSource implements WeatherSource return $this->netatmo->getPressureHistory($deviceId, $begin, $end); } + public function temperatureHistory(string $deviceId, string $moduleId, int $begin, int $end): array + { + return $this->netatmo->getTemperatureHistory($deviceId, $moduleId, $begin, $end); + } + public function forecast(float $lat, float $lon, string $timezone): array { return $this->openMeteo->forecast($lat, $lon, $timezone); diff --git a/src/MeasureSeries.php b/src/MeasureSeries.php new file mode 100644 index 0000000..0845d3d --- /dev/null +++ b/src/MeasureSeries.php @@ -0,0 +1,38 @@ + $body + * @return list elk punt heeft "at" en het veld $valueKey + */ + public static function map(array $body, \DateTimeZone $tz, string $valueKey): array + { + $points = []; + foreach ($body as $timestamp => $values) { + if (!is_numeric($timestamp) || !is_array($values) || !isset($values[0]) || !is_numeric($values[0])) { + continue; + } + $points[(int) $timestamp] = (float) $values[0]; + } + ksort($points); + + $out = []; + foreach ($points as $ts => $value) { + $out[] = [ + 'at' => (new \DateTimeImmutable('@' . $ts))->setTimezone($tz)->format(\DATE_ATOM), + $valueKey => $value, + ]; + } + return $out; + } +} diff --git a/src/NetatmoClient.php b/src/NetatmoClient.php index ae4b0db..74b0993 100644 --- a/src/NetatmoClient.php +++ b/src/NetatmoClient.php @@ -60,17 +60,33 @@ final class NetatmoClient /** @return array timestamp => [hPa], per 30 minuten, van het basisstation */ public function getPressureHistory(string $deviceId, int $begin, int $end): array { - $json = $this->apiGet('getmeasure', [ - 'device_id' => $deviceId, + return $this->measureSeries('Pressure', ['device_id' => $deviceId], $begin, $end); + } + + /** @return array timestamp => [°C], per 30 minuten, van de opgegeven module */ + public function getTemperatureHistory(string $deviceId, string $moduleId, int $begin, int $end): array + { + return $this->measureSeries('Temperature', ['device_id' => $deviceId, 'module_id' => $moduleId], $begin, $end); + } + + /** + * Eén meetreeks per 30 minuten ophalen. $target bevat device_id en eventueel module_id. + * + * @param array $target + * @return array + */ + private function measureSeries(string $type, array $target, int $begin, int $end): array + { + $json = $this->apiGet('getmeasure', $target + [ 'scale' => '30min', - 'type' => 'Pressure', + 'type' => $type, 'date_begin' => $begin, 'date_end' => $end, 'optimize' => 'false', 'real_time' => 'false', ]); if (!is_array($json['body'] ?? null)) { - throw new UpstreamException('getmeasure (Pressure): geen body in antwoord'); + throw new UpstreamException("getmeasure ($type): geen body in antwoord"); } return $json['body']; } diff --git a/src/PressureHistory.php b/src/PressureHistory.php deleted file mode 100644 index ec4d622..0000000 --- a/src/PressureHistory.php +++ /dev/null @@ -1,33 +0,0 @@ - $body - * @return list - */ - public static function map(array $body, \DateTimeZone $tz): array - { - $points = []; - foreach ($body as $timestamp => $values) { - if (!is_numeric($timestamp) || !is_array($values) || !isset($values[0]) || !is_numeric($values[0])) { - continue; - } - $points[(int) $timestamp] = (float) $values[0]; - } - ksort($points); - - $out = []; - foreach ($points as $ts => $hpa) { - $out[] = ['at' => (new \DateTimeImmutable('@' . $ts))->setTimezone($tz)->format(\DATE_ATOM), 'hpa' => $hpa]; - } - return $out; - } -} diff --git a/src/StationMapper.php b/src/StationMapper.php index d66f40a..4ea27ef 100644 --- a/src/StationMapper.php +++ b/src/StationMapper.php @@ -20,7 +20,7 @@ final class StationMapper * outdoor: array{temp: ?float, trend: ?string, min: ?float, min_at: ?int, max: ?float, max_at: ?int, humidity: ?float}, * indoor: list, * pressure: array{current: ?float, trend: ?string}, - * rain_module_id: ?string, sum_rain_24: ?float} + * outdoor_module_id: ?string, rain_module_id: ?string, sum_rain_24: ?float} */ public static function map(array $body, ?string $deviceId, array $indoorModules): array { @@ -29,7 +29,7 @@ final class StationMapper return [ 'timezone' => null, 'lat' => null, 'lon' => null, 'device_id' => null, 'measured_at' => null, 'outdoor' => self::EMPTY_OUTDOOR, 'indoor' => [], 'pressure' => self::EMPTY_PRESSURE, - 'rain_module_id' => null, 'sum_rain_24' => null, + 'outdoor_module_id' => null, 'rain_module_id' => null, 'sum_rain_24' => null, ]; } @@ -50,6 +50,7 @@ final class StationMapper 'current' => self::floatOrNull(self::dashboard($device)['Pressure'] ?? null), 'trend' => self::stringOrNull(self::dashboard($device)['pressure_trend'] ?? null), ], + 'outdoor_module_id' => $outdoor === null ? null : self::stringOrNull($outdoor['_id'] ?? null), 'rain_module_id' => $rain === null ? null : self::stringOrNull($rain['_id'] ?? null), 'sum_rain_24' => self::floatOrNull(self::dashboard($rain)['sum_rain_24'] ?? null), ]; diff --git a/src/WeatherSource.php b/src/WeatherSource.php index 33b35cd..22c8590 100644 --- a/src/WeatherSource.php +++ b/src/WeatherSource.php @@ -16,6 +16,9 @@ interface WeatherSource /** @return array de "body" van Netatmo getmeasure (timestamp => [hPa]) voor het basisstation */ public function pressureHistory(string $deviceId, int $begin, int $end): array; + /** @return array de "body" van Netatmo getmeasure (timestamp => [°C]) voor de buitenmodule */ + public function temperatureHistory(string $deviceId, string $moduleId, int $begin, int $end): array; + /** @return array{daily: array, hourly: array} de "daily"- en "hourly"-blokken van Open-Meteo */ public function forecast(float $lat, float $lon, string $timezone): array; } diff --git a/tests/ConfigTest.php b/tests/ConfigTest.php index c0a44b3..bb35a33 100644 --- a/tests/ConfigTest.php +++ b/tests/ConfigTest.php @@ -28,7 +28,7 @@ final class ConfigTest extends TestCase self::assertSame('Europe/Berlin', $config->get('timezone')); self::assertSame('fixture', $config->get('data_source')); - self::assertSame(['stations' => 300, 'measure' => 900, 'pressure' => 900, 'forecast' => 3600, 'radar' => 240], $config->get('cache_ttl')); + self::assertSame(['stations' => 300, 'measure' => 900, 'pressure' => 900, 'temperature' => 900, 'forecast' => 3600, 'radar' => 240], $config->get('cache_ttl')); self::assertSame([], $config->get('indoor_modules')); self::assertNull($config->get('netatmo_device_id')); } @@ -39,7 +39,7 @@ final class ConfigTest extends TestCase $config = Config::load($this->file); self::assertSame('Europe/Amsterdam', $config->get('timezone')); - self::assertSame(['stations' => 60, 'measure' => 900, 'pressure' => 900, 'forecast' => 3600, 'radar' => 240], $config->get('cache_ttl')); + self::assertSame(['stations' => 60, 'measure' => 900, 'pressure' => 900, 'temperature' => 900, 'forecast' => 3600, 'radar' => 240], $config->get('cache_ttl')); } public function testUnknownKeyReturnsGivenDefault(): void diff --git a/tests/DashboardServiceTest.php b/tests/DashboardServiceTest.php index a562ab3..88f4c16 100644 --- a/tests/DashboardServiceTest.php +++ b/tests/DashboardServiceTest.php @@ -32,7 +32,7 @@ final class DashboardServiceTest extends TestCase return new DashboardService( $this->source, new Cache($this->dir), - ['stations' => 300, 'measure' => 900, 'pressure' => 900, 'forecast' => 3600], + ['stations' => 300, 'measure' => 900, 'pressure' => 900, 'temperature' => 900, 'forecast' => 3600], 'Europe/Amsterdam', $lat, $lon, @@ -46,7 +46,7 @@ final class DashboardServiceTest extends TestCase { $p = $this->service()->build(); - self::assertSame(['generated_at', 'timezone', 'station', 'outdoor', 'indoor', 'rain', 'pressure', 'forecast', 'forecast_stale', 'errors'], array_keys($p)); + self::assertSame(['generated_at', 'timezone', 'station', 'outdoor', 'outdoor_history', 'indoor', 'rain', 'pressure', 'forecast', 'forecast_stale', 'errors'], array_keys($p)); self::assertSame('2026-09-11T14:32:05+02:00', $p['generated_at']); self::assertSame('Europe/Berlin', $p['timezone']); self::assertSame(['measured_at' => '2026-09-11T14:30:12+02:00', 'stale' => false], $p['station']); @@ -83,6 +83,32 @@ final class DashboardServiceTest extends TestCase self::assertSame((new \DateTimeImmutable('2026-09-11 12:32:05 UTC'))->getTimestamp(), $end); } + public function testOutdoorHistoryCoversTodayFromLocalMidnight(): void + { + $p = $this->service()->build(); + + [$deviceId, $moduleId, $begin, $end] = $this->source->lastArgs['temperature']; + self::assertSame('70:ee:50:00:00:01', $deviceId); + self::assertSame('02:00:00:00:00:01', $moduleId, 'de buitenmodule, niet het basisstation'); + self::assertSame((new \DateTimeImmutable('2026-09-11 00:00', new \DateTimeZone('Europe/Berlin')))->getTimestamp(), $begin); + self::assertSame((new \DateTimeImmutable('2026-09-11 12:32:05 UTC'))->getTimestamp(), $end); + + self::assertNotEmpty($p['outdoor_history']['points']); + self::assertSame(['at', 'temp'], array_keys($p['outdoor_history']['points'][0])); + self::assertFalse($p['outdoor_history']['stale']); + } + + public function testOutdoorHistoryFailureLeavesTheRestIntact(): void + { + $this->source->responses['temperature'] = new UpstreamException('500'); + + $p = $this->service()->build(); + + self::assertSame(['points' => [], 'stale' => true], $p['outdoor_history']); + self::assertSame(16.4, $p['outdoor']['temp'], 'de actuele waarden komen uit stationsdata en blijven staan'); + self::assertSame(['netatmo_unavailable'], $p['errors']); + } + public function testPressureHistoryIsRequestedForLast24Hours(): void { $this->service()->build(); @@ -130,6 +156,8 @@ final class DashboardServiceTest extends TestCase self::assertNull($p['rain']['today']); self::assertSame(0, $this->source->calls['measure'], 'zonder regenmeter-id geen getmeasure-call'); self::assertSame(0, $this->source->calls['pressure'], 'zonder device-id geen luchtdruk-call'); + self::assertSame(0, $this->source->calls['temperature'], 'zonder buitenmodule geen temperatuur-call'); + self::assertSame(['points' => [], 'stale' => false], $p['outdoor_history']); self::assertSame(['current' => null, 'trend' => null, 'history' => [], 'stale' => false], $p['pressure']); self::assertSame('Europe/Amsterdam', $p['timezone'], 'fallback-tijdzone uit config'); self::assertSame(['netatmo_unavailable', 'forecast_unavailable'], $p['errors']); @@ -203,7 +231,7 @@ final class DashboardServiceTest extends TestCase $this->service()->build(); $this->service('2026-09-11 12:36:05 UTC')->build(); - self::assertSame(['stations' => 1, 'measure' => 1, 'pressure' => 1, 'forecast' => 1], $this->source->calls); + self::assertSame(['stations' => 1, 'measure' => 1, 'pressure' => 1, 'temperature' => 1, 'forecast' => 1], $this->source->calls); $this->service('2026-09-11 12:38:05 UTC')->build(); self::assertSame(2, $this->source->calls['stations'], 'stations na 5 min opnieuw'); diff --git a/tests/FakeSource.php b/tests/FakeSource.php index 6a78ed3..7da26da 100644 --- a/tests/FakeSource.php +++ b/tests/FakeSource.php @@ -13,7 +13,7 @@ final class FakeSource implements WeatherSource public array $responses = []; /** @var array */ - public array $calls = ['stations' => 0, 'measure' => 0, 'pressure' => 0, 'forecast' => 0]; + public array $calls = ['stations' => 0, 'measure' => 0, 'pressure' => 0, 'temperature' => 0, 'forecast' => 0]; /** @var array */ public array $lastArgs = []; @@ -25,6 +25,7 @@ final class FakeSource implements WeatherSource 'stations' => json_decode((string) file_get_contents(__DIR__ . '/fixtures/getstationsdata.json'), true)['body'], 'measure' => json_decode((string) file_get_contents(__DIR__ . '/fixtures/getmeasure.json'), true)['body'], 'pressure' => json_decode((string) file_get_contents(__DIR__ . '/fixtures/getmeasure-pressure.json'), true)['body'], + 'temperature' => json_decode((string) file_get_contents(__DIR__ . '/fixtures/getmeasure-temperature.json'), true)['body'], 'forecast' => array_intersect_key( json_decode((string) file_get_contents(__DIR__ . '/fixtures/forecast.json'), true), ['daily' => null, 'hourly' => null], @@ -48,6 +49,11 @@ final class FakeSource implements WeatherSource return $this->answer('pressure', func_get_args()); } + public function temperatureHistory(string $deviceId, string $moduleId, int $begin, int $end): array + { + return $this->answer('temperature', func_get_args()); + } + public function forecast(float $lat, float $lon, string $timezone): array { return $this->answer('forecast', func_get_args()); diff --git a/tests/PressureHistoryTest.php b/tests/MeasureSeriesTest.php similarity index 52% rename from tests/PressureHistoryTest.php rename to tests/MeasureSeriesTest.php index 031dfa6..d48c824 100644 --- a/tests/PressureHistoryTest.php +++ b/tests/MeasureSeriesTest.php @@ -5,16 +5,16 @@ declare(strict_types=1); namespace Weerscherm\Tests; use PHPUnit\Framework\TestCase; -use Weerscherm\PressureHistory; +use Weerscherm\MeasureSeries; -final class PressureHistoryTest extends TestCase +final class MeasureSeriesTest extends TestCase { public function testMapsAndSortsPoints(): void { $body = json_decode((string) file_get_contents(__DIR__ . '/fixtures/getmeasure-pressure.json'), true)['body']; $shuffled = array_reverse($body, true); - $points = PressureHistory::map($shuffled, new \DateTimeZone('Europe/Berlin')); + $points = MeasureSeries::map($shuffled, new \DateTimeZone('Europe/Berlin'), 'hpa'); self::assertCount(49, $points); self::assertSame('2026-09-10T14:30:00+02:00', $points[0]['at']); @@ -25,8 +25,19 @@ final class PressureHistoryTest extends TestCase public function testSkipsGarbage(): void { - $points = PressureHistory::map(['abc' => [1000.0], '1789129800' => ['x'], '1789133400' => [1013.5]], new \DateTimeZone('UTC')); + $points = MeasureSeries::map(['abc' => [1000.0], '1789129800' => ['x'], '1789133400' => [1013.5]], new \DateTimeZone('UTC'), 'hpa'); self::assertSame([['at' => '2026-09-11T13:30:00+00:00', 'hpa' => 1013.5]], $points); } + + public function testTheValueFieldIsNamedPerMeasurement(): void + { + $body = ['1789129800' => [16.4], '1789131600' => [16.9]]; + + $temps = MeasureSeries::map($body, new \DateTimeZone('Europe/Berlin'), 'temp'); + + self::assertSame(['at', 'temp'], array_keys($temps[0])); + self::assertSame(16.4, $temps[0]['temp']); + self::assertSame(16.9, $temps[1]['temp']); + } } diff --git a/tests/fixtures/getmeasure-temperature.json b/tests/fixtures/getmeasure-temperature.json new file mode 100644 index 0000000..3fe9996 --- /dev/null +++ b/tests/fixtures/getmeasure-temperature.json @@ -0,0 +1,95 @@ +{ + "body": { + "1789077600": [ + 11.9 + ], + "1789079400": [ + 11.9 + ], + "1789081200": [ + 10.8 + ], + "1789083000": [ + 10.8 + ], + "1789084800": [ + 9.9 + ], + "1789086600": [ + 9.9 + ], + "1789088400": [ + 9.4 + ], + "1789090200": [ + 9.4 + ], + "1789092000": [ + 9.2 + ], + "1789093800": [ + 9.2 + ], + "1789095600": [ + 9.4 + ], + "1789097400": [ + 9.4 + ], + "1789099200": [ + 9.9 + ], + "1789101000": [ + 9.9 + ], + "1789102800": [ + 10.8 + ], + "1789104600": [ + 10.8 + ], + "1789106400": [ + 11.9 + ], + "1789108200": [ + 11.9 + ], + "1789110000": [ + 13.1 + ], + "1789111800": [ + 13.1 + ], + "1789113600": [ + 14.5 + ], + "1789115400": [ + 14.5 + ], + "1789117200": [ + 15.9 + ], + "1789119000": [ + 15.9 + ], + "1789120800": [ + 17.2 + ], + "1789122600": [ + 17.2 + ], + "1789124400": [ + 18.2 + ], + "1789126200": [ + 18.2 + ], + "1789128000": [ + 19.1 + ], + "1789129800": [ + 19.1 + ] + }, + "status": "ok" +}