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" +}