lcly-data-admin/app/Services/BiAngDeviceLogService.php

454 lines
18 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<?php
namespace App\Services;
use App\Enums\DeviceStatus;
use App\Enums\DeviceType;
use App\Iot\BiAng\HttpClient as BiAngHttpClient;
use App\Models\Device;
use App\Models\DeviceLog;
use App\Models\MeteorologicalMonitoringDailyLog;
use App\Models\MeteorologicalMonitoringLog;
use App\Models\SoilMonitoringDailyLog;
use App\Models\SoilMonitoringLog;
use Illuminate\Support\Arr;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;
class BiAngDeviceLogService
{
public function sync(Device $device): void
{
$config = json_decode($device->project?->value, true);
if (! isset($config['username'], $config['password'])) {
return;
}
$httpClient = new BiAngHttpClient($config['username'], $config['password']);
$data = null;
switch ($device->type) {
case DeviceType::Soil:
$data = $httpClient->getLatestSoilReport($device->sn);
break;
case DeviceType::Meteorological:
$data = $httpClient->getLatestMeteorologicalReport($device->sn);
break;
}
if (is_array($data) && isset($data['time'])) {
$log = DeviceLog::firstOrCreate([
'device_id' => $device->id,
'reported_at' => $data['time'],
], [
'data' => Arr::except($data, ['deviceId', 'time']),
]);
switch ($device->status) {
case DeviceStatus::Online:
// 如果设备数据在线时最近60分钟内没有数据则视为离线
if (now()->subMinutes(60)->gt($log->reported_at)) {
$device->update([
'status' => DeviceStatus::Offline,
]);
}
break;
case DeviceStatus::Offline:
// 如果设备数据离线时最近60分钟内有数据则视为在线
if (now()->subMinutes(60)->lt($log->reported_at)) {
$device->update([
'status' => DeviceStatus::Online,
]);
}
break;
}
}
}
/**
* 创建设备报告
*/
public function createReport(Device $device, Carbon $time): void
{
switch ($device->type) {
case DeviceType::Soil:
$this->createSoilReport($device, $time);
break;
case DeviceType::Meteorological:
$this->createMeteorologicalReport($device, $time);
break;
}
}
/**
* 创建土壤设备报告
*/
protected function createSoilReport(Device $device, Carbon $time): void
{
$reportedAt = $time->copy()->startOfHour();
/** @var \Illuminate\Database\Eloquent\Collection */
$logs = DeviceLog::where('device_id', $device->id)
->whereBetween('reported_at', [$reportedAt, $reportedAt->copy()->endOfHour()])
->oldest('reported_at')
->get();
if ($logs->isEmpty()) {
return;
}
$attributes = $logs->reduce(function (array $attributes, DeviceLog $log) {
if (is_array($data = $log->data)) {
foreach ($data as $k => $v) {
$attribute = match ($k) {
'soilAlkalineHydrolyzedNitrogen' => 'n',
'soilAvailablePotassium' => 'k',
'soilAvailablePhosphorus' => 'p',
'soilConductivity' => 'conductivity',
'soilTemperature' => 'temperature',
'soilMoisture' => 'humidity',
default => null,
};
if ($attribute) {
$attributes[$attribute] = $v;
}
}
}
return $attributes;
}, []);
$soilReport = SoilMonitoringLog::where([
'device_id' => $device->id,
'monitored_at' => $reportedAt,
])->first();
if ($soilReport === null) {
$lastSoilReport = SoilMonitoringLog::where([
'device_id' => $device->id,
'monitored_at' => $reportedAt->copy()->subHour(),
])->first();
$soilReport = $lastSoilReport?->replicate() ?: new SoilMonitoringLog();
$soilReport->fill([
'device_id' => $device->id,
'monitored_at' => $reportedAt,
'agricultural_base_id' => $device->agricultural_base_id,
]);
}
$soilReport->fill($attributes)->save();
}
/**
* 创建气象设备报告
*/
protected function createMeteorologicalReport(Device $device, Carbon $time): void
{
$reportedAt = $time->copy()->startOfHour();
/** @var \Illuminate\Database\Eloquent\Collection */
$logs = DeviceLog::where('device_id', $device->id)
->whereBetween('reported_at', [$reportedAt, $reportedAt->copy()->endOfHour()])
->oldest('reported_at')
->get();
if ($logs->isEmpty()) {
return;
}
$attributes = $logs->reduce(function (array $attributes, DeviceLog $log) {
if (is_array($data = $log->data)) {
foreach ($data as $k => $v) {
$attribute = match ($k) {
'rainfall' => 'moment_rainfall', //瞬时降雨量
'lightIntensity' => 'illumination',
'airTemperature' => 'air_temperature',
'airHumidity' => 'air_humidity',
'windDirection' => 'wind_degree',
'windSpeed' => 'wind_speed',
default => null,
};
if ($attribute) {
switch ($attribute) {
case 'moment_rainfall':
$attributes[$attribute] = bcadd($attributes[$attribute] ?? '0.00', $v, 2);
break;
case 'wind_degree':
$attributes['wind_direction'] = value(function ($v) {
if ($v >= 22.5 && $v < 67.5) {
return MeteorologicalMonitoringLog::WIND_DIRECTION_NORTHEAST;
} elseif ($v >= 67.5 && $v < 112.5) {
return MeteorologicalMonitoringLog::WIND_DIRECTION_EAST;
} elseif ($v >= 112.5 && $v < 157.5) {
return MeteorologicalMonitoringLog::WIND_DIRECTION_SOUTHEAST;
} elseif ($v >= 157.5 && $v < 202.5) {
return MeteorologicalMonitoringLog::WIND_DIRECTION_SOUTH;
} elseif ($v >= 202.5 && $v < 247.5) {
return MeteorologicalMonitoringLog::WIND_DIRECTION_SOUTHWEST;
} elseif ($v >= 247.5 && $v < 292.5) {
return MeteorologicalMonitoringLog::WIND_DIRECTION_WEST;
} elseif ($v >= 292.5 && $v < 337.5) {
return MeteorologicalMonitoringLog::WIND_DIRECTION_NORTHWEST;
}
return MeteorologicalMonitoringLog::WIND_DIRECTION_NORTH;
}, $v);
default:
$attributes[$attribute] = $v;
break;
}
}
}
}
return $attributes;
}, []);
# 累计雨量求和
$attributes['current_rainfall'] = DeviceLog::select(DB::raw("SUM((data->>'rainfall')::NUMERIC) as aggregate"))->where('device_id', $device->id)
->whereBetween('reported_at', [$reportedAt->copy()->startOfDay(), $reportedAt->copy()->endOfHour()])
->value('aggregate') ?: 0;
$meteorologicalReport = MeteorologicalMonitoringLog::where([
'device_id' => $device->id,
'monitored_at' => $reportedAt,
])->first();
if ($meteorologicalReport === null) {
$lastMeteorologicalReport = MeteorologicalMonitoringLog::where([
'device_id' => $device->id,
'monitored_at' => $reportedAt->copy()->subHour(),
])->first();
$meteorologicalReport = $lastMeteorologicalReport?->replicate() ?: new MeteorologicalMonitoringLog();
$meteorologicalReport->fill([
'device_id' => $device->id,
'monitored_at' => $reportedAt,
'agricultural_base_id' => $device->agricultural_base_id,
]);
}
$meteorologicalReport->fill($attributes)->save();
}
/**
* 创建设备每日报告
*/
public function createDailyReport(Device $device, Carbon $time): void
{
switch ($device->type) {
case DeviceType::Meteorological:
$this->createMeteorologicalDailyReport($device, $time);
break;
case DeviceType::Soil:
$this->createSoilDailyReport($device, $time);
break;
}
}
/**
* 创建土壤设备每日报告
*/
protected function createSoilDailyReport(Device $device, Carbon $date): void
{
/** @var \Illuminate\Database\Eloquent\Collection */
$soilReports = SoilMonitoringLog::where('device_id', $device->id)
->whereDate('monitored_at', $date)
->oldest('monitored_at')
->get();
if ($soilReports->isEmpty()) {
return;
}
$attributes = value(function ($soilReports) {
$data = [
'n' => ['sum' => 0, 'count' => 0],
'p' => ['sum' => 0, 'count' => 0],
'k' => ['sum' => 0, 'count' => 0],
'conductivity' => ['sum' => 0, 'count' => 0],
'temperature' => ['sum' => 0, 'count' => 0],
'humidity' => ['sum' => 0, 'count' => 0],
'moisture' => ['sum' => 0, 'count' => 0],
];
foreach ($soilReports as $soilReport) {
foreach ($data as $k => $item) {
if (is_null($v = $soilReport->{$k})) {
continue;
}
$item['sum'] = bcadd($item['sum'], $v, 2);
$item['count']++;
$data[$k] = $item;
}
}
$attributes = [];
foreach ($data as $key => $item) {
$attributes[$key] = $item['count'] > 0 ? round(bcdiv($item['sum'], $item['count'], 2), 2) : null;
}
return $attributes;
}, $soilReports);
/** @var \App\Models\SoilDailyReport */
$soilDailyReport = SoilMonitoringDailyLog::firstOrNew([
'device_id' => $device->id,
'monitored_at' => $date->format('Y-m-d'),
], [
'agricultural_base_id' => $device->agricultural_base_id,
]);
$soilDailyReport->fill($attributes)->save();
}
/**
* 创建气象设备每日报告
*/
protected function createMeteorologicalDailyReport(Device $device, Carbon $date): void
{
/** @var \Illuminate\Database\Eloquent\Collection */
$meteorologicalReports = MeteorologicalMonitoringLog::where('device_id', $device->id)
->whereDate('monitored_at', $date)
->oldest('monitored_at')
->get();
if ($meteorologicalReports->isEmpty()) {
return;
}
$attributes = value(function ($meteorologicalReports) {
$data = [
'current_rainfall' => 0,
'illumination' => ['sum' => 0, 'count' => 0],
'air_temperature' => ['sum' => 0, 'count' => 0],
'air_humidity' => ['sum' => 0, 'count' => 0],
'wind_speed' => ['sum' => 0, 'count' => 0],
'wind_samples' => [],
];
foreach ($meteorologicalReports as $meteorologicalReport) {
foreach ($data as $k => $item) {
if ($k === 'wind_samples') {
if (is_null($meteorologicalReport->wind_degree) || is_null($meteorologicalReport->wind_speed)) {
continue;
}
$item[] = [
'wind_degree' => $meteorologicalReport->wind_degree, // 风向度数
'wind_speed' => $meteorologicalReport->wind_speed, // 风速
];
} elseif (! is_null($v = $meteorologicalReport->{$k})) {
if ($k === 'current_rainfall') {
$item = $v;
} else {
$item['sum'] = bcadd($item['sum'], $v, 2);
$item['count']++;
}
}
$data[$k] = $item;
}
}
$attributes = [];
foreach ($data as $key => $item) {
switch ($key) {
case 'current_rainfall':
$attributes['daily_rainfall'] = $item;
break;
case 'wind_samples':
if (! empty($item)) {
$attributes['wind_degree'] = value(function (array $windSamples) {
if (empty($windSamples)) {
return null;
}
$x = 0;
$y = 0;
foreach ($windSamples as $sample) {
if ($sample['wind_degree'] == 0 && $sample['wind_speed'] == 0) {
continue;
}
// 角度转弧度
$radian = deg2rad($sample['wind_degree']);
// $x += $sample['wind_speed'] * sin($radian);
// $y += $sample['wind_speed'] * cos($radian);
$x += sin($radian);
$y += cos($radian);
}
$degree = round(rad2deg(atan2($y, $x)));
if (($x > 0 || $x < 0) && $y < 0) {
$degree += 180;
} elseif ($x < 0 && $y > 0) {
$degree += 360;
}
return $degree;
}, $item);
$attributes['wind_direction'] = value(function ($windDegree) {
if (is_null($windDegree)) {
return null;
}
if ($windDegree >= 22.5 && $windDegree < 67.5) {
return MeteorologicalMonitoringDailyLog::WIND_DIRECTION_NORTHEAST;
} elseif ($windDegree >= 67.5 && $windDegree < 112.5) {
return MeteorologicalMonitoringDailyLog::WIND_DIRECTION_EAST;
} elseif ($windDegree >= 112.5 && $windDegree < 157.5) {
return MeteorologicalMonitoringDailyLog::WIND_DIRECTION_SOUTHEAST;
} elseif ($windDegree >= 157.5 && $windDegree < 202.5) {
return MeteorologicalMonitoringDailyLog::WIND_DIRECTION_SOUTH;
} elseif ($windDegree >= 202.5 && $windDegree < 247.5) {
return MeteorologicalMonitoringDailyLog::WIND_DIRECTION_SOUTHWEST;
} elseif ($windDegree >= 247.5 && $windDegree < 292.5) {
return MeteorologicalMonitoringDailyLog::WIND_DIRECTION_WEST;
} elseif ($windDegree >= 292.5 && $windDegree < 337.5) {
return MeteorologicalMonitoringDailyLog::WIND_DIRECTION_NORTHWEST;
}
return MeteorologicalMonitoringDailyLog::WIND_DIRECTION_NORTH;
}, $attributes['wind_degree']);
}
break;
default:
$attributes[$key] = $item['count'] > 0 ? round(bcdiv($item['sum'], $item['count'], 2), 2) : null;
break;
}
}
return $attributes;
}, $meteorologicalReports);
/** @var \App\Models\MeteorologicalMonitoringDailyLog */
$meteorologicalDailyReport = MeteorologicalMonitoringDailyLog::firstOrNew([
'device_id' => $device->id,
'monitored_at' => $date,
], [
'agricultural_base_id' => $device->agricultural_base_id,
]);
$meteorologicalDailyReport->fill($attributes)->save();
}
}