1
0
Fork 0

Merge branch 'develop' of gitea.hmily.club:liutk/internet-everythings-agricultural into develop

develop
panliang 2023-05-16 11:27:51 +08:00
commit 2660bd86bc
30 changed files with 1315 additions and 299 deletions

View File

@ -86,6 +86,10 @@ class Components extends BaseRenderer {
->options(Keyword::getByParentKey($typeKey)->pluck('name', 'id')->toArray());
}
public function keywordsTag($label = '标签'){
return amisMake()->Tag()->label($label)->displayMode('rounded')->color('inactive');
}
/**
* 生成统计图config
* 折线图或者柱状图

View File

@ -37,7 +37,7 @@ class CustomRegionController extends AdminController
}
private function regionList($categoryId){
$regionList = Region::with('devices')->where('category_id', $categoryId)->get();
$regionList = Region::with('monitorModes')->where('category_id', $categoryId)->get();
$resList = [];
foreach($regionList as $region){
$tabs = Region::regionTabConfig($region);

View File

@ -2,12 +2,13 @@
namespace App\Admin\Controllers;
use Slowlyo\OwlAdmin\Renderers\{Button, Form, Page, TableColumn, TextControl, Component, CRUDTable, Card, Video, DateRangeControl};
use Slowlyo\OwlAdmin\Renderers\{Button, Form, Page, TableColumn, TextControl, Json, Component, CRUDTable, Card, Video, DateRangeControl, Mapping, SelectControl};
use Slowlyo\OwlAdmin\Controllers\AdminController;
use App\Services\Admin\DeviceService;
use App\Models\Device;
use App\Models\Keyword;
use App\Admin\Components;
use App\Models\MonitorMode;
use App\Models\Region;
class DeviceController extends AdminController
{
@ -33,24 +34,19 @@ class DeviceController extends AdminController
TextControl::make()->name('name')->label('名称')->size('md'),
amisMake()->SelectControl()->name('factory')->label('厂家')->options(Keyword::getByParentKey('device-factory')->pluck('name', 'id')->toArray())->size('md'),
amisMake()->SelectControl()->name('type')->label('类型')->options(Device::typeMap())->size('md'),
Components::make()->keywordsTagControl('group_tags', '分组', 'device-group')->size('md'),
Button::make()->label(__('admin.reset'))->actionType('clear-and-submit'),
amis('submit')->label(__('admin.search'))->level('primary'),
]))
->quickSaveItemApi(admin_url('quick-edit/devices/$id'))
->columns([
TableColumn::make()->name('id')->label('ID')->sortable(true),
TableColumn::make()->name('name')->label('名称'),
TableColumn::make()->name('factory.name')->label('厂家'),
TableColumn::make()->name('sn')->label('编号'),
TableColumn::make()->name('type')->type('mapping')->map(Device::typeMap())->label('类型'),
TableColumn::make()->name('is_enable')->type('switch')->label('显示')->quickEdit(Components::make()->enableControl('is_enable', '', 'inline')->saveImmediately(true)),
TableColumn::make()->name('is_recommend')->type('switch')->label('推荐')->quickEdit(Components::make()->enableControl('is_recommend', '', 'inline')->saveImmediately(true)),
TableColumn::make()->name('sort')->label(__('admin.order'))->align('center')->quickEdit(Components::make()->sortControl('sort', __('admin.order'))->saveImmediately(true)),
TableColumn::make()->name('created_at')->label('创建时间')->type('datetime')->sortable(true),
TableColumn::make()->name('created_at')->label('创建时间')->type('datetime')->sortable(true),
TableColumn::make()->name('updated_at')->label('更新时间')->type('datetime')->sortable(true),
$this->rowActions(true, 'lg'),
]);
])->debug(true);
return $this->baseList($crud);
}
@ -59,7 +55,7 @@ class DeviceController extends AdminController
{
return $this->baseForm()->body([
TextControl::make()->name('name')->label('名称')->required(true),
TextControl::make()->name('sn')->label('编号')->required(true),
TextControl::make()->name('sn')->label('设备编号')->required(true),
\amisMake()->SelectControl()->name('powered_by')->label('厂家')->options(Keyword::getByParentKey('device-factory')->pluck('name', 'id')->toArray())->required(true),
TextControl::make()->name('model_sn')->label('型号'),
\amisMake()->RadiosControl()->name('type')->label('类型')->options(Device::typeMap())->required(true),
@ -68,11 +64,6 @@ class DeviceController extends AdminController
TextControl::make()->name('extends.rtsp_url')->hiddenOn('data.type != '.Device::TYPE_MONITOR)->label(__('device.rtsp_url')),
// rtsp://admin:lcdx12345@172.16.40.2:554/Streaming/tracks/5201
TextControl::make()->name('extends.rtsp_history')->hiddenOn('data.type != '.Device::TYPE_MONITOR)->label(__('device.rtsp_history')),
Components::make()->keywordsTagControl('group_tags', '分组', 'device-group'),
Components::make()->sortControl('sort', __('admin.order')),
TextControl::make()->name('is_enable')->type('switch')->default(1)->label('显示'),
TextControl::make()->name('is_recommend')->type('switch')->default(0)->label('推荐'),
]);
}
@ -82,6 +73,12 @@ class DeviceController extends AdminController
TextControl::make()->static(true)->name('id')->label('ID'),
TextControl::make()->static(true)->name('name')->label('名称'),
TextControl::make()->static(true)->name('sn')->label('编号'),
TextControl::make()->static(true)->name('factory.name')->label('厂家'),
TextControl::make()->static(true)->name('model_sn')->label('型号'),
TextControl::make()->static(true)->name('type')->label('类型')->staticSchema(
Mapping::make()->map(Device::typeMap())),
TextControl::make()->static(true)->name('extends')->label('扩展信息')->staticSchema(
Json::make()),
TextControl::make()->static(true)->name('created_at')->label('创建时间'),
TextControl::make()->static(true)->name('updated_at')->label('更新时间')
]);
@ -95,6 +92,13 @@ class DeviceController extends AdminController
if ($this->actionOfGetData()) {
return $this->response()->success($this->service->list());
}
$regionId = request()->input('region_id', 0);
if($regionId){
$region = Region::find($regionId);
$query = $region->monitorModes()->where('type', MonitorMode::TYPE_MONITOR)->pluck('name','monitor_id');
}else{
$query = MonitorMode::where('type', MonitorMode::TYPE_MONITOR)->pluck('name','id');
}
return CRUDTable::make()
->mode('cards')
->hideCheckToggler()
@ -107,7 +111,7 @@ class DeviceController extends AdminController
->footerToolbar(['statistics', 'pagination'])
->headerToolbar([])
->filter($this->baseFilter()->actions([])->body([
TextControl::make()->name('name')->label('点位名称')->size('md'),
amisMake()->SelectControl('monitor_mode', '点位名称')->size('md')->options($query->toArray())->selectFirst(true),
Button::make()->label(__('admin.reset'))->actionType('clear-and-submit'),
Component::make()->setType('submit')->label(__('admin.search'))->level('primary'),
]))

View File

@ -0,0 +1,216 @@
<?php
namespace App\Admin\Controllers;
use Slowlyo\OwlAdmin\Renderers\Page;
use Slowlyo\OwlAdmin\Renderers\Form;
use Slowlyo\OwlAdmin\Renderers\TableColumn;
use Slowlyo\OwlAdmin\Renderers\TextControl;
use Slowlyo\OwlAdmin\Controllers\AdminController;
use App\Services\Admin\MonitorModeService;
use App\Admin\Components;
use App\Models\MonitorMode;
use App\Models\Device;
use App\Models\Keyword;
/**
* @property MonitorModeService $service
*/
class MonitorModeController extends AdminController
{
protected string $serviceName = MonitorModeService::class;
public function list(): Page
{
$crud = $this->baseCRUD()
->filterTogglable(false)
->headerToolbar([
$this->createButton(true, 'lg'),
...$this->baseHeaderToolBar(),
])
->filter($this->baseFilter()->actions([])->body([
TextControl::make()->name('name')->label('名称')->size('md'),
amisMake()->SelectControl()->name('type')->label('类型')->options(MonitorMode::typeMap())->size('md'),
Components::make()->keywordsTagControl('group_tags', '分组', 'monitor-mode-group')->size('md'),
amis('button')->label(__('admin.reset'))->actionType('clear-and-submit'),
amis('submit')->label(__('admin.search'))->level('primary'),
]))
->columns([
TableColumn::make()->name('id')->label('ID')->sortable(true),
TableColumn::make()->name('name')->label('名称'),
TableColumn::make()->name('type')->type('mapping')->map(MonitorMode::typeMap())->label('类型'),
TableColumn::make()->name('tags')->type('mapping')->map(Keyword::tagsMap('monitor-mode-group'))->label('分组'),
TableColumn::make()->name('created_at')->label(__('admin.created_at'))->type('datetime')->sortable(true),
amisMake()->Operation()->label(__('admin.actions'))->buttons([
$this->setAboutDevice(),
$this->rowEditButton(true, 'lg'),
$this->rowDeleteButton()
]),
]);
return $this->baseList($crud);
}
public function form(): Form
{
return $this->baseForm()->body([
TextControl::make()->name('name')->label('名称'),
\amisMake()->RadiosControl()->name('type')->label('类型')->options(MonitorMode::typeMap())->required(true)->disabledOn('data.id > 0'),
Components::make()->keywordsTagControl('group_tags', '分组', 'monitor-mode-group'),
Components::make()->sortControl('sort', __('admin.order')),
TextControl::make()->name('is_enable')->type('switch')->default(1)->label('显示'),
TextControl::make()->name('is_recommend')->type('switch')->default(0)->label('推荐'),
]);
}
private function setAboutDevice(){
return amisMake()->DrawerAction()->label('设置监测设备')->icon('fa-solid fa-gear')->level('link')->drawer(
amisMake()->Drawer()->title('设置监测设备')->resizable(true)->closeOnOutside(true)->closeOnEsc(true)->body([
amisMake()
->Form()
->api(admin_url('monitor-mode-save-devices'))
->initApi($this->getEditGetDataPath())
->mode('normal')
->data(['id' => '${id}'])
->body([
amisMake()->RadiosControl()->name('type')->label('类型')->options(MonitorMode::typeMap())->disabled(true),
amisMake()->PickerControl('picker_devices', '监控设备')->visibleOn('data.type == '.MonitorMode::TYPE_MONITOR)
->valueField('id')
->labelField('name')
->multiple(true)->joinValues(false)->extractValue(true)
->size('lg')
->source([
'method' => 'get',
'url' => admin_url('devices?_action=getData&type='.Device::TYPE_MONITOR),
'data' => [
'name'=>'${device_name}',
]
])
->pickerSchema(
[
'mode' => 'table',
'name' => 'monitor_list',
'headerToolbar' => amisMake()->form()
->wrapWithPanel(false)
->className('text-right')
->target('monitor_list')
->mode('inline')
->body([
amisMake()->TextControl('device_name', '名称')->addOn(
amis('submit')->label(__('admin.search'))->level('primary')
)
]),
'columns' => [
TableColumn::make()->name('id')->label('ID')->sortable(true),
TableColumn::make()->name('name')->label('名称'),
TableColumn::make()->name('sn')->label('编号'),
TableColumn::make()->name('created_at')->label('创建时间')->type('datetime')->sortable(true),
TableColumn::make()->name('updated_at')->label('更新时间')->type('datetime')->sortable(true),
]
]
),
amisMake()->ArrayControl('array_devices', '土壤监测')->visibleOn('data.type == '.MonitorMode::TYPE_SOIL)
->items(
amisMake()->ComboControl()->items([
\amisMake()->CheckboxesControl('device_fields', '监测字段')->checkAll(true)->options(MonitorMode::fieldMap(MonitorMode::TYPE_SOIL)),
\amisMake()->SelectControl('device_id', '监测设备')->options(Device::where('type', Device::TYPE_SOIL)->get()->pluck('name', 'id')),
]),
),
amisMake()->ArrayControl('array_devices', '水质监测')->visibleOn('data.type == '.MonitorMode::TYPE_WATER_QUALITY)
->items(
amisMake()->ComboControl()->items([
\amisMake()->CheckboxesControl('device_fields', '监测字段')->checkAll(true)->options(MonitorMode::fieldMap(MonitorMode::TYPE_WATER_QUALITY)),
\amisMake()->SelectControl('device_id', '监测设备'),
]),
),
amisMake()->ArrayControl('array_devices', '气象监测')->visibleOn('data.type == '.MonitorMode::TYPE_METEOROLOGICAL)
->items(
amisMake()->ComboControl()->items([
\amisMake()->CheckboxesControl('device_fields', '监测字段')->checkAll(true)->options(MonitorMode::fieldMap(MonitorMode::TYPE_METEOROLOGICAL)),
\amisMake()->SelectControl('device_id', '监测设备'),
]),
),
amisMake()->PickerControl('picker_devices', '通风设备')->visibleOn('data.type == '.MonitorMode::TYPE_AIR)
->valueField('id')
->labelField('name')
->multiple(true)
->size('lg')
->source([
'method' => 'get',
'url' => admin_url('devices?_action=getData&type='.Device::TYPE_AIR),
'data' => [
'name'=>'${device_name}',
]
])
->pickerSchema(
[
'mode' => 'table',
'name' => 'monitor_list',
'headerToolbar' => amisMake()->form()
->wrapWithPanel(false)
->className('text-right')
->target('monitor_list')
->mode('inline')
->body([
amisMake()->TextControl('device_name', '名称')->addOn(
amis('submit')->label(__('admin.search'))->level('primary')
)
]),
'columns' => [
TableColumn::make()->name('id')->label('ID')->sortable(true),
TableColumn::make()->name('name')->label('名称'),
TableColumn::make()->name('sn')->label('编号'),
TableColumn::make()->name('created_at')->label('创建时间')->type('datetime')->sortable(true),
TableColumn::make()->name('updated_at')->label('更新时间')->type('datetime')->sortable(true),
]
]
),
amisMake()->PickerControl('picker_devices', '喷雾设备')->visibleOn('data.type == '.MonitorMode::TYPE_ATOMIZING)
->valueField('id')
->labelField('name')
->multiple(true)
->size('lg')
->source([
'method' => 'get',
'url' => admin_url('devices?_action=getData&type='.Device::TYPE_ATOMIZING),
'data' => [
'name'=>'${device_name}',
]
])
->pickerSchema(
[
'mode' => 'table',
'name' => 'monitor_list',
'headerToolbar' => amisMake()->form()
->wrapWithPanel(false)
->className('text-right')
->target('monitor_list')
->mode('inline')
->body([
amisMake()->TextControl('device_name', '名称')->addOn(
amis('submit')->label(__('admin.search'))->level('primary')
)
]),
'columns' => [
TableColumn::make()->name('id')->label('ID')->sortable(true),
TableColumn::make()->name('name')->label('名称'),
TableColumn::make()->name('sn')->label('编号'),
TableColumn::make()->name('created_at')->label('创建时间')->type('datetime')->sortable(true),
TableColumn::make()->name('updated_at')->label('更新时间')->type('datetime')->sortable(true),
]
]
),
])
])->footer([
amisMake()->Button()->label('保存')->type('submit')->level('primary'),
])
);
}
public function saveDevices()
{
$result = $this->service->saveDevices(request('id'), request());
return $this->autoResponse($result, __('admin.save'));
}
}

