Merge feature/temperatuurverloop: verloop van vandaag naast de buitentemperatuur

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-13 11:50:40 +02:00
co-authored by Claude Opus 5
20 changed files with 318 additions and 55 deletions
+1
View File
@@ -31,6 +31,7 @@ return [
'stations' => 300,
'measure' => 900,
'pressure' => 900,
'temperature' => 900,
'forecast' => 3600,
'radar' => 240,
],
+7
View File
@@ -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,
+22
View File
@@ -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 `<span style="${style}">${formatTime(t.at, state.tz)}</span>`;
})
.concat(['<span style="right:0">nu</span>'])
.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: [] });
+10
View File
@@ -27,7 +27,17 @@
<section class="a-hero">
<p class="a-label">Buiten <span class="a-trend" id="trend"></span><span class="a-hum" id="out-hum"></span></p>
<div class="a-temprow">
<p class="a-temp num" id="temp">–</p>
<div class="a-today" id="today-chart" hidden>
<p class="a-tcap">Vandaag</p>
<svg class="a-tspark" viewBox="0 0 200 96" preserveAspectRatio="none" aria-hidden="true">
<line class="a-spark-base" x1="0" y1="95.5" x2="200" y2="95.5"/>
<path class="a-spark-line" id="today-path" d=""/>
</svg>
<div class="a-px num" id="today-hours"></div>
</div>
</div>
<div class="a-range">
<div class="a-track"><span class="a-mark" id="mark" style="left:50%"></span></div>
<div class="a-ends num">
+8 -1
View File
@@ -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; }
+1 -1
View File
@@ -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',
+1
View File
@@ -19,6 +19,7 @@ final class Config
'stations' => 300,
'measure' => 900,
'pressure' => 900,
'temperature' => 900,
'forecast' => 3600,
'radar' => 240,
],
+29 -2
View File
@@ -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<array{at: string, temp: float}>, 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'],
];
}
+19 -1
View File
@@ -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];
+5
View File
@@ -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);
+38
View File
@@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace Weerscherm;
/**
* Zet een getmeasure-body (één waarde per tijdstip, bv. scale=30min met type=Pressure of Temperature)
* om naar een chronologisch gesorteerde reeks punten. De naam van het waardeveld verschilt per meting,
* zodat de payload "hpa" of "temp" kan heten.
*/
final class MeasureSeries
{
/**
* @param array<string|int, mixed> $body
* @return list<array{at: string, ...}> 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;
}
}
+20 -4
View File
@@ -60,17 +60,33 @@ final class NetatmoClient
/** @return array<string|int, mixed> 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<string|int, mixed> 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<string, string> $target
* @return array<string|int, mixed>
*/
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'];
}
-33
View File
@@ -1,33 +0,0 @@
<?php
declare(strict_types=1);
namespace Weerscherm;
/** Zet een getmeasure-body (scale=30min, type=Pressure) om naar een gesorteerde reeks punten. */
final class PressureHistory
{
public const HOURS = 24;
/**
* @param array<string|int, mixed> $body
* @return list<array{at: string, hpa: float}>
*/
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;
}
}
+3 -2
View File
@@ -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<array{id: string, name: string, temp: ?float, min: ?float, max: ?float, humidity: ?float, co2: ?int, reachable: bool}>,
* 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),
];
+3
View File
@@ -16,6 +16,9 @@ interface WeatherSource
/** @return array<string|int, mixed> de "body" van Netatmo getmeasure (timestamp => [hPa]) voor het basisstation */
public function pressureHistory(string $deviceId, int $begin, int $end): array;
/** @return array<string|int, mixed> 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<string, mixed>, hourly: array<string, mixed>} de "daily"- en "hourly"-blokken van Open-Meteo */
public function forecast(float $lat, float $lon, string $timezone): array;
}
+2 -2
View File
@@ -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
+31 -3
View File
@@ -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');
+7 -1
View File
@@ -13,7 +13,7 @@ final class FakeSource implements WeatherSource
public array $responses = [];
/** @var array<string, int> */
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<string, 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());
@@ -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']);
}
}
+95
View File
@@ -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"
}