dev
Jing Li 2023-08-30 20:08:00 +08:00
parent 74fc14fd90
commit 261997d651
8 changed files with 326 additions and 273 deletions

View File

@ -156,7 +156,7 @@ class DeviceLogSyncCommand extends Command
$disk->put($path, file_get_contents($item['url']));
}
WormPhoto::firstOrCreate([
WormPhoto::updateOrCreate([
'device_id' => $device->id,
'uploaded_at' => $item['time'],
], [

View File

@ -0,0 +1,158 @@
<?php
namespace App\Console\Commands\Linkos;
use App\Enums\DeviceStatus;
use App\Enums\DeviceType;
use App\Iot\Linkos\FarmClient;
use App\Models\Device;
use App\Models\WormPhoto;
use App\Models\WormReport;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Storage;
use Throwable;
class WormReportCommand extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'linkos:worm-report';
/**
* The console command description.
*
* @var string
*/
protected $description = 'linkos 虫情设备数据同步';
/**
* Execute the console command.
*/
public function handle()
{
$this->info('------------------------------------------');
$this->info(now());
try {
$this->sync();
} catch (Throwable $e) {
report($e);
}
$this->info('------------------------------------------');
}
protected function sync(): void
{
// 接口接口限制每分钟最多访问6次因此每次访问后需休眠10秒
$client = new FarmClient('xunwang', 'qwer1234');
$now = now();
/** @var \Illuminate\Database\Eloquent\Collection */
$devices = Device::supplierBy('device-supplier-linkos')
->where('type', DeviceType::Worm)
->whereIn('status', [DeviceStatus::Online, DeviceStatus::Offline])
->get();
if ($devices->isEmpty()) {
$this->warn('没有找到虫情设备');
return;
}
$this->info('==================================');
$this->info('尝试更新设备状态...');
$realTimeData = $client->realTimeData($devices->pluck('sn')->all());
foreach ($realTimeData as $item) {
foreach ($devices as $device) {
if ($item['deviceAddr'] != $device->sn) {
continue;
}
// 更新设备状态
$device->update([
'state' => $item['status'] === 'online' ? DeviceStatus::Online : DeviceStatus::Offline,
]);
}
}
$this->info("设备状态更新完成");
$this->info('==================================');
$this->info('尝试同步虫情区域统计...');
for ($i=2; $i > 0; $i--) {
$reportedAt = $now->copy()->subDays($i);
$statistics = collect(
$client->wormStatistics(
'E05F10DAIB6F4I4977IB95FI82554A48DE7C',
$reportedAt->copy()->startOfDay(),
$reportedAt->copy()->endOfDay(),
)
)->mapWithKeys(fn ($item) => [$item['deviceAddr'] => $item['wornData']]);
foreach ($devices as $device) {
$data = $statistics[$device->sn] ?? [];
WormReport::updateOrCreate([
'device_id' => $device->id,
'reported_at' => $reportedAt->toDateString(),
], [
'agricultural_base_id' => $device->agricultural_base_id,
'worm_num' => collect($data)->sum('num'),
'data' => $data,
]);
}
}
$this->info("同步虫情区域统计完成");
$this->info('==================================');
// 接口请求次数
$requests = 4;
// 同步最近7天的分析报表记录
$this->info('尝试同步分析报表记录...');
foreach ($devices->pluck('sn') as $sn) {
$data = $client->wormAnalyseData($sn, $now->copy()->subDays(7), $now, 1, 100);
foreach ($data['rows'] as $item) {
foreach ($devices as $device) {
if ($item['deviceAddr'] != $device->sn) {
continue;
}
$url = $item['analyseCoordUrl'] ?: $item['imagesUrl'];
// 下载图片
$name = md5($url);
if ($ext = pathinfo($url, PATHINFO_EXTENSION)) {
$name .= ".{$ext}";
}
$path = "worm-photos/{$device->id}/{$name}";
$disk = Storage::disk('public');
if (! $disk->exists($path)) {
$disk->put($path, file_get_contents($url));
}
WormPhoto::updateOrCreate([
'device_id' => $device->id,
'uploaded_at' => $item['createTime'],
], [
'url' => $path,
]);
}
}
$requests++;
// 接口请求频率: 每分钟6次
if ($requests == 6) {
$requests = 0;
sleep(61);
}
}
$this->info("同步分析报表记录完成");
}
}

View File

@ -1,74 +0,0 @@
<?php
namespace App\Console\Commands;
use App\Enums\DeviceType;
use App\Models\Device;
use App\Models\LinkosDeviceLog;
use App\Services\LinkosDeviceLogService;
use Carbon\Carbon;
use Illuminate\Console\Command;
class LinkosDeviceLogArchiveCommand extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'linkos:device-log-archive';
/**
* The console command description.
*
* @var string
*/
protected $description = 'LinkOS 设备流水归档';
/**
* Execute the console command.
*
* @param \App\Services\LinkosDeviceLogService $linkosDeviceLogService
* @return int
*/
public function handle(LinkosDeviceLogService $linkosDeviceLogService)
{
$devices = Device::whereIn('type', [DeviceType::Meteorological, DeviceType::Soil, DeviceType::WaterQuality])->get();
// 物联平台目前只有水质监测设备和气象监测设备
LinkosDeviceLog::orderBy('reported_at', 'asc')->lazy()->each(function ($log) use ($devices, $linkosDeviceLogService) {
if (empty($log->data)) {
return;
}
foreach ($devices as $device) {
if ($device->sn !== $log->device_id) {
continue;
}
match ($device->type) {
DeviceType::Soil => $linkosDeviceLogService->handleSoilMonitoringLog($device, $log->data, $log->reported_at),
DeviceType::Meteorological => $linkosDeviceLogService->handleMeteorologicalMonitoringLog($device, $log->data, $log->reported_at),
DeviceType::WaterQuality => $linkosDeviceLogService->handleWaterQualityMonitoringLog($device, $log->data, $log->reported_at),
};
}
});
$now = now();
$date = Carbon::parse('2022-06-01');
while ($date->lt($now)) {
foreach ($devices as $device) {
match ($device->type) {
DeviceType::Soil => $linkosDeviceLogService->handleSoilMonitoringDailyLog($device, $date),
DeviceType::Meteorological => $linkosDeviceLogService->handleMeteorologicalMonitoringDailyLog($device, $date),
DeviceType::WaterQuality => $linkosDeviceLogService->handleWaterQualityMonitoringDailyLog($device, $date),
};
}
$date->addDay();
}
return Command::SUCCESS;
}
}

View File

@ -1,197 +0,0 @@
<?php
namespace App\Console\Commands;
use App\Exceptions\BizException;
use App\Models\LinkosDeviceLog;
use App\Services\LinkosService;
use Illuminate\Console\Command;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Cache;
class LinkosDeviceLogSyncCommand extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'linkos:device-log-sync {device}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'LinkOS 设备流水同步';
/**
* @var \App\Services\LinkosService
*/
protected $linkosService;
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
$now = now();
$device = $this->argument('device');
// 最近同步时间
$lastDate = $this->getLastDate($device);
do {
if ($lastDate === null) {
$lastDate = Carbon::parse('2022-06-01');
} else {
$lastDate->addDay();
}
$start = $lastDate->copy()->startOfDay();
if ($start->gt($now)) {
throw new BizException('开始时间大约当前时间');
}
$end = $lastDate->copy()->endOfDay();
if ($end->gt($now)) {
$end = $now;
}
$this->info('----------------------------------');
$this->info('设备编号: '.$device);
$this->info('开始时间: '.$start->toDateTimeString());
$this->info('结束时间: '.$end->toDateTimeString());
$this->info('开始同步');
$this->info('...');
$this->synchronize($device, $start, $end);
$this->info('Done!');
$this->info('----------------------------------');
if ($now->isSameDay($lastDate)) {
break;
}
$this->setLastDate($device, $lastDate);
} while (true);
return Command::SUCCESS;
}
/**
* 同步设备历史数据
*
* @param string $device
* @param \Carbon\Carbon $start
* @param \Carbon\Carbon $end
* @return void
*/
protected function synchronize(string $device, Carbon $start, Carbon $end)
{
// 分页页码
$page = 0;
// 每页条数
$size = 50;
// 开始时间戳
$startTime = $start->unix() * 1000;
// 结束时间戳
$endTime = $end->unix() * 1000;
LinkosDeviceLog::where('device_id', $device)->whereBetween('reported_at', [$start, $end])->delete();
do {
$result = retry(5, function () use ($device, $page, $size, $startTime, $endTime) {
return $this->linkosService()->post('/deviceFlow/v1/list', [
'device_id' => $device,
'start_time' => $startTime,
'end_time' => $endTime,
'pageable' => [
'page' => $page,
'size' => $size,
],
]);
}, 100);
$data = collect($result['data']['content']);
$count = $data->count();
if ($count === 0) {
break;
}
$time = now();
LinkosDeviceLog::insert(
$data->map(function ($item) use ($time) {
return [
'device_id' => $item['device_id'],
'device_unit' => $item['device_unit'],
'device_category' => $item['device_category'],
'data' => ! empty($item['data']) ? json_encode($item['data']) : '{}',
'reported_at' => $item['createTime'],
'created_at' => $time->toDateTimeString(),
'updated_at' => $time->toDateTimeString(),
];
})->toArray()
);
unset($result, $data);
$page++;
} while ($count === $size);
}
/**
* @return \App\Services\LinkosService
*/
protected function linkosService(): LinkosService
{
if ($this->linkosService === null) {
$this->linkosService = app(LinkosService::class);
}
return $this->linkosService;
}
/**
* 获取设备最后同步日期
*
* @param string $device
* @return \Carbon\Carbon|null
*/
protected function getLastDate(string $device): ?Carbon
{
if (is_null($date = Cache::get($this->generateKey($device)))) {
return null;
}
return Carbon::parse($date);
}
/**
* 设置设备最后同步日期
*
* @param string $device
* @param \Carbon\Carbon $date
* @return void
*/
protected function setLastDate(string $device, Carbon $date): void
{
Cache::put($this->generateKey($device), $date->toDateString(), 86400);
}
/**
* @param string $device
* @return string
*/
protected function generateKey(string $device): string
{
return 'linkos_device_log:'.$device.'_last_sync_date';
}
}