View File

@ -5,16 +5,12 @@ namespace App\Admin\Controllers;
use Slowlyo\OwlAdmin\Renderers\Page;
use Slowlyo\OwlAdmin\Renderers\Form;
use Slowlyo\OwlAdmin\Renderers\TableColumn;
use Slowlyo\OwlAdmin\Renderers\TextControl;
use Slowlyo\OwlAdmin\Controllers\AdminController;
use App\Services\Admin\RegionService;
use Slowlyo\OwlAdmin\Renderers\Button;
use Slowlyo\OwlAdmin\Renderers\Image;
use App\Admin\Components;
use App\Models\Device;
use App\Models\Region;
use Slowlyo\OwlAdmin\Renderers\Html;
use Illuminate\Http\Request;
use App\Models\MonitorMode;
class RegionController extends AdminController
{
@ -46,7 +42,6 @@ class RegionController extends AdminController
TableColumn::make()->name('category.name')->label('分类')->className('text-primary'),
TableColumn::make()->name('director')->label('负责人'),
TableColumn::make()->name('area')->label('面积m²'),
TableColumn::make()->name('devices_count')->label('设备数量'),
TableColumn::make()->name('is_enable')->type('switch')->label('显示')->quickEdit(Components::make()->enableControl('is_enable', '', 'inline')->saveImmediately(true)),
TableColumn::make()->name('created_at')->label('创建时间')->type('datetime')->sortable(true),
// TableColumn::make()->name('updated_at')->label('更新时间')->type('datetime')->sortable(true),
@ -55,7 +50,6 @@ class RegionController extends AdminController
$this->rowEditButton(true, 'lg'),
$this->rowDeleteButton()
]),
]);
return $this->baseList($crud);
@ -63,12 +57,12 @@ class RegionController extends AdminController
public function form(): Form
{
$deviceType = [];
$devices = Device::get()->groupBy('type');
foreach(Device::typeMap() as $key => $item) {;
$deviceType[] = [
$monitorModeType = [];
$monitorModes = MonitorMode::get()->groupBy('type');
foreach(MonitorMode::typeMap() as $key => $item) {;
$monitorModeType[] = [
'label' => $item,
'children' => $devices->get($key)?->pluck('name', 'id')->toArray(),
'children' => $monitorModes->get($key)?->pluck('name', 'id')->toArray(),
];
}
return $this->baseForm()->body([
@ -80,25 +74,10 @@ class RegionController extends AdminController
Components::make()->decimalControl('area', '面积m²'),
Components::make()->sortControl(),
\amisMake()->SwitchControl()->name('is_enable')->value(1)->label('显示'),
\amisMake()->TransferControl()->name('devices')->label('关联设备')
\amisMake()->TransferControl()->name('monitorModes')->label('关联监测')
->selectMode('chained')->searchable(true)
->joinValues(false)->extractValue(true)
->options($deviceType),
]);
}
public function detail(): Form
{
return $this->baseDetail()->body([
TextControl::make()->static(true)->name('id')->label('ID'),
TextControl::make()->static(true)->name('name')->label('名称'),
TextControl::make()->static(true)->name('category.name')->label('分类'),
TextControl::make()->static(true)->name('director')->label('负责人'),
TextControl::make()->name('cover')->label('封面')->static(true)->staticSchema(Image::make()),
TextControl::make()->static(true)->name('area')->label('面积m²'),
TextControl::make()->name('description')->label('内容介绍')->static(true)->staticSchema(Html::make()),
TextControl::make()->static(true)->name('created_at')->label('创建时间'),
TextControl::make()->static(true)->name('updated_at')->label('更新时间')
->options($monitorModeType),
]);
}

View File

@ -44,11 +44,15 @@ Route::group([
$router->resource('keywords', \App\Admin\Controllers\KeywordController::class);
//设备管理
$router->resource('devices', \App\Admin\Controllers\DeviceController::class);
$router->post('quick-edit/devices/{device}',[\App\Admin\Controllers\DeviceController::class, 'update']);
//设备预警
$router->get('warning-setting', '\App\Admin\Controllers\WarningSettingController@settingIndex');
$router->get('warning-notice', '\App\Admin\Controllers\WarningNoticeController@index');
//监测点位
$router->resource('monitor-modes', \App\Admin\Controllers\MonitorModeController::class);
$router->post('monitor-mode-save-devices', '\App\Admin\Controllers\MonitorModeController@saveDevices');
//区域分类
$router->resource('region-categories', \App\Admin\Controllers\RegionCategoryController::class);
$router->post('quick-edit/region-categories/{region_category}', '\App\Admin\Controllers\RegionCategoryController@update');

View File

@ -0,0 +1,125 @@
<?php
namespace App\Console\Commands;
use App\Models\Device;
use App\Models\MeteorologicalDailyReport;
use App\Models\MeteorologicalReport;
use App\Models\WaterQualityDailyReport;
use App\Models\WaterQualityReport;
use App\Services\DeviceLogService;
use Illuminate\Console\Command;
class DeviceLogDailyReportCommand extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'device-log:daily-report
{factory}
{--sleep=300 : 监控报告生产后的休眠时间(秒)}';
/**
* The console command description.
*
* @var string
*/
protected $description = '按设备厂商生成监控报告';
/**
* @var \App\Services\DeviceLogService
*/
protected $deviceLogService;
/**
* Execute the console command.
*/
public function handle(DeviceLogService $deviceLogService)
{
$this->deviceLogService = $deviceLogService;
$factory = $this->argument('factory');
$sleep = (int) value(fn ($sleep) => is_numeric($sleep) ? $sleep : 300, $this->option('sleep'));
while (true) {
/** @var \Illuminate\Database\Eloquent\Collection */
$devices = Device::with(['factory'])->poweredBy($factory)->get();
foreach ($devices as $device) {
switch ($device->factory?->key) {
case 'link-os':
$this->createReportToLinkosDevice($device);
break;
}
}
sleep($sleep);
};
}
/**
* 创建 linkos 设备报告
*/
protected function createReportToLinkosDevice(Device $device): void
{
$lastReportedAt = null;
switch ($device->type) {
case Device::TYPE_WATER_QUALITY:
$lastReportedAt = WaterQualityDailyReport::where('device_id', $device->id)
->latest('reported_at')
->value('reported_at');
$lastReportedAt ??= WaterQualityReport::where('device_id', $device->id)
->oldest('reported_at')
->value('reported_at');
break;
case Device::TYPE_METEOROLOGICAL:
$lastReportedAt = MeteorologicalDailyReport::where('device_id', $device->id)
->latest('reported_at')
->value('reported_at');
$lastReportedAt ??= MeteorologicalReport::where('device_id', $device->id)
->oldest('reported_at')
->value('reported_at');
break;
}
if ($lastReportedAt === null) {
return;
}
$latestReportedAt = null;
switch ($device->type) {
case Device::TYPE_WATER_QUALITY:
$latestReportedAt = WaterQualityReport::where('device_id', $device->id)
->latest('reported_at')
->value('reported_at');
break;
case Device::TYPE_METEOROLOGICAL:
$latestReportedAt = MeteorologicalReport::where('device_id', $device->id)
->latest('reported_at')
->value('reported_at');
break;
}
if ($latestReportedAt === null) {
return;
}
/** @var \Carbon\Carbon */
$startAt = $lastReportedAt->copy()->startOfDay();
do {
$this->deviceLogService->createDailyReportToLinkosDevice($device, $startAt->copy());
$startAt->addDay();
} while ($latestReportedAt->gte($startAt));
}
}

View File

@ -4,10 +4,9 @@ namespace App\Console\Commands;
use App\Models\Device;
use App\Models\MeteorologicalReport;
use App\Models\SoilReport;
use App\Models\WaterQualityReport;
use App\Services\DeviceLogService;
use Illuminate\Console\Command;
use Illuminate\Support\Arr;
class DeviceLogReportCommand extends Command
{
@ -27,11 +26,18 @@ class DeviceLogReportCommand extends Command
*/
protected $description = '按设备厂商生成监控报告';
/**
* @var \App\Services\DeviceLogService
*/
protected $deviceLogService;
/**
* Execute the console command.
*/
public function handle()
public function handle(DeviceLogService $deviceLogService)
{
$this->deviceLogService = $deviceLogService;
$factory = $this->argument('factory');
$sleep = (int) value(fn ($sleep) => is_numeric($sleep) ? $sleep : 300, $this->option('sleep'));
@ -52,225 +58,32 @@ class DeviceLogReportCommand extends Command
};
}
/**
* 创建 linkos 设备报告
*/
protected function createReportToLinkosDevice(Device $device): void
{
switch ($device->type) {
case Device::TYPE_SOIL:
$this->createReportToLinkosSoilDevice($device);
break;
$lastReportedAt = match ($device->type) {
Device::TYPE_WATER_QUALITY => WaterQualityReport::where('device_id', $device->id)->latest('reported_at')->value('reported_at'),
Device::TYPE_METEOROLOGICAL => MeteorologicalReport::where('device_id', $device->id)->latest('reported_at')->value('reported_at'),
default => null,
};
case Device::TYPE_METEOROLOGICAL:
$this->createReportToLinkosMeteorologicalDevice($device);
break;
case Device::TYPE_WATER_QUALITY:
$this->createReportToLinkosWaterQualityDevice($device);
break;
}
}
protected function createReportToLinkosSoilDevice(Device $device): void
{
$lastSoilReport = SoilReport::where('device_id', $device->id)
->latest('reported_at')
->first();
$lastReportedAt = $lastSoilReport?->reported_at
?: $device->logs()->oldest('reported_at')->value('reported_at');
if ($lastReportedAt === null) {
if (is_null($lastReportedAt ??= $device->logs()->oldest('reported_at')->value('reported_at'))) {
return;
}
$latestReportedAt = $device->logs()->latest('reported_at')->value('reported_at');
if ($latestReportedAt === null) {
if (is_null($latestReportedAt = $device->logs()->latest('reported_at')->value('reported_at'))) {
return;
}
/** @var \Carbon\Carbon */
$startAt = $lastReportedAt->copy()->startOfHour();
/** @var \Carbon\Carbon */
$endAt = $latestReportedAt->copy()->startOfHour();
do {
/** @var \Illuminate\Database\Eloquent\Collection */
$logs = $device->logs()
->whereBetween('reported_at', [$startAt, $startAt->copy()->endOfHour()])
->oldest('reported_at')
->get();
if ($logs->isNotEmpty()) {
$soilReport = SoilReport::firstOrCreate(
[
'device_id' => $device->id,
'reported_at' => $startAt,
],
Arr::except($lastSoilReport?->setHidden([])?->attributesToArray() ?: [], ['reported_at'])
);
/** @var \App\Models\DeviceLog */
foreach ($logs as $log) {
foreach ([
'conductivity' => 'conductivity',
'soil_humidity' => 'humidity',
'soil_temperature' => 'temperature',
'nitrogen_content' => 'n',
'potassium_content' => 'k',
'phosphorus_content' => 'p',
] as $key => $attribute) {
if (! is_array($log->data) || ! array_key_exists($key, $log->data)) {
continue;
}
$soilReport->{$attribute} = $log->data[$key];
}
$lastSoilReport = tap($soilReport)->save();
}
}
$this->deviceLogService->createReportToLinkosDevice($device, $startAt->copy());
$startAt->addHour();
} while ($endAt->gte($startAt));
}
protected function createReportToLinkosMeteorologicalDevice(Device $device): void
{
$lastMeteorologicalReport = MeteorologicalReport::where('device_id', $device->id)
->latest('reported_at')
->first();
$lastReportedAt = $lastMeteorologicalReport?->reported_at
?: $device->logs()->oldest('reported_at')->value('reported_at');
if ($lastReportedAt === null) {
return;
}
$latestReportedAt = $device->logs()->latest('reported_at')->value('reported_at');
if ($latestReportedAt === null) {
return;
}
/** @var \Carbon\Carbon */
$startAt = $lastReportedAt->copy()->startOfHour();
/** @var \Carbon\Carbon */
$endAt = $latestReportedAt->copy()->startOfHour();
do {
/** @var \Illuminate\Database\Eloquent\Collection */
$logs = $device->logs()
->whereBetween('reported_at', [$startAt, $startAt->copy()->endOfHour()])
->oldest('reported_at')
->get();
if ($logs->isNotEmpty()) {
$meteorologicalReport = MeteorologicalReport::firstOrCreate(
[
'device_id' => $device->id,
'reported_at' => $startAt,
],
Arr::except($lastMeteorologicalReport?->setHidden([])?->attributesToArray() ?: [], ['reported_at'])
);
/** @var \App\Models\DeviceLog */
foreach ($logs as $log) {
foreach ([
'current_rainfall' => 'today_rainfall',
'day_rainfall' => 'yesterday_rainfall',
'accumulate_rainfall' => 'accumulate_rainfall',
'moment_rainfall' => 'moment_rainfall',
'pm10_concentration' => 'pm10',
'pm25_concentration' => 'pm25',
'box_illumination' => 'box_illumination',
'box_pressure' => 'box_pressure',
'box_carbon' => 'box_co2',
'box_temperature' => 'box_temperature',
'box_humidity' => 'box_humidity',
'box_noise' => 'box_noise',
'wind_degree' => 'wind_degree',
'wind_direction' => 'wind_direction',
'wind_power' => 'wind_power',
'wind_speed' => 'wind_speed',
] as $key => $attribute) {
if (! is_array($log->data) || ! array_key_exists($key, $log->data)) {
continue;
}
$meteorologicalReport->{$attribute} = $log->data[$key];
}
$lastMeteorologicalReport = tap($meteorologicalReport)->save();
}
}
$startAt->addHour();
} while ($endAt->gte($startAt));
}
protected function createReportToLinkosWaterQualityDevice(Device $device): void
{
$lastWaterQualityReport = WaterQualityReport::where('device_id', $device->id)
->latest('reported_at')
->first();
$lastReportedAt = $lastWaterQualityReport?->reported_at
?: $device->logs()->oldest('reported_at')->value('reported_at');
if ($lastReportedAt === null) {
return;
}
$latestReportedAt = $device->logs()->latest('reported_at')->value('reported_at');
if ($latestReportedAt === null) {
return;
}
/** @var \Carbon\Carbon */
$startAt = $lastReportedAt->copy()->startOfHour();
/** @var \Carbon\Carbon */
$endAt = $latestReportedAt->copy()->startOfHour();
do {
/** @var \Illuminate\Database\Eloquent\Collection */
$logs = $device->logs()
->whereBetween('reported_at', [$startAt, $startAt->copy()->endOfHour()])
->oldest('reported_at')
->get();
if ($logs->isNotEmpty()) {
$waterQualityReport = WaterQualityReport::firstOrCreate(
[
'device_id' => $device->id,
'reported_at' => $startAt,
],
Arr::except($lastWaterQualityReport?->setHidden([])?->attributesToArray() ?: [], ['reported_at'])
);
/** @var \App\Models\DeviceLog */
foreach ($logs as $log) {
foreach ([
'chlorine' => 'chlorine',
'conductivity' => 'conductivity',
'oxygen' => 'oxygen',
'ph' => 'ph',
'temp' => 'temperature',
'turbidity' => 'turbidity',
] as $key => $attribute) {
if (! is_array($log->data) || ! array_key_exists($key, $log->data)) {
continue;
}
$waterQualityReport->{$attribute} = $log->data[$key];
}
$lastWaterQualityReport = tap($waterQualityReport)->save();
}
}
$startAt->addHour();
} while ($endAt->gte($startAt));
} while ($latestReportedAt->gte($startAt));
}
}

View File

@ -4,6 +4,7 @@ namespace App\Filters\Admin;
use EloquentFilter\ModelFilter;
use App\Models\Device;
use App\Models\MonitorMode;
class DeviceFilter extends ModelFilter
{
@ -28,6 +29,13 @@ class DeviceFilter extends ModelFilter
return $this->where('type', $type);
}
public function monitorMode($monitorMode){
if($monitorMode){
$deviceIds = MonitorMode::find($monitorMode)?->devices()->get()->pluck('id')->toArray();
}
return $this->whereIn('id', $deviceIds);
}
/**
* 类型
*/

View File

@ -0,0 +1,43 @@
<?php
namespace App\Filters\Admin;
use EloquentFilter\ModelFilter;
use App\Models\Device;
class MonitorModeFilter extends ModelFilter
{
/**
* 名称
*/
public function name($name){
return $this->where('name', 'like', '%'.$name.'%');
}
/**
* 类型
*/
public function type($type){
return $this->where('type', $type);
}
/**
* 类型
*/
public function typeName($typeName){
$type = 0;
foreach(Device::typeMap() as $key => $item){
if($item == $typeName){
$type = $key;
break;
}
};
return $this->where('type', $type);
}
/**
* 分组
*/
public function groupTags($groupTags){
return $this->whereRaw("FIND_IN_SET(group_tags,'$groupTags')");
}
}

View File

@ -17,6 +17,7 @@ class Device extends Model
public const TYPE_METEOROLOGICAL = 4; //气象设备
public const TYPE_AIR = 5; //通风设备
public const TYPE_ATOMIZING = 6; //喷雾设备
public const TYPE_INSECT = 7; //虫情监测
public const STATE_DISABLED = 0;
public const STATE_ONLINE = 1;
@ -28,9 +29,7 @@ class Device extends Model
];
protected $fillable = [
'name', 'sn', 'powered_by', 'type', 'model_sn', 'state', 'extends',
'is_enable', 'sort', 'is_recommend',
'group_tags'
'name', 'sn', 'powered_by', 'type', 'model_sn', 'state', 'extends'
];
public function scopePoweredBy(Builder $query, string $factory): void
@ -48,10 +47,11 @@ class Device extends Model
return [
self::TYPE_MONITOR => '监控设备',
self::TYPE_SOIL => '土壤设备',
self::TYPE_WATER_QUALITY => '水质设备',
// self::TYPE_WATER_QUALITY => '水质设备',
self::TYPE_METEOROLOGICAL => '气象设备',
self::TYPE_AIR => '通风设备',
self::TYPE_ATOMIZING => '喷雾设备'
self::TYPE_ATOMIZING => '喷雾设备',
self::TYPE_INSECT => '虫情监测',
];
}

View File

@ -6,6 +6,7 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use EloquentFilter\Filterable;
use Illuminate\Database\Eloquent\Builder;
use App\Admin\Components;
class Keyword extends Model
{
@ -54,4 +55,10 @@ class Keyword extends Model
return self::query()->where('type_key', $key)->get();
}
public static function tagsMap(String $key){
$list = self::query()->where('type_key', $key)->get()->pluck('name', 'id')->toArray();
return array_map(function($item){
return Components::make()->keywordsTag($item);
}, $list);
}
}

