diff --git a/app/Admin/Components.php b/app/Admin/Components.php index 925fdba..ea688ff 100644 --- a/app/Admin/Components.php +++ b/app/Admin/Components.php @@ -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 * 折线图或者柱状图 diff --git a/app/Admin/Controllers/CustomRegionController.php b/app/Admin/Controllers/CustomRegionController.php index 6bb167f..f8273ec 100644 --- a/app/Admin/Controllers/CustomRegionController.php +++ b/app/Admin/Controllers/CustomRegionController.php @@ -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); diff --git a/app/Admin/Controllers/DeviceController.php b/app/Admin/Controllers/DeviceController.php index 74b8b2b..cd3cac7 100644 --- a/app/Admin/Controllers/DeviceController.php +++ b/app/Admin/Controllers/DeviceController.php @@ -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'), ])) diff --git a/app/Admin/Controllers/MonitorModeController.php b/app/Admin/Controllers/MonitorModeController.php new file mode 100644 index 0000000..a39a990 --- /dev/null +++ b/app/Admin/Controllers/MonitorModeController.php @@ -0,0 +1,216 @@ +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')); + } +} diff --git a/app/Admin/Controllers/RegionController.php b/app/Admin/Controllers/RegionController.php index 08b4c99..94aa1dc 100644 --- a/app/Admin/Controllers/RegionController.php +++ b/app/Admin/Controllers/RegionController.php @@ -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), ]); } diff --git a/app/Admin/routes.php b/app/Admin/routes.php index 25cf941..a955e74 100644 --- a/app/Admin/routes.php +++ b/app/Admin/routes.php @@ -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'); diff --git a/app/Console/Commands/DeviceLogDailyReportCommand.php b/app/Console/Commands/DeviceLogDailyReportCommand.php new file mode 100644 index 0000000..d9609aa --- /dev/null +++ b/app/Console/Commands/DeviceLogDailyReportCommand.php @@ -0,0 +1,125 @@ +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)); + } +} diff --git a/app/Console/Commands/DeviceLogReportCommand.php b/app/Console/Commands/DeviceLogReportCommand.php index 35e961c..fe1b8fd 100644 --- a/app/Console/Commands/DeviceLogReportCommand.php +++ b/app/Console/Commands/DeviceLogReportCommand.php @@ -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)); } } diff --git a/app/Filters/Admin/DeviceFilter.php b/app/Filters/Admin/DeviceFilter.php index bb1fa21..8aef7f7 100644 --- a/app/Filters/Admin/DeviceFilter.php +++ b/app/Filters/Admin/DeviceFilter.php @@ -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); + } + /** * 类型 */ diff --git a/app/Filters/Admin/MonitorModeFilter.php b/app/Filters/Admin/MonitorModeFilter.php new file mode 100644 index 0000000..8903517 --- /dev/null +++ b/app/Filters/Admin/MonitorModeFilter.php @@ -0,0 +1,43 @@ +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')"); + } +} diff --git a/app/Models/Device.php b/app/Models/Device.php index bc21c8b..f4e8fc6 100644 --- a/app/Models/Device.php +++ b/app/Models/Device.php @@ -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 => '虫情监测', ]; } diff --git a/app/Models/Keyword.php b/app/Models/Keyword.php index e254230..bbb008b 100644 --- a/app/Models/Keyword.php +++ b/app/Models/Keyword.php @@ -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); + } } diff --git a/app/Models/MeteorologicalDailyReport.php b/app/Models/MeteorologicalDailyReport.php new file mode 100644 index 0000000..07b8ad1 --- /dev/null +++ b/app/Models/MeteorologicalDailyReport.php @@ -0,0 +1,45 @@ + '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', + ]; +} diff --git a/app/Models/MonitorDevice.php b/app/Models/MonitorDevice.php new file mode 100644 index 0000000..70b3809 --- /dev/null +++ b/app/Models/MonitorDevice.php @@ -0,0 +1,13 @@ + '视频监控', + 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'); + } +} diff --git a/app/Models/Region.php b/app/Models/Region.php index 7101f1b..fe6bf3e 100644 --- a/app/Models/Region.php +++ b/app/Models/Region.php @@ -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, ]; diff --git a/app/Models/RegionDevice.php b/app/Models/RegionMonitor.php similarity index 81% rename from app/Models/RegionDevice.php rename to app/Models/RegionMonitor.php index 08b13e0..dc0e21d 100644 --- a/app/Models/RegionDevice.php +++ b/app/Models/RegionMonitor.php @@ -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; } diff --git a/app/Models/WaterQualityDailyReport.php b/app/Models/WaterQualityDailyReport.php new file mode 100644 index 0000000..edaf742 --- /dev/null +++ b/app/Models/WaterQualityDailyReport.php @@ -0,0 +1,26 @@ + 'date' + ]; + + protected $fillable = [ + 'device_id', + 'chlorine', + 'conductivity', + 'oxygen', + 'ph', + 'temperature', + 'turbidity', + 'reported_at', + ]; +} diff --git a/app/Services/Admin/MonitorModeService.php b/app/Services/Admin/MonitorModeService.php new file mode 100644 index 0000000..bf649e0 --- /dev/null +++ b/app/Services/Admin/MonitorModeService.php @@ -0,0 +1,78 @@ +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); + } + +} diff --git a/app/Services/Admin/RegionService.php b/app/Services/Admin/RegionService.php index ae3e4bd..fadfce5 100644 --- a/app/Services/Admin/RegionService.php +++ b/app/Services/Admin/RegionService.php @@ -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; diff --git a/app/Services/DeviceLogService.php b/app/Services/DeviceLogService.php index 713c822..b7ca938 100644 --- a/app/Services/DeviceLogService.php +++ b/app/Services/DeviceLogService.php @@ -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); + } + } } diff --git a/database/migrations/2023_03_20_162006_create_devices_table.php b/database/migrations/2023_03_20_162006_create_devices_table.php index 053c9fd..481b6de 100644 --- a/database/migrations/2023_03_20_162006_create_devices_table.php +++ b/database/migrations/2023_03_20_162006_create_devices_table.php @@ -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(); }); } diff --git a/database/migrations/2023_03_21_104958_create_regions_table.php b/database/migrations/2023_03_21_104958_create_regions_table.php index 0a9ca53..b923d2c 100644 --- a/database/migrations/2023_03_21_104958_create_regions_table.php +++ b/database/migrations/2023_03_21_104958_create_regions_table.php @@ -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('封面图'); diff --git a/database/migrations/2023_05_08_141249_create_water_quality_daily_reports_table.php b/database/migrations/2023_05_08_141249_create_water_quality_daily_reports_table.php new file mode 100644 index 0000000..e148e34 --- /dev/null +++ b/database/migrations/2023_05_08_141249_create_water_quality_daily_reports_table.php @@ -0,0 +1,41 @@ +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'); + } +}; diff --git a/database/migrations/2023_05_08_141302_create_meteorological_daily_reports_table.php b/database/migrations/2023_05_08_141302_create_meteorological_daily_reports_table.php new file mode 100644 index 0000000..2dfd58d --- /dev/null +++ b/database/migrations/2023_05_08_141302_create_meteorological_daily_reports_table.php @@ -0,0 +1,51 @@ +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'); + } +}; diff --git a/database/migrations/2023_05_10_173853_create_monitor_modes_table.php b/database/migrations/2023_05_10_173853_create_monitor_modes_table.php new file mode 100644 index 0000000..aecf348 --- /dev/null +++ b/database/migrations/2023_05_10_173853_create_monitor_modes_table.php @@ -0,0 +1,38 @@ +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'); + } +}; diff --git a/database/migrations/2023_05_10_175324_create_monitor_devices_table.php b/database/migrations/2023_05_10_175324_create_monitor_devices_table.php new file mode 100644 index 0000000..ef40b2b --- /dev/null +++ b/database/migrations/2023_05_10_175324_create_monitor_devices_table.php @@ -0,0 +1,38 @@ +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'); + } +}; diff --git a/database/migrations/2023_05_10_175642_create_region_monitors_table.php b/database/migrations/2023_05_10_175642_create_region_monitors_table.php new file mode 100644 index 0000000..0cb9c75 --- /dev/null +++ b/database/migrations/2023_05_10_175642_create_region_monitors_table.php @@ -0,0 +1,38 @@ +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'); + } +}; diff --git a/database/seeders/AdminMenuSeeder.php b/database/seeders/AdminMenuSeeder.php index c9544dd..8abd537 100644 --- a/database/seeders/AdminMenuSeeder.php +++ b/database/seeders/AdminMenuSeeder.php @@ -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, diff --git a/database/seeders/KeywordSeeder.php b/database/seeders/KeywordSeeder.php index 33534e8..8e2f31c 100644 --- a/database/seeders/KeywordSeeder.php +++ b/database/seeders/KeywordSeeder.php @@ -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'] ]], ];