View File

@ -171,7 +171,7 @@ class WormReportCommand extends Command
$disk->put($path, file_get_contents($url));
}
WormPhoto::firstOrCreate([
WormPhoto::updateOrCreate([
'device_id' => $log->device_id,
'uploaded_at' => $log->reported_at,
], [

View File

@ -18,6 +18,10 @@ class Kernel extends ConsoleKernel
$schedule->command(Commands\BiAng\WormStatisticsSyncCommand::class)
->hourly()
->runInBackground();
$schedule->command(Commands\Linkos\WormReportCommand::class)
->hourlyAt(15)
->runInBackground();
}
/**

View File

@ -0,0 +1,9 @@
<?php
namespace App\Exceptions;
use RuntimeException;
class LinkosFarmException extends RuntimeException
{
}

View File

@ -0,0 +1,153 @@
<?php
namespace App\Iot\Linkos;
use App\Exceptions\LinkosFarmException;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Http;
class FarmClient
{
public const ENDPOINT_URL = 'http://api.farm.0531yun.cn';
/**
* 授权令牌
*
* @var string
*/
protected $token;
/**
* 授权令牌有效期
*
* @var int
*/
protected $expires;
public function __construct(
protected readonly string $loginName,
protected readonly string $loginPwd,
) {
}
/**
* 获取设备列表
*/
public function devices(?string $groupId = null, ?string $deviceType = null): array
{
$result = $this->get('/api/v2.0/entrance/device/getsysUserDevice', [
'groupId' => $groupId,
'deviceType' => $deviceType,
]);
return $result['data'];
}
/**
* 获取用户区域
*/
public function groups(?string $groupName = null): array
{
$result = $this->get('/api/v2.0/entrance/group/getsysUserGroup', [
'groupName' => $groupName,
]);
return $result['data'];
}
/**
* 获取设备实时数据
*/
public function realTimeData(array $devices = []): array
{
$result = $this->get('/api/v2.0/entrance/device/getRealTimeData', [
'deviceAddrs' => implode(',', $devices),
]);
return $result['data'];
}
/**
* 虫情区域统计
*/
public function wormStatistics(string $groupId, Carbon $beginTime, Carbon $endTime): array
{
$result = $this->get('/api/v2.0/worm/deviceData/getWormStatisticsByGroup', [
'groupId' => $groupId,
'beginTime' => $beginTime->toDateTimeString(),
'endTime' => $endTime->toDateTimeString(),
]);
return $result['data'];
}
/**
* 虫情设备分析报表
*/
public function wormAnalyseData(string $device, Carbon $beginTime, Carbon $endTime, int $pages, int $limit = 100): array
{
$result = $this->get('/api/v2.0/worm/deviceData/getWormDataList', [
'deviceAddr' => $device,
'beginTime' => $beginTime->toDateTimeString(),
'endTime' => $endTime->toDateTimeString(),
'pages' => $pages,
'limit' => $limit,
]);
return $result['data'];
}
public function get(string $url, array $query = []): array
{
return $this->request('GET', $url, [
'query' => $query,
]);
}
public function post(string $url, array $data = []): array
{
return $this->request('POST', $url, [
'json' => $data,
]);
}
protected function request(string $method, string $url, array $options = []): array
{
$headers = [
'Content-Type' => 'application/json',
];
if ($url !== '/api/v2.0/entrance/user/userLogin') {
$headers['token'] = $this->token();
}
/** @var \Illuminate\Http\Client\Response */
$response = Http::withHeaders($headers)->baseUrl(self::ENDPOINT_URL)->send($method, $url, $options);
$json = $response->throw()->json();
if (data_get($json, 'code') === 1000) {
return $json;
}
throw new LinkosFarmException(
data_get($json, 'message', '出错啦'),
data_get($json, 'code', 0),
);
}
protected function token(): string
{
if ($this->token && $this->expires > now()->unix() + 30) {
return $this->token;
}
$result = $this->post('/api/v2.0/entrance/user/userLogin', [
'loginName' => $this->loginName,
'loginPwd' => $this->loginPwd,
]);
$this->expires = $result['data']['expDate'];
return $this->token = $result['data']['token'];
}
}