View File

@ -0,0 +1,45 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class MeteorologicalDailyReport extends Model
{
use HasFactory;
const WIND_DIRECTION_NORTH = 0;
const WIND_DIRECTION_NORTHEAST = 1;
const WIND_DIRECTION_EAST = 2;
const WIND_DIRECTION_SOUTHEAST = 3;
const WIND_DIRECTION_SOUTH = 4;
const WIND_DIRECTION_SOUTHWEST = 5;
const WIND_DIRECTION_WEST = 6;
const WIND_DIRECTION_NORTHWEST = 7;
protected $casts = [
'reported_at' => 'date'
];
protected $fillable = [
'device_id',
'today_rainfall',
'yesterday_rainfall',
'accumulate_rainfall',
'moment_rainfall',
'pm10',
'pm25',
'box_illumination',
'box_pressure',
'box_co2',
'box_temperature',
'box_humidity',
'box_noise',
'wind_degree',
'wind_direction',
'wind_power',
'wind_speed',
'reported_at',
];
}

View File

@ -0,0 +1,13 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class MonitorDevice extends Model
{
use HasFactory;
public $timestamps = false;
}

View File

@ -0,0 +1,92 @@
<?php
namespace App\Models;
use EloquentFilter\Filterable;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Casts\Attribute;
class MonitorMode extends Model
{
use Filterable;
protected $fillable = [
'name', 'type',
'is_enable', 'sort', 'is_recommend',
'group_tags'
];
protected $appends = ['tags'];
public const TYPE_MONITOR = 1; //视频监控
public const TYPE_SOIL = 2; //土壤监测
public const TYPE_WATER_QUALITY = 3; //水质监测
public const TYPE_METEOROLOGICAL = 4; //气象监测
public const TYPE_AIR = 5; //通风控制
public const TYPE_ATOMIZING = 6; //喷雾控制
public const TYPE_INSECT = 7; //虫情监测
public static function typeMap()
{
return [
self::TYPE_MONITOR => '视频监控',
self::TYPE_SOIL => '土壤监测',
// self::TYPE_WATER_QUALITY => '水质监测',
self::TYPE_METEOROLOGICAL => '气象监测',
self::TYPE_AIR => '通风控制',
self::TYPE_ATOMIZING => '喷雾控制',
self::TYPE_INSECT => '虫情监测',
];
}
public static function fieldMap($type)
{
$arr = [];
switch ($type) {
case self::TYPE_SOIL:
$arr = [
'conductivity'=>'导电率',
'humidity'=>'湿度',
'temperature'=>'温度',
'n'=>'氮',
'p'=>'磷',
'k'=>'钾'
];
break;
case self::TYPE_WATER_QUALITY:
$arr = [
];
break;
case self::TYPE_METEOROLOGICAL:
$arr = [
'box_temperature' => '温度',
'box_humidity' => '湿度',
'box_illumination' => '光照强度',
'moment_rainfall' => '降雨量',
'wind_speed' => '风速',
'wind_direction' => '风向',
'box_noise' => '噪音',
'pm10' => 'PM10',
'pm25' => 'PM25',
'box_co2' => 'CO2'
];
break;
}
return $arr;
}
protected function serializeDate(\DateTimeInterface $date){
return $date->format('Y-m-d H:i:s');
}
protected function tags():Attribute
{
return Attribute::make(
get: fn($value) => $this->group_tags ? explode(',', $this->group_tags) : [],
);
}
public function devices(){
return $this->belongsToMany(Device::class, MonitorDevice::class, 'monitor_id', 'device_id')->withPivot('fields');
}
}

