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:
@@ -18,6 +18,7 @@ final class Config
|
||||
'cache_ttl' => [
|
||||
'stations' => 300,
|
||||
'measure' => 900,
|
||||
'pressure' => 900,
|
||||
'forecast' => 3600,
|
||||
],
|
||||
'storage_path' => null,
|
||||
|
||||
@@ -18,7 +18,7 @@ final class DashboardService
|
||||
private array $errors = [];
|
||||
|
||||
/**
|
||||
* @param array{stations: int, measure: int, forecast: int} $ttl
|
||||
* @param array{stations: int, measure: int, pressure: int, forecast: int} $ttl
|
||||
* @param array<string, string> $indoorModules
|
||||
*/
|
||||
public function __construct(
|
||||
@@ -47,6 +47,7 @@ final class DashboardService
|
||||
$now = $nowUtc->setTimezone($tz);
|
||||
|
||||
$rain = $this->rain($station, $now);
|
||||
$pressure = $this->pressure($station, $now);
|
||||
$forecast = $this->forecast($station, $tz);
|
||||
|
||||
return [
|
||||
@@ -63,9 +64,11 @@ final class DashboardService
|
||||
'min_at' => $this->iso($station['outdoor']['min_at'], $tz),
|
||||
'max' => $station['outdoor']['max'],
|
||||
'max_at' => $this->iso($station['outdoor']['max_at'], $tz),
|
||||
'humidity' => $station['outdoor']['humidity'],
|
||||
],
|
||||
'indoor' => $station['indoor'],
|
||||
'rain' => $rain,
|
||||
'pressure' => $pressure,
|
||||
'forecast' => $forecast['days'],
|
||||
'forecast_stale' => $forecast['stale'],
|
||||
'errors' => $this->errors,
|
||||
@@ -91,6 +94,25 @@ final class DashboardService
|
||||
return RainCalculator::compute($measure['data'], $now, $station['sum_rain_24']) + ['stale' => $measure['stale']];
|
||||
}
|
||||
|
||||
/** @return array{current: ?float, trend: ?string, history: list<array{at: string, hpa: float}>, stale: bool} */
|
||||
private function pressure(array $station, \DateTimeImmutable $now): array
|
||||
{
|
||||
$base = ['current' => $station['pressure']['current'], 'trend' => $station['pressure']['trend']];
|
||||
if ($station['device_id'] === null) {
|
||||
return $base + ['history' => [], 'stale' => false];
|
||||
}
|
||||
$begin = $now->getTimestamp() - PressureHistory::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()),
|
||||
'stale' => $measure['stale'],
|
||||
];
|
||||
}
|
||||
|
||||
/** @return array{days: list<array>, stale: bool} */
|
||||
private function forecast(array $station, \DateTimeZone $tz): array
|
||||
{
|
||||
|
||||
+16
-5
@@ -27,16 +27,17 @@ final class FixtureSource implements WeatherSource
|
||||
return ['devices' => [[
|
||||
'_id' => '70:ee:50:00:00:01', 'type' => 'NAMain', 'station_name' => 'Thuis', 'module_name' => 'Woonkamer', 'reachable' => true,
|
||||
'place' => ['timezone' => $this->timezone, 'location' => [6.55, 51.82]],
|
||||
'dashboard_data' => ['time_utc' => $at('-2 minutes'), 'Temperature' => 21.3, 'min_temp' => 20.4, 'max_temp' => 22.1,
|
||||
'date_min_temp' => $today('05:50'), 'date_max_temp' => $today('13:40'), 'temp_trend' => 'stable'],
|
||||
'dashboard_data' => ['time_utc' => $at('-2 minutes'), 'Temperature' => 21.3, 'CO2' => 612, 'Humidity' => 48,
|
||||
'Pressure' => 1014.2, 'AbsolutePressure' => 1011.8, 'pressure_trend' => 'up', 'Noise' => 38,
|
||||
'min_temp' => 20.4, 'max_temp' => 22.1, 'date_min_temp' => $today('05:50'), 'date_max_temp' => $today('13:40'), 'temp_trend' => 'stable'],
|
||||
'modules' => [
|
||||
['_id' => '02:00:00:00:00:01', 'type' => 'NAModule1', 'module_name' => 'Buiten', 'reachable' => true,
|
||||
'dashboard_data' => ['time_utc' => $at('-3 minutes'), 'Temperature' => 16.4, 'min_temp' => 9.2, 'max_temp' => 19.8,
|
||||
'dashboard_data' => ['time_utc' => $at('-3 minutes'), 'Temperature' => 16.4, 'Humidity' => 71, 'min_temp' => 9.2, 'max_temp' => 19.8,
|
||||
'date_min_temp' => $today('06:10'), 'date_max_temp' => $today('13:05'), 'temp_trend' => 'down']],
|
||||
['_id' => '03:00:00:00:00:02', 'type' => 'NAModule4', 'module_name' => 'Slaapkamer', 'reachable' => true,
|
||||
'dashboard_data' => ['time_utc' => $at('-4 minutes'), 'Temperature' => 18.9, 'min_temp' => 18.1, 'max_temp' => 19.6, 'temp_trend' => 'up']],
|
||||
'dashboard_data' => ['time_utc' => $at('-4 minutes'), 'Temperature' => 18.9, 'CO2' => 1180, 'Humidity' => 52, 'min_temp' => 18.1, 'max_temp' => 19.6, 'temp_trend' => 'up']],
|
||||
['_id' => '03:00:00:00:00:03', 'type' => 'NAModule4', 'module_name' => 'Kantoor', 'reachable' => true,
|
||||
'dashboard_data' => ['time_utc' => $at('-4 minutes'), 'Temperature' => 20.2, 'min_temp' => 19.0, 'max_temp' => 21.0, 'temp_trend' => 'stable']],
|
||||
'dashboard_data' => ['time_utc' => $at('-4 minutes'), 'Temperature' => 20.2, 'CO2' => 1640, 'Humidity' => 44, 'min_temp' => 19.0, 'max_temp' => 21.0, 'temp_trend' => 'stable']],
|
||||
['_id' => '05:00:00:00:00:04', 'type' => 'NAModule3', 'module_name' => 'Regenmeter', 'reachable' => true,
|
||||
'dashboard_data' => ['time_utc' => $at('-3 minutes'), 'Rain' => 0, 'sum_rain_1' => 0, 'sum_rain_24' => 2.4]],
|
||||
],
|
||||
@@ -56,6 +57,16 @@ final class FixtureSource implements WeatherSource
|
||||
return $body;
|
||||
}
|
||||
|
||||
public function pressureHistory(string $deviceId, int $begin, int $end): array
|
||||
{
|
||||
$body = [];
|
||||
for ($ts = $begin - ($begin % 1800); $ts <= $end; $ts += 1800) {
|
||||
$i = ($end - $ts) / 1800;
|
||||
$body[(string) $ts] = [round(1012 + 3 * sin($i / 8) + ($i % 5) * 0.1, 1)];
|
||||
}
|
||||
return $body;
|
||||
}
|
||||
|
||||
public function forecast(float $lat, float $lon, string $timezone): array
|
||||
{
|
||||
$dates = [];
|
||||
|
||||
@@ -34,6 +34,11 @@ final class LiveSource implements WeatherSource
|
||||
return $this->netatmo->getMeasure($deviceId, $moduleId, $begin, $end);
|
||||
}
|
||||
|
||||
public function pressureHistory(string $deviceId, int $begin, int $end): array
|
||||
{
|
||||
return $this->netatmo->getPressureHistory($deviceId, $begin, $end);
|
||||
}
|
||||
|
||||
public function forecast(float $lat, float $lon, string $timezone): array
|
||||
{
|
||||
return $this->openMeteo->dailyForecast($lat, $lon, $timezone);
|
||||
|
||||
@@ -57,6 +57,24 @@ final class NetatmoClient
|
||||
return $json['body'];
|
||||
}
|
||||
|
||||
/** @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,
|
||||
'scale' => '30min',
|
||||
'type' => 'Pressure',
|
||||
'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');
|
||||
}
|
||||
return $json['body'];
|
||||
}
|
||||
|
||||
/** @param array<string, string|int|float> $query */
|
||||
private function apiGet(string $path, array $query): array
|
||||
{
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
+14
-4
@@ -10,14 +10,16 @@ namespace Weerscherm;
|
||||
*/
|
||||
final class StationMapper
|
||||
{
|
||||
private const EMPTY_OUTDOOR = ['temp' => null, 'trend' => null, 'min' => null, 'min_at' => null, 'max' => null, 'max_at' => null];
|
||||
private const EMPTY_OUTDOOR = ['temp' => null, 'trend' => null, 'min' => null, 'min_at' => null, 'max' => null, 'max_at' => null, 'humidity' => null];
|
||||
private const EMPTY_PRESSURE = ['current' => null, 'trend' => null];
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $body
|
||||
* @param array<string, string> $indoorModules _id => label, in de gewenste volgorde
|
||||
* @return array{timezone: ?string, lat: ?float, lon: ?float, device_id: ?string, measured_at: ?int,
|
||||
* outdoor: array{temp: ?float, trend: ?string, min: ?float, min_at: ?int, max: ?float, max_at: ?int},
|
||||
* indoor: list<array{id: string, name: string, temp: ?float, min: ?float, max: ?float, reachable: bool}>,
|
||||
* 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}
|
||||
*/
|
||||
public static function map(array $body, ?string $deviceId, array $indoorModules): array
|
||||
@@ -26,7 +28,8 @@ final class StationMapper
|
||||
if ($device === null) {
|
||||
return [
|
||||
'timezone' => null, 'lat' => null, 'lon' => null, 'device_id' => null, 'measured_at' => null,
|
||||
'outdoor' => self::EMPTY_OUTDOOR, 'indoor' => [], 'rain_module_id' => null, 'sum_rain_24' => null,
|
||||
'outdoor' => self::EMPTY_OUTDOOR, 'indoor' => [], 'pressure' => self::EMPTY_PRESSURE,
|
||||
'rain_module_id' => null, 'sum_rain_24' => null,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -43,6 +46,10 @@ final class StationMapper
|
||||
'measured_at' => self::intOrNull(self::dashboard($device)['time_utc'] ?? self::dashboard($outdoor)['time_utc'] ?? null),
|
||||
'outdoor' => self::mapOutdoor($outdoor),
|
||||
'indoor' => self::mapIndoor($device, $modules, $indoorModules),
|
||||
'pressure' => [
|
||||
'current' => self::floatOrNull(self::dashboard($device)['Pressure'] ?? null),
|
||||
'trend' => self::stringOrNull(self::dashboard($device)['pressure_trend'] ?? null),
|
||||
],
|
||||
'rain_module_id' => $rain === null ? null : self::stringOrNull($rain['_id'] ?? null),
|
||||
'sum_rain_24' => self::floatOrNull(self::dashboard($rain)['sum_rain_24'] ?? null),
|
||||
];
|
||||
@@ -92,6 +99,7 @@ final class StationMapper
|
||||
'min_at' => self::intOrNull($d['date_min_temp'] ?? null),
|
||||
'max' => self::floatOrNull($d['max_temp'] ?? null),
|
||||
'max_at' => self::intOrNull($d['date_max_temp'] ?? null),
|
||||
'humidity' => self::floatOrNull($d['Humidity'] ?? null),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -118,6 +126,8 @@ final class StationMapper
|
||||
'temp' => self::floatOrNull($d['Temperature'] ?? null),
|
||||
'min' => self::floatOrNull($d['min_temp'] ?? null),
|
||||
'max' => self::floatOrNull($d['max_temp'] ?? null),
|
||||
'humidity' => self::floatOrNull($d['Humidity'] ?? null),
|
||||
'co2' => self::intOrNull($d['CO2'] ?? null),
|
||||
'reachable' => $d !== [],
|
||||
];
|
||||
}
|
||||
|
||||
@@ -13,6 +13,9 @@ interface WeatherSource
|
||||
/** @return array<string|int, mixed> de "body" van Netatmo getmeasure (timestamp => [mm]) */
|
||||
public function measure(string $deviceId, string $moduleId, int $begin, int $end): array;
|
||||
|
||||
/** @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, mixed> het "daily"-blok van Open-Meteo */
|
||||
public function forecast(float $lat, float $lon, string $timezone): array;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user