feat(api): humidity, CO2, current pressure and 24h pressure history
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -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, 'forecast' => 3600], $config->get('cache_ttl'));
|
||||
self::assertSame(['stations' => 300, 'measure' => 900, 'pressure' => 900, 'forecast' => 3600], $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, 'forecast' => 3600], $config->get('cache_ttl'));
|
||||
self::assertSame(['stations' => 60, 'measure' => 900, 'pressure' => 900, 'forecast' => 3600], $config->get('cache_ttl'));
|
||||
}
|
||||
|
||||
public function testUnknownKeyReturnsGivenDefault(): void
|
||||
|
||||
@@ -32,7 +32,7 @@ final class DashboardServiceTest extends TestCase
|
||||
return new DashboardService(
|
||||
$this->source,
|
||||
new Cache($this->dir),
|
||||
['stations' => 300, 'measure' => 900, 'forecast' => 3600],
|
||||
['stations' => 300, 'measure' => 900, 'pressure' => 900, 'forecast' => 3600],
|
||||
'Europe/Amsterdam',
|
||||
$lat,
|
||||
$lon,
|
||||
@@ -46,12 +46,19 @@ final class DashboardServiceTest extends TestCase
|
||||
{
|
||||
$p = $this->service()->build();
|
||||
|
||||
self::assertSame(['generated_at', 'timezone', 'station', 'outdoor', 'indoor', 'rain', 'forecast', 'forecast_stale', 'errors'], array_keys($p));
|
||||
self::assertSame(['generated_at', 'timezone', 'station', 'outdoor', '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']);
|
||||
self::assertSame(['temp' => 16.4, 'trend' => 'down', 'min' => 9.2, 'min_at' => '2026-09-11T06:10:00+02:00', 'max' => 19.8, 'max_at' => '2026-09-11T13:05:00+02:00'], $p['outdoor']);
|
||||
self::assertSame(['temp' => 16.4, 'trend' => 'down', 'min' => 9.2, 'min_at' => '2026-09-11T06:10:00+02:00', 'max' => 19.8, 'max_at' => '2026-09-11T13:05:00+02:00', 'humidity' => 71.0], $p['outdoor']);
|
||||
self::assertCount(3, $p['indoor']);
|
||||
self::assertSame(612, $p['indoor'][0]['co2']);
|
||||
self::assertSame(48.0, $p['indoor'][0]['humidity']);
|
||||
self::assertSame(1014.2, $p['pressure']['current']);
|
||||
self::assertSame('up', $p['pressure']['trend']);
|
||||
self::assertCount(49, $p['pressure']['history']);
|
||||
self::assertSame('2026-09-10T14:30:00+02:00', $p['pressure']['history'][0]['at']);
|
||||
self::assertFalse($p['pressure']['stale']);
|
||||
self::assertSame(2.4, $p['rain']['today']);
|
||||
self::assertSame(['2026-09-08', '2026-09-09', '2026-09-10'], array_column($p['rain']['previous_days'], 'date'));
|
||||
self::assertEqualsWithDelta(38.5, $p['rain']['month_total'], 0.0001);
|
||||
@@ -73,6 +80,29 @@ final class DashboardServiceTest extends TestCase
|
||||
self::assertSame((new \DateTimeImmutable('2026-09-11 12:32:05 UTC'))->getTimestamp(), $end);
|
||||
}
|
||||
|
||||
public function testPressureHistoryIsRequestedForLast24Hours(): void
|
||||
{
|
||||
$this->service()->build();
|
||||
|
||||
[$deviceId, $begin, $end] = $this->source->lastArgs['pressure'];
|
||||
$now = (new \DateTimeImmutable('2026-09-11 12:32:05 UTC'))->getTimestamp();
|
||||
self::assertSame('70:ee:50:00:00:01', $deviceId);
|
||||
self::assertSame($now - 86400, $begin);
|
||||
self::assertSame($now, $end);
|
||||
}
|
||||
|
||||
public function testPressureFailureKeepsCurrentValueAndMarksStale(): void
|
||||
{
|
||||
$this->source->responses['pressure'] = new UpstreamException('500');
|
||||
|
||||
$p = $this->service()->build();
|
||||
|
||||
self::assertSame(1014.2, $p['pressure']['current']);
|
||||
self::assertSame([], $p['pressure']['history']);
|
||||
self::assertTrue($p['pressure']['stale']);
|
||||
self::assertSame(['netatmo_unavailable'], $p['errors']);
|
||||
}
|
||||
|
||||
public function testForecastUsesStationLocationUnlessConfigured(): void
|
||||
{
|
||||
$this->service()->build();
|
||||
@@ -96,6 +126,8 @@ final class DashboardServiceTest extends TestCase
|
||||
self::assertSame([], $p['indoor']);
|
||||
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(['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']);
|
||||
}
|
||||
@@ -152,7 +184,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, 'forecast' => 1], $this->source->calls);
|
||||
self::assertSame(['stations' => 1, 'measure' => 1, 'pressure' => 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');
|
||||
|
||||
@@ -13,7 +13,7 @@ final class FakeSource implements WeatherSource
|
||||
public array $responses = [];
|
||||
|
||||
/** @var array<string, int> */
|
||||
public array $calls = ['stations' => 0, 'measure' => 0, 'forecast' => 0];
|
||||
public array $calls = ['stations' => 0, 'measure' => 0, 'pressure' => 0, 'forecast' => 0];
|
||||
|
||||
/** @var array<string, array> */
|
||||
public array $lastArgs = [];
|
||||
@@ -24,6 +24,7 @@ final class FakeSource implements WeatherSource
|
||||
$s->responses = [
|
||||
'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'],
|
||||
'forecast' => json_decode((string) file_get_contents(__DIR__ . '/fixtures/forecast.json'), true)['daily'],
|
||||
];
|
||||
return $s;
|
||||
@@ -39,6 +40,11 @@ final class FakeSource implements WeatherSource
|
||||
return $this->answer('measure', func_get_args());
|
||||
}
|
||||
|
||||
public function pressureHistory(string $deviceId, int $begin, int $end): array
|
||||
{
|
||||
return $this->answer('pressure', func_get_args());
|
||||
}
|
||||
|
||||
public function forecast(float $lat, float $lon, string $timezone): array
|
||||
{
|
||||
return $this->answer('forecast', func_get_args());
|
||||
|
||||
@@ -198,4 +198,18 @@ final class NetatmoClientTest extends TestCase
|
||||
'date_begin' => 1000, 'date_end' => 2000, 'optimize' => 'false', 'real_time' => 'false',
|
||||
], $this->http->requests[0]['params']);
|
||||
}
|
||||
|
||||
public function testGetPressureHistoryQuery(): void
|
||||
{
|
||||
$this->store->write(['access_token' => 'A1', 'refresh_token' => 'R1', 'expires_at' => self::NOW + 3600]);
|
||||
$this->http->queue(200, json_decode((string) file_get_contents(__DIR__ . '/fixtures/getmeasure-pressure.json'), true));
|
||||
|
||||
$body = $this->client()->getPressureHistory('dev', 1000, 2000);
|
||||
|
||||
self::assertCount(49, $body);
|
||||
self::assertSame([
|
||||
'device_id' => 'dev', 'scale' => '30min', 'type' => 'Pressure',
|
||||
'date_begin' => 1000, 'date_end' => 2000, 'optimize' => 'false', 'real_time' => 'false',
|
||||
], $this->http->requests[0]['params']);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Weerscherm\Tests;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Weerscherm\PressureHistory;
|
||||
|
||||
final class PressureHistoryTest 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'));
|
||||
|
||||
self::assertCount(49, $points);
|
||||
self::assertSame('2026-09-10T14:30:00+02:00', $points[0]['at']);
|
||||
self::assertSame('2026-09-11T14:30:00+02:00', $points[48]['at']);
|
||||
self::assertIsFloat($points[0]['hpa']);
|
||||
self::assertSame(['at', 'hpa'], array_keys($points[0]));
|
||||
}
|
||||
|
||||
public function testSkipsGarbage(): void
|
||||
{
|
||||
$points = PressureHistory::map(['abc' => [1000.0], '1789129800' => ['x'], '1789133400' => [1013.5]], new \DateTimeZone('UTC'));
|
||||
|
||||
self::assertSame([['at' => '2026-09-11T13:30:00+00:00', 'hpa' => 1013.5]], $points);
|
||||
}
|
||||
}
|
||||
@@ -38,6 +38,7 @@ final class StationMapperTest extends TestCase
|
||||
'min_at' => 1789099800,
|
||||
'max' => 19.8,
|
||||
'max_at' => 1789124700,
|
||||
'humidity' => 71.0,
|
||||
], $m['outdoor']);
|
||||
}
|
||||
|
||||
@@ -47,8 +48,8 @@ final class StationMapperTest extends TestCase
|
||||
|
||||
self::assertCount(3, $m['indoor']);
|
||||
self::assertSame(['Woonkamer', 'Slaapkamer', 'Kantoor'], array_column($m['indoor'], 'name'));
|
||||
self::assertSame(['id' => '70:ee:50:00:00:01', 'name' => 'Woonkamer', 'temp' => 21.3, 'min' => 20.4, 'max' => 22.1, 'reachable' => true], $m['indoor'][0]);
|
||||
self::assertSame(['id' => '03:00:00:00:00:03', 'name' => 'Kantoor', 'temp' => null, 'min' => null, 'max' => null, 'reachable' => false], $m['indoor'][2]);
|
||||
self::assertSame(['id' => '70:ee:50:00:00:01', 'name' => 'Woonkamer', 'temp' => 21.3, 'min' => 20.4, 'max' => 22.1, 'humidity' => 48.0, 'co2' => 612, 'reachable' => true], $m['indoor'][0]);
|
||||
self::assertSame(['id' => '03:00:00:00:00:03', 'name' => 'Kantoor', 'temp' => null, 'min' => null, 'max' => null, 'humidity' => null, 'co2' => null, 'reachable' => false], $m['indoor'][2]);
|
||||
}
|
||||
|
||||
public function testIndoorConfigDeterminesOrderAndLabels(): void
|
||||
@@ -69,6 +70,24 @@ final class StationMapperTest extends TestCase
|
||||
self::assertSame(2.4, $m['sum_rain_24']);
|
||||
}
|
||||
|
||||
public function testPressureFromBaseStation(): void
|
||||
{
|
||||
$m = StationMapper::map($this->body(), null, []);
|
||||
|
||||
self::assertSame(['current' => 1014.2, 'trend' => 'up'], $m['pressure']);
|
||||
}
|
||||
|
||||
public function testPressureNullWhenBaseStationUnreachable(): void
|
||||
{
|
||||
$body = $this->body();
|
||||
$body['devices'][0]['reachable'] = false;
|
||||
unset($body['devices'][0]['dashboard_data']);
|
||||
$m = StationMapper::map($body, null, []);
|
||||
|
||||
self::assertSame(['current' => null, 'trend' => null], $m['pressure']);
|
||||
self::assertSame(1789129780, $m['measured_at'], 'valt terug op time_utc van de buitenmodule');
|
||||
}
|
||||
|
||||
public function testWithoutRainModule(): void
|
||||
{
|
||||
$body = $this->body();
|
||||
@@ -89,7 +108,7 @@ final class StationMapperTest extends TestCase
|
||||
unset($body['devices'][0]['modules'][0]['dashboard_data']);
|
||||
$m = StationMapper::map($body, null, []);
|
||||
|
||||
self::assertSame(['temp' => null, 'trend' => null, 'min' => null, 'min_at' => null, 'max' => null, 'max_at' => null], $m['outdoor']);
|
||||
self::assertSame(['temp' => null, 'trend' => null, 'min' => null, 'min_at' => null, 'max' => null, 'max_at' => null, 'humidity' => null], $m['outdoor']);
|
||||
}
|
||||
|
||||
public function testUnknownDeviceIdFallsBackToFirstStation(): void
|
||||
@@ -108,5 +127,6 @@ final class StationMapperTest extends TestCase
|
||||
self::assertSame([], $m['indoor']);
|
||||
self::assertNull($m['outdoor']['temp']);
|
||||
self::assertNull($m['rain_module_id']);
|
||||
self::assertSame(['current' => null, 'trend' => null], $m['pressure']);
|
||||
}
|
||||
}
|
||||
|
||||
+152
@@ -0,0 +1,152 @@
|
||||
{
|
||||
"body": {
|
||||
"1789043400": [
|
||||
1011.5
|
||||
],
|
||||
"1789045200": [
|
||||
1011
|
||||
],
|
||||
"1789047000": [
|
||||
1010.6
|
||||
],
|
||||
"1789048800": [
|
||||
1010.2
|
||||
],
|
||||
"1789050600": [
|
||||
1010.3
|
||||
],
|
||||
"1789052400": [
|
||||
1009.9
|
||||
],
|
||||
"1789054200": [
|
||||
1009.6
|
||||
],
|
||||
"1789056000": [
|
||||
1009.4
|
||||
],
|
||||
"1789057800": [
|
||||
1009.1
|
||||
],
|
||||
"1789059600": [
|
||||
1009.4
|
||||
],
|
||||
"1789061400": [
|
||||
1009.3
|
||||
],
|
||||
"1789063200": [
|
||||
1009.2
|
||||
],
|
||||
"1789065000": [
|
||||
1009.2
|
||||
],
|
||||
"1789066800": [
|
||||
1009.2
|
||||
],
|
||||
"1789068600": [
|
||||
1009.7
|
||||
],
|
||||
"1789070400": [
|
||||
1009.8
|
||||
],
|
||||
"1789072200": [
|
||||
1009.9
|
||||
],
|
||||
"1789074000": [
|
||||
1010.1
|
||||
],
|
||||
"1789075800": [
|
||||
1010.3
|
||||
],
|
||||
"1789077600": [
|
||||
1011
|
||||
],
|
||||
"1789079400": [
|
||||
1011.2
|
||||
],
|
||||
"1789081200": [
|
||||
1011.5
|
||||
],
|
||||
"1789083000": [
|
||||
1011.8
|
||||
],
|
||||
"1789084800": [
|
||||
1012
|
||||
],
|
||||
"1789086600": [
|
||||
1012.8
|
||||
],
|
||||
"1789088400": [
|
||||
1013.1
|
||||
],
|
||||
"1789090200": [
|
||||
1013.3
|
||||
],
|
||||
"1789092000": [
|
||||
1013.6
|
||||
],
|
||||
"1789093800": [
|
||||
1013.8
|
||||
],
|
||||
"1789095600": [
|
||||
1014.5
|
||||
],
|
||||
"1789097400": [
|
||||
1014.6
|
||||
],
|
||||
"1789099200": [
|
||||
1014.8
|
||||
],
|
||||
"1789101000": [
|
||||
1014.8
|
||||
],
|
||||
"1789102800": [
|
||||
1014.9
|
||||
],
|
||||
"1789104600": [
|
||||
1015.4
|
||||
],
|
||||
"1789106400": [
|
||||
1015.3
|
||||
],
|
||||
"1789108200": [
|
||||
1015.2
|
||||
],
|
||||
"1789110000": [
|
||||
1015
|
||||
],
|
||||
"1789111800": [
|
||||
1014.8
|
||||
],
|
||||
"1789113600": [
|
||||
1015.1
|
||||
],
|
||||
"1789115400": [
|
||||
1014.8
|
||||
],
|
||||
"1789117200": [
|
||||
1014.5
|
||||
],
|
||||
"1789119000": [
|
||||
1014.1
|
||||
],
|
||||
"1789120800": [
|
||||
1013.8
|
||||
],
|
||||
"1789122600": [
|
||||
1013.8
|
||||
],
|
||||
"1789124400": [
|
||||
1013.4
|
||||
],
|
||||
"1789126200": [
|
||||
1012.9
|
||||
],
|
||||
"1789128000": [
|
||||
1012.5
|
||||
],
|
||||
"1789129800": [
|
||||
1012
|
||||
]
|
||||
},
|
||||
"status": "ok"
|
||||
}
|
||||
Reference in New Issue
Block a user