View File

@ -12,7 +12,6 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
*/
class Region extends Model
{
use HasFactory;
use Filterable;
protected $fillable = [
@ -44,10 +43,8 @@ class Region extends Model
return $this->belongsTo(RegionCategory::class, 'category_id');
}
// 关联设备
public function devices()
{
return $this->belongsToMany(Device::class, RegionDevice::class, 'region_id', 'device_id')->withTimestamps();
public function monitorModes(){
return $this->belongsToMany(MonitorMode::class, RegionMonitor::class, 'region_id', 'monitor_id')->withTimestamps();
}
// 种植记录
@ -78,47 +75,47 @@ class Region extends Model
'sort' => 0,
],
];
if($region?->devices){
foreach($region->devices as $device){
switch($device->type)
if($region?->monitorModes){
foreach($region->monitorModes as $monitorMode){
switch($monitorMode->type)
{
case Device::TYPE_MONITOR:
case MonitorMode::TYPE_MONITOR:
$tabs[] = [//有监控设备才有
'title' => '监控视频',
'value' => 'monitor',
'tab'=>\amisMake()->Service()->schemaApi(admin_url('custom-region-monitor?id='.$region->id)),
'tab'=>\amisMake()->Service()->schemaApi(admin_url('custom-region-monitor?region_id='.$region->id)),
'unmountOnExit' => true,//每次切换tab都要销毁
'sort' => 1,
];
break;
case Device::TYPE_SOIL:
case MonitorMode::TYPE_SOIL:
$tabs[] = [//有土壤设备才有
'title' => '土壤数据',
'value' => 'turang',
'value' => 'soil',
'tab'=>\amisMake()->Service()->schemaApi(admin_url('custom-region-soil?id='.$region->id)),
'unmountOnExit' => true,//每次切换tab都要销毁
'sort' => 2,
];
break;
case Device::TYPE_WATER_QUALITY:
case MonitorMode::TYPE_WATER_QUALITY:
$tabs[] = [//有水质设备才有
'title' => '水质数据',
'value' => 'shuizi',
'value' => 'quality',
'tab'=>\amisMake()->Service()->schemaApi(admin_url('custom-region-water?id='.$region->id)),
'unmountOnExit' => true,//每次切换tab都要销毁
'sort' => 3,
];
break;
case Device::TYPE_METEOROLOGICAL:
case MonitorMode::TYPE_METEOROLOGICAL:
$tabs[] = [//有气象设备才有
'title' => '气象数据',
'value' => 'qixiang',
'value' => 'meteorological',
'tab'=>\amisMake()->Service()->schemaApi(admin_url('custom-region-meteorological?id='.$region->id)),
'unmountOnExit' => true,//每次切换tab都要销毁
'sort' => 4,
];
break;
case Device::TYPE_AIR:
case MonitorMode::TYPE_AIR:
$tabs[] = [//有通风设备才有
'title' => '通风设备',
'value' => 'air',
@ -127,11 +124,11 @@ class Region extends Model
'sort' => 5,
];
break;
case Device::TYPE_ATOMIZING:
case MonitorMode::TYPE_ATOMIZING:
$tabs[] = [//有喷雾设备才有
'title' => '喷雾设备',
'value' => 'wasserstrahl',
'tab'=>\amisMake()->Service()->schemaApi(admin_url('custom-region-wasserstrahl?id='.$region->id)),
'value' => 'atomizing',
'tab'=>\amisMake()->Service()->schemaApi(admin_url('custom-region-atomizing?id='.$region->id)),
'unmountOnExit' => true,//每次切换tab都要销毁
'sort' => 6,
];

View File

@ -5,7 +5,7 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class RegionDevice extends Model
class RegionMonitor extends Model
{
use HasFactory;
}

View File

@ -0,0 +1,26 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class WaterQualityDailyReport extends Model
{
use HasFactory;
protected $casts = [
'reported_at' => 'date'
];
protected $fillable = [
'device_id',
'chlorine',
'conductivity',
'oxygen',
'ph',
'temperature',
'turbidity',
'reported_at',
];
}

View File

@ -0,0 +1,78 @@
<?php
namespace App\Services\Admin;
use App\Models\MonitorMode;
use App\Filters\Admin\MonitorModeFilter;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Builder;
/**
* @method MonitorMode getModel()
* @method MonitorMode|\Illuminate\Database\Query\Builder query()
*/
class MonitorModeService extends BaseService
{
protected string $modelName = MonitorMode::class;
protected string $modelFilterName = MonitorModeFilter::class;
public function getEditData($id): Model|\Illuminate\Database\Eloquent\Collection|Builder|array|null
{
$region = parent::getEditData($id);
//处理详情数据;
$region->load('devices');
switch($region->type) {
case MonitorMode::TYPE_MONITOR:
case MonitorMode::TYPE_AIR:
case MonitorMode::TYPE_ATOMIZING:
$region->offsetSet('picker_devices', $region->devices->pluck('id')->toArray());
break;
case MonitorMode::TYPE_SOIL:
case MonitorMode::TYPE_WATER_QUALITY:
case MonitorMode::TYPE_METEOROLOGICAL:
$region->offsetSet('array_devices', $region->devices->map(function($item, $key){
return [
'device_id' => $item->id,
'device_fields' => explode(',', $item->pivot->fields ?? ''),
];
}));
break;
}
return $region;
}
public function saveDevices($primaryKey, $params){
$model = $this->query()->whereKey($primaryKey)->first();
$type = $params['type'] ?? null;
$syncData = [];
switch($type){
case MonitorMode::TYPE_MONITOR:
case MonitorMode::TYPE_AIR:
case MonitorMode::TYPE_ATOMIZING:
$syncData = $params['picker_devices'] ?? [];
break;
case MonitorMode::TYPE_SOIL:
case MonitorMode::TYPE_WATER_QUALITY:
case MonitorMode::TYPE_METEOROLOGICAL:
$arrayDevices = $params['array_devices'] ?? [];
if($arrayDevices){
foreach($arrayDevices as $arrayDevice){
//过滤重复设备
if(isset($syncData[$arrayDevice['device_id']])){
return $this->setError('请勿重复选择监测设备');
}
$syncData[$arrayDevice['device_id']] = [
'fields' => $arrayDevice['device_fields']
];
}
}
break;
}
return $model->devices()->sync($syncData);
}
}

View File

@ -25,7 +25,7 @@ class RegionService extends BaseService
public function getEditData($id): Model|\Illuminate\Database\Eloquent\Collection|Builder|array|null
{
$region = parent::getEditData($id);
$region->devices = $region->devices()->selectRaw('devices.id as value, devices.name as label')->get()->toArray();
$region->monitorModes = $region->monitorModes()->get()->pluck('id')->toArray();
return $region;
}
@ -45,8 +45,8 @@ class RegionService extends BaseService
DB::beginTransaction();
if($model->save()){
//处理关联设备
$devices = Arr::get($data, 'devices');
$model->devices()->sync($devices ?? []);
$monitorModes = Arr::get($data, 'monitorModes');
$model->monitorModes()->sync($monitorModes ?? []);
}
DB::commit();
return true;
@ -73,8 +73,8 @@ class RegionService extends BaseService
DB::beginTransaction();
if($model->save()){
//处理关联设备
$devices = Arr::get($data, 'devices');
$model->devices()->sync($devices ?? []);
$monitorModes = Arr::get($data, 'monitorModes');
$model->monitorModes()->sync($monitorModes ?? []);
}
DB::commit();
return true;

View File

@ -4,6 +4,11 @@ namespace App\Services;
use App\Iot\Linkos\HttpClient as LinkosHttpClient;
use App\Models\Device;
use App\Models\DeviceLog;
use App\Models\MeteorologicalDailyReport;
use App\Models\MeteorologicalReport;
use App\Models\WaterQualityDailyReport;
use App\Models\WaterQualityReport;
use Illuminate\Support\Arr;
use Illuminate\Support\Carbon;
@ -73,4 +78,353 @@ class DeviceLogService
$page++;
} while ($countResults === $perPage);
}
/**
* 创建 linkos 设备报告
*/
public function createReportToLinkosDevice(Device $device, Carbon $time): void
{
switch ($device->type) {
case Device::TYPE_METEOROLOGICAL:
$this->createReportToLinkosMeteorologicalDevice($device, $time);
break;
case Device::TYPE_WATER_QUALITY:
$this->createReportToLinkosWaterQualityDevice($device, $time);
break;
}
}
/**
* 创建 linkos 气象设备报告
*/
protected function createReportToLinkosMeteorologicalDevice(Device $device, Carbon $time): void
{
$reportedAt = $time->copy()->startOfHour();
/** @var \Illuminate\Database\Eloquent\Collection */
$logs = $device->logs()
->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) {
'current_rainfall' => 'today_rainfall',
'day_rainfall' => 'yesterday_rainfall',
'accumulate_rainfall' => 'accumulate_rainfall',
'moment_rainfall' => 'moment_rainfall',
'pm10_concentration' => 'pm10',
'pm25_concentration' => 'pm25',
'box_illumination' => 'box_illumination',
'box_pressure' => 'box_pressure',
'box_carbon' => 'box_co2',
'box_temperature' => 'box_temperature',
'box_humidity' => 'box_humidity',
'box_noise' => 'box_noise',
'wind_degree' => 'wind_degree',
'wind_direction' => 'wind_direction',
'wind_power' => 'wind_power',
'wind_speed' => 'wind_speed',
default => null,
};
if ($attribute) {
$attributes[$attribute] = $v;
}
}
}
return $attributes;
}, []);
$meteorologicalReport = MeteorologicalReport::where([
'device_id' => $device->id,
'reported_at' => $reportedAt,
])->first();
if ($meteorologicalReport === null) {
$lastMeteorologicalReport = MeteorologicalReport::where([
'device_id' => $device->id,
'reported_at' => $reportedAt->copy()->subHour(),
])->first();
$meteorologicalReport = $lastMeteorologicalReport?->replicate() ?: new MeteorologicalReport();
$meteorologicalReport->fill([
'device_id' => $device->id,
'reported_at' => $reportedAt,
]);
}
$meteorologicalReport->fill($attributes)->save();
}
/**
* 创建 linkos 水质设备报告
*/
protected function createReportToLinkosWaterQualityDevice(Device $device, Carbon $time): void
{
$reportedAt = $time->copy()->startOfHour();
/** @var \Illuminate\Database\Eloquent\Collection */
$logs = $device->logs()
->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) {
'chlorine' => 'chlorine',
'conductivity' => 'conductivity',
'oxygen' => 'oxygen',
'ph' => 'ph',
'temp' => 'temperature',
'turbidity' => 'turbidity',
default => null,
};
if ($attribute) {
$attributes[$attribute] = $v;
}
}
}
return $attributes;
}, []);
$waterQualityReport = WaterQualityReport::where([
'device_id' => $device->id,
'reported_at' => $reportedAt,
])->first();
if ($waterQualityReport === null) {
$lastWaterQualityReport = WaterQualityReport::where([
'device_id' => $device->id,
'reported_at' => $reportedAt->copy()->subHour(),
])->first();
$waterQualityReport = $lastWaterQualityReport?->replicate() ?: new WaterQualityReport();
$waterQualityReport->fill([
'device_id' => $device->id,
'reported_at' => $reportedAt,
]);
}
$waterQualityReport->fill($attributes)->save();
}
/**
* 创建 linkos 设备每日报告
*/
public function createDailyReportToLinkosDevice(Device $device, Carbon $time): void
{
switch ($device->type) {
case Device::TYPE_METEOROLOGICAL:
$this->createDailyReportToLinkosMeteorologicalDevice($device, $time);
break;
case Device::TYPE_WATER_QUALITY:
$this->createDailyReportToLinkosWaterQualityDevice($device, $time);
break;
}
}
/**
* 创建 linkos 气象设备每日报告
*/
protected function createDailyReportToLinkosMeteorologicalDevice(Device $device, Carbon $date): void
{
/** @var \Illuminate\Database\Eloquent\Collection */
$meteorologicalReports = MeteorologicalReport::where('device_id', $device->id)
->whereDate('reported_at', $date)
->oldest('reported_at')
->get();
if ($meteorologicalReports->isEmpty()) {
return;
}
$attributes = value(function ($meteorologicalReports) {
$data = [
'today_rainfall' => ['sum' => 0, 'count' => 0],
'yesterday_rainfall' => ['sum' => 0, 'count' => 0],
'accumulate_rainfall' => ['sum' => 0, 'count' => 0],
'moment_rainfall' => ['sum' => 0, 'count' => 0],
'pm10' => ['sum' => 0, 'count' => 0],
'pm25' => ['sum' => 0, 'count' => 0],
'box_illumination' => ['sum' => 0, 'count' => 0],
'box_pressure' => ['sum' => 0, 'count' => 0],
'box_co2' => ['sum' => 0, 'count' => 0],
'box_temperature' => ['sum' => 0, 'count' => 0],
'box_humidity' => ['sum' => 0, 'count' => 0],
'box_noise' => ['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})) {
$item['sum'] = bcadd($item['sum'], $v, 2);
$item['count']++;
}
$data[$k] = $item;
}
}
$attributes = [];
foreach ($data as $key => $item) {
switch ($key) {
case 'wind_samples':
$attributes['wind_degree'] = value(function (array $windSamples) {
$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 ($windDegree >= 22.5 && $windDegree < 67.5) {
return MeteorologicalDailyReport::WIND_DIRECTION_NORTHEAST;
} elseif ($windDegree >= 67.5 && $windDegree < 112.5) {
return MeteorologicalDailyReport::WIND_DIRECTION_EAST;
} elseif ($windDegree >= 112.5 && $windDegree < 157.5) {
return MeteorologicalDailyReport::WIND_DIRECTION_SOUTHEAST;
} elseif ($windDegree >= 157.5 && $windDegree < 202.5) {
return MeteorologicalDailyReport::WIND_DIRECTION_SOUTH;
} elseif ($windDegree >= 202.5 && $windDegree < 247.5) {
return MeteorologicalDailyReport::WIND_DIRECTION_SOUTHWEST;
} elseif ($windDegree >= 247.5 && $windDegree < 292.5) {
return MeteorologicalDailyReport::WIND_DIRECTION_WEST;
} elseif ($windDegree >= 292.5 && $windDegree < 337.5) {
return MeteorologicalDailyReport::WIND_DIRECTION_NORTHWEST;
}
return MeteorologicalDailyReport::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\MeteorologicalDailyReport */
$meteorologicalDailyReport = MeteorologicalDailyReport::firstOrCreate([
'device_id' => $device->id,
'reported_at' => $date,
], $attributes);
if (! $meteorologicalDailyReport->wasRecentlyCreated) {
$meteorologicalDailyReport->update($attributes);
}
}
/**
* 创建 linkos 水质设备每日报告
*/
protected function createDailyReportToLinkosWaterQualityDevice(Device $device, Carbon $date): void
{
/** @var \Illuminate\Database\Eloquent\Collection */
$waterQualityReports = WaterQualityReport::where('device_id', $device->id)
->whereDate('reported_at', $date)
->oldest('reported_at')
->get();
if ($waterQualityReports->isEmpty()) {
return;
}
$attributes = value(function ($waterQualityReports) {
$data = [
'chlorine' => ['sum' => 0, 'count' => 0],
'conductivity' => ['sum' => 0, 'count' => 0],
'oxygen' => ['sum' => 0, 'count' => 0],
'ph' => ['sum' => 0, 'count' => 0],
'temperature' => ['sum' => 0, 'count' => 0],
'turbidity' => ['sum' => 0, 'count' => 0],
];
foreach ($waterQualityReports as $waterQualityReport) {
foreach ($data as $k => $item) {
if (is_null($v = $waterQualityReport->{$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;
}, $waterQualityReports);
/** @var \App\Models\WaterQualityDailyReport */
$waterQualityDailyReport = WaterQualityDailyReport::firstOrCreate([
'device_id' => $device->id,
'reported_at' => $date->format('Y-m-d'),
], $attributes);
if (! $waterQualityDailyReport->wasRecentlyCreated) {
$waterQualityDailyReport->update($attributes);
}
}
}

View File

@ -14,18 +14,15 @@ return new class extends Migration
public function up()
{
Schema::create('devices', function (Blueprint $table) {
$table->engine = 'InnoDB';
$table->id();
$table->string('name')->comment('设备名称');
$table->string('sn')->comment('设备唯一编码');
$table->string('powered_by')->nullable()->comment('厂家');
$table->unsignedBigInteger('powered_by')->nullable()->comment('厂家');
$table->string('model_sn')->nullable()->comment('型号');
$table->unsignedTinyInteger('type')->comment('类型: 1 监控设备, 2 土壤设备, 3 水质设备, 4 气象设备');
$table->unsignedTinyInteger('state')->default(2)->comment('状态: 0 禁用, 1 在线, 2 离线, 3 故障');
$table->text('extends')->nullable()->comment('扩展信息');
$table->unsignedTinyInteger('is_recommend')->default(0)->comment('推荐开关');
$table->unsignedTinyInteger('is_enable')->default(1)->comment('显示开关');
$table->unsignedInteger('sort')->default(0)->comment('排序');
$table->string('group_tags')->nullable()->comment('分组标签');
$table->timestamps();
});
}

View File

@ -14,6 +14,7 @@ return new class extends Migration
public function up()
{
Schema::create('regions', function (Blueprint $table) {
$table->engine = 'InnoDB';
$table->id();
$table->string('name');
$table->string('cover')->nullable()->comment('封面图');

View File

@ -0,0 +1,41 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('water_quality_daily_reports', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('device_id')->comment('设备ID');
$table->decimal('chlorine', 8, 2)->nullable()->comment('余氯(单位: mg/L');
$table->decimal('conductivity', 8, 2)->nullable()->comment('电导率(单位: us/cm');
$table->decimal('oxygen', 8, 2)->nullable()->comment('溶解氧(单位: mg/L');
$table->decimal('ph', 8, 2)->nullable()->comment('PH');
$table->decimal('temperature', 8, 2)->nullable()->comment('温度(单位: ℃)');
$table->decimal('turbidity', 8, 2)->nullable()->comment('浊度(单位: NTU');
$table->date('reported_at')->comment('报告日期');
$table->timestamps();
$table->unique(['device_id', 'reported_at']);
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('water_quality_daily_reports');
}
};

View File

@ -0,0 +1,51 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('meteorological_daily_reports', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('device_id');
$table->decimal('today_rainfall', 8, 2)->nullable()->comment('今日积雨量 (单位: mm)');
$table->decimal('yesterday_rainfall', 8, 2)->nullable()->comment('昨日积雨量 (单位: mm)');
$table->decimal('accumulate_rainfall', 8, 2)->nullable()->comment('累积雨量 (单位: mm)');
$table->decimal('moment_rainfall', 8, 2)->nullable()->comment('瞬时雨量 (单位: mm)');
$table->decimal('pm10', 8, 2)->nullable()->comment('PM10浓度 (单位: ug/m3)');
$table->decimal('pm25', 8, 2)->nullable()->comment('PM2.5浓度 (单位: ug/m3)');
$table->decimal('box_illumination', 10, 2)->nullable()->comment('百叶箱光照度 (单位: Lux)');
$table->decimal('box_pressure', 8, 2)->nullable()->comment('百叶箱大气压力 (单位: Kpa)');
$table->decimal('box_co2', 8, 2)->nullable()->comment('百叶箱CO2浓度 (单位: ppm)');
$table->decimal('box_temperature', 8, 2)->nullable()->comment('百叶箱温度 (单位: ℃)');
$table->decimal('box_humidity', 8, 2)->nullable()->comment('百叶箱湿度 (单位: %RH)');
$table->decimal('box_noise', 8, 2)->nullable()->comment('百叶箱噪声 (单位: db)');
$table->integer('wind_degree')->nullable()->comment('风向度数');
$table->tinyInteger('wind_direction')->nullable()->comment('风向: 0 北风, 1 东北风, 2 东风, 3 东南风, 4 南风, 5 西南风, 6 西风, 7 西北风');
$table->integer('wind_power')->nullable()->comment('风力 (单位: 级)');
$table->decimal('wind_speed', 8, 2)->nullable()->comment('风速 (单位: m/s)');
$table->date('reported_at')->comment('报告日期');
$table->timestamps();
$table->unique(['device_id', 'reported_at']);
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('meteorological_daily_reports');
}
};

View File

@ -0,0 +1,38 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('monitor_modes', function (Blueprint $table) {
$table->engine = 'InnoDB';
$table->id();
$table->string('name')->comment('名称');
$table->unsignedTinyInteger('type')->comment('类型: 1 视频监控, 2 土壤监测, 3 水质监测, 4 气象监测, 5 通风控制');
$table->unsignedTinyInteger('is_recommend')->default(0)->comment('推荐开关');
$table->unsignedTinyInteger('is_enable')->default(1)->comment('显示开关');
$table->unsignedInteger('sort')->default(0)->comment('排序');
$table->string('group_tags')->nullable()->comment('分组标签');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('monitor_modes');
}
};

View File

@ -0,0 +1,38 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('monitor_devices', function (Blueprint $table) {
$table->engine = 'InnoDB';
$table->id();
$table->unsignedBigInteger('monitor_id');
$table->unsignedBigInteger('device_id');
$table->text('fields')->nullable()->comment('监测字段');
$table->unsignedInteger('sort')->default(0)->comment('排序');
$table->foreign('monitor_id')->references('id')->on('monitor_modes')->onDelete('cascade');
$table->foreign('device_id')->references('id')->on('devices')->onDelete('cascade');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('monitor_devices');
}
};

View File

@ -0,0 +1,38 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('region_monitors', function (Blueprint $table) {
$table->engine = 'InnoDB';
$table->id();
$table->unsignedBigInteger('region_id');
$table->unsignedBigInteger('monitor_id');
$table->text('config')->nullable()->comment('配置信息');
$table->timestamps();
$table->foreign('monitor_id')->references('id')->on('monitor_modes')->onDelete('cascade');
$table->foreign('region_id')->references('id')->on('regions')->onDelete('cascade');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('region_monitors');
}
};

View File

@ -48,22 +48,26 @@ class AdminMenuSeeder extends Seeder
['title' => '稻田列表', 'icon' => 'icon-park:more-app', 'url' => '/custom-region/daotian', 'order'=>2],
]
],
['title' => '设备管理', 'icon' => 'icon-park:devices', 'url' => '/devices', 'order'=>6],
['title' => '设备数据', 'icon' => 'icon-park:data-switching', 'url' => '', 'order'=>7,
['title' => '监测管理', 'icon' => 'icon-park:monitor-one', 'url' => '', 'order'=>6,
'children' => [
['title' => '监测点位', 'icon' => 'icon-park:monitor-camera', 'url' => '/monitor-modes', 'order'=>1],
['title' => '设备管理', 'icon' => 'icon-park:devices', 'url' => '/devices', 'order'=>2],
]
],
['title' => '监测数据', 'icon' => 'icon-park:data-switching', 'url' => '', 'order'=>7,
'children' => [
['title' => '实时监控', 'icon'=>'icon-park:videocamera', 'url'=> '/custom-region-monitor', 'order'=>1],
['title' => '历史视频', 'icon'=>'icon-park:film', 'url'=> '/custom-region-monitor-video', 'order'=>2],
['title' => '气象数据', 'icon'=>'icon-park:brightness', 'url'=> '/custom-region-meteorological', 'order'=>3],
['title' => '土壤数据', 'icon'=>'icon-park:floor-tile', 'url'=> '/custom-region-water', 'order'=>4],
['title' => '气象监测', 'icon'=>'icon-park:brightness', 'url'=> '/custom-region-meteorological', 'order'=>3],
['title' => '土壤监测', 'icon'=>'icon-park:floor-tile', 'url'=> '/custom-region-water', 'order'=>4],
// ['title' => '水质数据', 'icon'=>'icon-park:diving-bottle', 'url'=> '/custom-region-soil', 'order'=>5],
]
],
['title' => '设备预警', 'icon' => 'icon-park:alarm', 'url' => '', 'order'=>8,
['title' => '监测预警', 'icon' => 'icon-park:alarm', 'url' => '', 'order'=>8,
'children' => [
['title' => '预警设置', 'icon'=>'icon-park:six-circular-connection', 'url'=> '/warning-setting', 'order'=>1],
['title' => '报警记录', 'icon'=>'icon-park:massage-chair-one', 'url'=> '/warning-notice', 'order'=>2],
]
]
],
// ['title' => '友情链接', 'icon' => 'icon-park:copy-link', 'url' => '/friend-links', 'order'=>9],
['title' => '系统管理', 'icon' => 'icon-park:setting', 'url' => '/system', 'order'=>10,

View File

@ -23,7 +23,7 @@ class KeywordSeeder extends Seeder
['key' => 'device-factory', 'name' => '厂家', 'list' => [
['name' => 'LINK-OS', 'key'=>'link-os']
]],
['key' => 'device-group', 'name' => '设备组', 'list' => [
['key' => 'monitor-mode-group', 'name' => '监测组', 'list' => [
['name' => '农机', 'key'=>'machinery']
]],
];