feat: StationMapper maps getstationsdata to outdoor/indoor/rain

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
2026-09-11 14:36:00 +02:00
co-authored by Claude Fable 5.1
parent b38b7a3ac4
commit 12f5f97a53
4 changed files with 378 additions and 1 deletions
+149
View File
@@ -0,0 +1,149 @@
<?php
declare(strict_types=1);
namespace Weerscherm;
/**
* Zet de body van Netatmo getstationsdata om naar een platte tussenstructuur.
* Ruwe waarden, unix-timestamps; de formattering gebeurt later.
*/
final class StationMapper
{
private const EMPTY_OUTDOOR = ['temp' => null, 'trend' => null, 'min' => null, 'min_at' => null, 'max' => null, 'max_at' => 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}>,
* rain_module_id: ?string, sum_rain_24: ?float}
*/
public static function map(array $body, ?string $deviceId, array $indoorModules): array
{
$device = self::pickDevice($body['devices'] ?? [], $deviceId);
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,
];
}
$modules = is_array($device['modules'] ?? null) ? $device['modules'] : [];
$outdoor = self::firstOfType($modules, 'NAModule1');
$rain = self::firstOfType($modules, 'NAModule3');
$location = $device['place']['location'] ?? null;
return [
'timezone' => self::stringOrNull($device['place']['timezone'] ?? null),
'lat' => is_array($location) && isset($location[1]) ? (float) $location[1] : null,
'lon' => is_array($location) && isset($location[0]) ? (float) $location[0] : null,
'device_id' => self::stringOrNull($device['_id'] ?? null),
'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),
'rain_module_id' => $rain === null ? null : self::stringOrNull($rain['_id'] ?? null),
'sum_rain_24' => self::floatOrNull(self::dashboard($rain)['sum_rain_24'] ?? null),
];
}
/** @param list<array<string, mixed>> $devices */
private static function pickDevice(array $devices, ?string $deviceId): ?array
{
foreach ($devices as $device) {
if ($deviceId !== null && ($device['_id'] ?? null) === $deviceId) {
return $device;
}
}
return $devices[0] ?? null;
}
/** @param list<array<string, mixed>> $modules */
private static function firstOfType(array $modules, string $type): ?array
{
foreach ($modules as $module) {
if (($module['type'] ?? null) === $type) {
return $module;
}
}
return null;
}
/** Dashboard-data van een module, of leeg als de module onbereikbaar is. */
private static function dashboard(?array $module): array
{
if ($module === null || ($module['reachable'] ?? true) === false) {
return [];
}
return is_array($module['dashboard_data'] ?? null) ? $module['dashboard_data'] : [];
}
private static function mapOutdoor(?array $module): array
{
$d = self::dashboard($module);
if ($d === []) {
return self::EMPTY_OUTDOOR;
}
return [
'temp' => self::floatOrNull($d['Temperature'] ?? null),
'trend' => self::stringOrNull($d['temp_trend'] ?? null),
'min' => self::floatOrNull($d['min_temp'] ?? null),
'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),
];
}
/**
* @param list<array<string, mixed>> $modules
* @param array<string, string> $indoorModules
*/
private static function mapIndoor(array $device, array $modules, array $indoorModules): array
{
$candidates = [];
foreach (array_merge([$device], $modules) as $module) {
$type = $module['type'] ?? null;
if ($type !== 'NAMain' && $type !== 'NAModule4') {
continue;
}
$id = self::stringOrNull($module['_id'] ?? null);
if ($id === null) {
continue;
}
$d = self::dashboard($module);
$candidates[$id] = [
'id' => $id,
'name' => $indoorModules[$id] ?? self::stringOrNull($module['module_name'] ?? null) ?? $id,
'temp' => self::floatOrNull($d['Temperature'] ?? null),
'min' => self::floatOrNull($d['min_temp'] ?? null),
'max' => self::floatOrNull($d['max_temp'] ?? null),
'reachable' => $d !== [],
];
}
$ordered = [];
foreach (array_keys($indoorModules) as $id) {
if (isset($candidates[$id])) {
$ordered[] = $candidates[$id];
unset($candidates[$id]);
}
}
return array_merge($ordered, array_values($candidates));
}
private static function floatOrNull(mixed $v): ?float
{
return is_int($v) || is_float($v) ? (float) $v : null;
}
private static function intOrNull(mixed $v): ?int
{
return is_int($v) || is_float($v) ? (int) $v : null;
}
private static function stringOrNull(mixed $v): ?string
{
return is_string($v) && $v !== '' ? $v : null;
}
}