From 32a33dbd87d4b13b264ba56cef4ad1ebf1e8fc9c Mon Sep 17 00:00:00 2001 From: sayliraut Date: Mon, 31 Mar 2025 15:15:12 +0530 Subject: [PATCH 1/3] customer-device-info modify --- .../CustomerDeviceInfoController.php | 327 +++++++++++------- app/Services/CustomerInfoService.php | 40 ++- 2 files changed, 231 insertions(+), 136 deletions(-) diff --git a/app/Http/Controllers/APIS/CustomerApi/CustomerDeviceInfoController.php b/app/Http/Controllers/APIS/CustomerApi/CustomerDeviceInfoController.php index a5731ab..2b855cb 100644 --- a/app/Http/Controllers/APIS/CustomerApi/CustomerDeviceInfoController.php +++ b/app/Http/Controllers/APIS/CustomerApi/CustomerDeviceInfoController.php @@ -7,6 +7,7 @@ use App\Models\Customer; use App\Models\User; use Illuminate\Http\Request; use App\Services\CustomerInfoService; +use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Log; use Tymon\JWTAuth\Facades\JWTAuth; @@ -23,8 +24,8 @@ class CustomerDeviceInfoController extends Controller -// public function customerDeviceInfo(Request $request) -// { + // public function customerDeviceInfo(Request $request) + // { // try { // $tokenPayload = getTokenFromHeader($request); @@ -94,166 +95,230 @@ class CustomerDeviceInfoController extends Controller // ], 500); // } -// } + // } -// public function customerDeviceInfo(Request $request) -// { -// try { + // public function customerDeviceInfo(Request $request) + // { + // try { -// $token = readHeaderToken(); + // $token = readHeaderToken(); -// if (!$token) { -// return response()->json([ -// 'success' => false, -// 'error' => 'Authorization token required' -// ], 401); -// } + // if (!$token) { + // return response()->json([ + // 'success' => false, + // 'error' => 'Authorization token required' + // ], 401); + // } -// $user = User::with(['assets.devices:id,name,asset_id']) -// ->find($token['sub']); + // $user = User::with(['assets.devices:id,name,asset_id']) + // ->find($token['sub']); -// if (!$user) { -// return response()->json([ -// 'error' => 'user_not_found', -// 'message' => 'User not found' -// ], 404); -// } + // if (!$user) { + // return response()->json([ + // 'error' => 'user_not_found', + // 'message' => 'User not found' + // ], 404); + // } -// $devicesArray = $this->customerInfoService->getThingsBoardDevices($user->customer_id); + // $devicesArray = $this->customerInfoService->getThingsBoardDevices($user->customer_id); -// if (isset($devicesArray['error'])) { -// throw new \Exception($devicesArray['message'] ?? 'Failed to fetch devices'); -// } + // if (isset($devicesArray['error'])) { + // throw new \Exception($devicesArray['message'] ?? 'Failed to fetch devices'); + // } -// if (!is_array($devicesArray)) { -// throw new \Exception("Invalid devices data format received from service"); -// } + // if (!is_array($devicesArray)) { + // throw new \Exception("Invalid devices data format received from service"); + // } -// $userDeviceIds = $user->assets->flatMap->devices->pluck('id')->toArray(); + // $userDeviceIds = $user->assets->flatMap->devices->pluck('id')->toArray(); -// $activeCount = collect($devicesArray) -// ->whereIn('id.id', $userDeviceIds) -// ->where('active', true) -// ->count(); + // $activeCount = collect($devicesArray) + // ->whereIn('id.id', $userDeviceIds) + // ->where('active', true) + // ->count(); -// $deviceIdCount = count($userDeviceIds); -// return response()->json([ -// 'data' => [ -// 'active_count' => $activeCount, -// 'total_count' => $deviceIdCount, -// ], -// 'success' => true -// ], 200); + // $deviceIdCount = count($userDeviceIds); + // return response()->json([ + // 'data' => [ + // 'active_count' => $activeCount, + // 'total_count' => $deviceIdCount, + // ], + // 'success' => true + // ], 200); -// } catch (\Exception $e) { -// Log::error("Device info error: " . $e->getMessage()); -// $statusCode = ($e->getMessage() === "Invalid devices data format received from service") ? 503 : 500; + // } catch (\Exception $e) { + // Log::error("Device info error: " . $e->getMessage()); + // $statusCode = ($e->getMessage() === "Invalid devices data format received from service") ? 503 : 500; -// return response()->json([ -// 'error' => 'server_error', -// 'message' => 'An error occurred', -// 'details' => $e->getMessage() -// ], $statusCode); -// } -// } + // return response()->json([ + // 'error' => 'server_error', + // 'message' => 'An error occurred', + // 'details' => $e->getMessage() + // ], $statusCode); + // } + // } -public function customerDeviceInfo(Request $request) -{ - try { - $token = readHeaderToken($request); - if (!$token) { - return response()->json([ - 'success' => false, - 'error' => 'authorization_required', - 'message' => 'Authorization token required' - ], 401); - } + public function customerDeviceInfo(Request $request) + { + try { + $token = readHeaderToken($request); + if (!$token) { + return response()->json([ + 'success' => false, + 'error' => 'authorization_required', + 'message' => 'Authorization token required' + ], 401); + } - // Get user with devices - $user = User::with(['assets.devices:id,name,asset_id']) - ->find($token['sub']); + // Get user with devices + $user = User::with(['assets.devices:id,name,asset_id']) + ->find($token['sub']); - if (!$user) { - return response()->json([ - 'success' => false, - 'error' => 'user_not_found', - 'message' => 'User not found' - ], 404); - } + if (!$user) { + return response()->json([ + 'success' => false, + 'error' => 'user_not_found', + 'message' => 'User not found' + ], 404); + } - // Get user's device IDs - $userDeviceIds = $user->assets->flatMap->devices->pluck('id')->toArray(); + // Get user's device IDs + $userDevices = $user->assets->flatMap->devices; + $userDeviceIds = $userDevices->pluck('id')->toArray(); + + if (empty($userDeviceIds)) { + return response()->json([ + 'data' => [ + 'active_count' => 0, + 'total_count' => 0, + 'alarms_count' => 0, + 'red_count' => 0, + 'yellow_count' => 0, + 'green_count' => 0 + ], + 'success' => true + ], 200); + } + + // Get devices and alarms from service + $serviceResponse = $this->customerInfoService->getCustomerDevicesAndAlarms( + $user->customer_id, + $userDeviceIds + ); + + // Handle service errors + if (isset($serviceResponse['error'])) { + throw new \Exception($serviceResponse['message'] ?? 'Service request failed'); + } + + // Process devices + $devices = $serviceResponse['devices'] ?? []; + $activeCount = collect($devices) + ->whereIn('id.id', $userDeviceIds) + ->where('active', true) + ->count(); + + // Process alarms + $alarms = $serviceResponse['alarms'] ?? []; + $recentAlarms = $this->filterRecentAlarms($alarms); + + $bearerToken = $this->customerInfoService->getToken(); + $apiBaseUrl = env('THINGSBOARD_URL', 'http://65.0.131.117:8080'); + + + $redCount = 0; + $yellowCount = 0; + $greenCount = 0; + + foreach ($userDevices as $device) { + $telemetryResponse = Http::withHeaders([ + 'accept' => 'application/json', + 'Authorization' => 'Bearer ' . $bearerToken, + ])->get("$apiBaseUrl/api/plugins/telemetry/DEVICE/{$device->id}/values/timeseries", [ + 'useStrictDataTypes' => 'false' + ]); + + if (!$telemetryResponse->successful()) { + Log::error("Failed to fetch telemetry for Device ID: {$device->id}, Error: " . $telemetryResponse->body()); + continue; + } + + $telemetryData = $telemetryResponse->json(); + // Log::info("Telemetry data: " . json_encode($telemetryData)); + $engineName = $telemetryData['Engine_Name'][0]['value'] ?? null; + + $mechanicalHealthValue1 = isset($telemetryData['MechanicalHealth_value'][0]['value']) + ? (float) $telemetryData['MechanicalHealth_value'][0]['value'] + : null; + + $engineEfficiencyValue1 = isset($telemetryData['EngineEfficiency_value'][0]['value']) + ? (float) $telemetryData['EngineEfficiency_value'][0]['value'] + : null; + + $engineEfficiencyValue4 = isset($telemetryData['EngineEfficiency_valueInHealth'][0]['value']) + ? (float) $telemetryData['EngineEfficiency_valueInHealth'][0]['value'] + : null; + + $powerLossValue1 = isset($telemetryData['PowerLoss_value'][0]['value']) + ? (float) $telemetryData['PowerLoss_value'][0]['value'] + : null; + + // Determine health status + if ($engineName === "Torque") { + $greenCount++; + } elseif ( + ($mechanicalHealthValue1 > 0 && $mechanicalHealthValue1 < 31) || + ($engineEfficiencyValue1 > 0 && $engineEfficiencyValue1 < 31) || + ($engineEfficiencyValue4 > 0 && $engineEfficiencyValue4 < 31) || + ($powerLossValue1 > 0 && $powerLossValue1 < 31) + ) { + $redCount++; + } elseif ( + ($mechanicalHealthValue1 >= 31 && $mechanicalHealthValue1 < 71) || + ($engineEfficiencyValue1 >= 31 && $engineEfficiencyValue1 < 71) || + ($engineEfficiencyValue4 >= 31 && $engineEfficiencyValue4 < 71) || + ($powerLossValue1 >= 31 && $powerLossValue1 < 71) + ) { + $yellowCount++; + } else { + $greenCount++; + } + } - if (empty($userDeviceIds)) { return response()->json([ 'data' => [ - 'active_count' => 0, - 'total_count' => 0, - - 'alarms_count' => 0 + 'active_count' => $activeCount, + 'total_count' => count($userDeviceIds), + 'alarms_count' => count($recentAlarms), + 'red_count' => $redCount, + 'yellow_count' => $yellowCount, + 'green_count' => $greenCount, ], 'success' => true ], 200); + } catch (\Exception $e) { + Log::error("Device info error: " . $e->getMessage()); + return response()->json([ + 'success' => false, + 'error' => 'server_error', + 'message' => 'An error occurred while fetching device information', + 'details' => $e->getMessage() + ], 500); } + } - // Get devices and alarms from service - $serviceResponse = $this->customerInfoService->getCustomerDevicesAndAlarms( - $user->customer_id, - $userDeviceIds - ); - // Handle service errors - if (isset($serviceResponse['error'])) { - throw new \Exception($serviceResponse['message'] ?? 'Service request failed'); - } + private function filterRecentAlarms(array $alarms): array + { + $twentyFourHoursAgo = (time() - 86400) * 1000; - // Process devices - $devices = $serviceResponse['devices'] ?? []; - $activeCount = collect($devices) - ->whereIn('id.id', $userDeviceIds) - ->where('active', true) - ->count(); - - // Process alarms - $alarms = $serviceResponse['alarms'] ?? []; - - $recentAlarms = $this->filterRecentAlarms($alarms); - - return response()->json([ - 'data' => [ - 'active_count' => $activeCount, - 'total_count' => count($userDeviceIds), - - 'alarms_count' => count($recentAlarms), - - ], - 'success' => true - ], 200); - - } catch (\Exception $e) { - Log::error("Device info error: " . $e->getMessage()); - return response()->json([ - 'success' => false, - 'error' => 'server_error', - 'message' => 'An error occurred while fetching device information', - 'details' => $e->getMessage() - ], 500); + return array_values(array_filter($alarms, function ($alarm) use ($twentyFourHoursAgo) { + return ($alarm['createdTime'] ?? 0) >= $twentyFourHoursAgo; + })); } } - -private function filterRecentAlarms(array $alarms): array -{ - $twentyFourHoursAgo = (time() - 86400) * 1000; - - return array_values(array_filter($alarms, function($alarm) use ($twentyFourHoursAgo) { - return ($alarm['createdTime'] ?? 0) >= $twentyFourHoursAgo; - })); -} - - -} diff --git a/app/Services/CustomerInfoService.php b/app/Services/CustomerInfoService.php index 060d02d..2987e03 100644 --- a/app/Services/CustomerInfoService.php +++ b/app/Services/CustomerInfoService.php @@ -8,6 +8,8 @@ use Illuminate\Support\Facades\Log; use App\Services\AdminService; use Exception; use Illuminate\Support\Facades\Request; +use Illuminate\Support\Facades\Cache; + class CustomerInfoService { @@ -60,7 +62,7 @@ class CustomerInfoService - public function getTelemetryData($device, $keyNames, $startTs , $endTs ) + public function getTelemetryData($device, $keyNames, $startTs, $endTs) { $token = $this->adminService->getToken(); @@ -271,7 +273,6 @@ class CustomerInfoService } return $result; - } catch (\Exception $e) { Log::error("ThingsBoard service error: " . $e->getMessage()); return [ @@ -304,7 +305,6 @@ class CustomerInfoService $data = $response->json(); return $data['data'] ?? []; - } catch (\Exception $e) { Log::error("Device fetch error: " . $e->getMessage()); return [ @@ -344,7 +344,6 @@ class CustomerInfoService } return $response->json(); - } catch (\Exception $e) { Log::error("Alarm fetch error: " . $e->getMessage()); return [ @@ -355,4 +354,35 @@ class CustomerInfoService } } -} \ No newline at end of file + + public function getToken() + { + $baseUrl = env('THINGSBOARD_URL', 'http://65.0.131.117:8080'); + $username = env('THINGSBOARD_USERNAME', 'tenant1@thingsboard.org'); + $password = env('THINGSBOARD_PASSWORD', 'tenant1'); + + + if (Cache::has('thingsboard_token')) { + return Cache::get('thingsboard_token'); + } + + $response = Http::withHeaders([ + // 'accept' => 'application/json', + 'Content-Type' => 'application/json', + ]) + ->post("{$baseUrl}/api/auth/login", [ + 'username' => $username, + 'password' => $password, + ]); + + + if ($response->successful()) { + $token = $response->json('token'); + Cache::put('thingsboard_token', $token, now()->addMinutes(15)); + return $token; + } else { + Log::error("ThingsBoard Authentication Failed: " . $response->body()); + throw new Exception('Unable to authenticate with ThingsBoard: ' . $response->body()); + } + } +} From 955cdb7081b88aeeb18aa73a9e3513759d5c6550 Mon Sep 17 00:00:00 2001 From: sayliraut Date: Mon, 31 Mar 2025 15:18:32 +0530 Subject: [PATCH 2/3] customer-device-info modify --- .../CustomerDeviceInfoController.php | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/app/Http/Controllers/APIS/CustomerApi/CustomerDeviceInfoController.php b/app/Http/Controllers/APIS/CustomerApi/CustomerDeviceInfoController.php index 2b855cb..63074dd 100644 --- a/app/Http/Controllers/APIS/CustomerApi/CustomerDeviceInfoController.php +++ b/app/Http/Controllers/APIS/CustomerApi/CustomerDeviceInfoController.php @@ -231,9 +231,9 @@ class CustomerDeviceInfoController extends Controller $apiBaseUrl = env('THINGSBOARD_URL', 'http://65.0.131.117:8080'); - $redCount = 0; - $yellowCount = 0; - $greenCount = 0; + $bad = 0; + $moderate = 0; + $good = 0; foreach ($userDevices as $device) { $telemetryResponse = Http::withHeaders([ @@ -270,23 +270,23 @@ class CustomerDeviceInfoController extends Controller // Determine health status if ($engineName === "Torque") { - $greenCount++; + $good++; } elseif ( ($mechanicalHealthValue1 > 0 && $mechanicalHealthValue1 < 31) || ($engineEfficiencyValue1 > 0 && $engineEfficiencyValue1 < 31) || ($engineEfficiencyValue4 > 0 && $engineEfficiencyValue4 < 31) || ($powerLossValue1 > 0 && $powerLossValue1 < 31) ) { - $redCount++; + $bad++; } elseif ( ($mechanicalHealthValue1 >= 31 && $mechanicalHealthValue1 < 71) || ($engineEfficiencyValue1 >= 31 && $engineEfficiencyValue1 < 71) || ($engineEfficiencyValue4 >= 31 && $engineEfficiencyValue4 < 71) || ($powerLossValue1 >= 31 && $powerLossValue1 < 71) ) { - $yellowCount++; + $moderate++; } else { - $greenCount++; + $good++; } } @@ -295,9 +295,9 @@ class CustomerDeviceInfoController extends Controller 'active_count' => $activeCount, 'total_count' => count($userDeviceIds), 'alarms_count' => count($recentAlarms), - 'red_count' => $redCount, - 'yellow_count' => $yellowCount, - 'green_count' => $greenCount, + 'bad' => $bad, + 'moderate' => $moderate, + 'good' => $good, ], 'success' => true ], 200); From e01728c38d95eae9fc02763465fa5573ae4c32fe Mon Sep 17 00:00:00 2001 From: kshitige Date: Mon, 31 Mar 2025 19:18:36 +0530 Subject: [PATCH 3/3] changes in alarm changes in telemetry data device --- .../CustomerDeviceInfoController.php | 1 - .../APIS/CustomerApi/TelemetryController.php | 239 ++++++-- .../CustomerApi/UserAssetLinkController.php | 527 ++++++++++++++---- app/Services/CustomerInfoService.php | 2 +- 4 files changed, 597 insertions(+), 172 deletions(-) diff --git a/app/Http/Controllers/APIS/CustomerApi/CustomerDeviceInfoController.php b/app/Http/Controllers/APIS/CustomerApi/CustomerDeviceInfoController.php index a5731ab..3c5a0f7 100644 --- a/app/Http/Controllers/APIS/CustomerApi/CustomerDeviceInfoController.php +++ b/app/Http/Controllers/APIS/CustomerApi/CustomerDeviceInfoController.php @@ -228,7 +228,6 @@ public function customerDeviceInfo(Request $request) 'data' => [ 'active_count' => $activeCount, 'total_count' => count($userDeviceIds), - 'alarms_count' => count($recentAlarms), ], diff --git a/app/Http/Controllers/APIS/CustomerApi/TelemetryController.php b/app/Http/Controllers/APIS/CustomerApi/TelemetryController.php index 08c7247..e12617d 100644 --- a/app/Http/Controllers/APIS/CustomerApi/TelemetryController.php +++ b/app/Http/Controllers/APIS/CustomerApi/TelemetryController.php @@ -55,7 +55,6 @@ class TelemetryController extends Controller foreach ($devices as $device) { $telemetry = []; - // Fetch key names and additional columns from TimeseriesKeyMaster $keysData = TimeseriesKeyMaster::where('device_profile_xid', $device->device_profile_id) ->where(function ($query) { $query->where('display_on_dashboard', true) @@ -101,10 +100,109 @@ class TelemetryController extends Controller } + // public function telemetryDataDevice(Request $request) + // { + // try { + + // $token = readHeaderToken(); + // if (!$token) { + // return response()->json([ + // 'success' => false, + // 'error' => 'Authorization token required' + // ], 401); + // } + + // $validator = Validator::make($request->all(), [ + // 'device_id' => 'required|string', + // 'startTs' => 'nullable|string', + // 'endTs' => 'nullable|string', + // ]); + + // if ($validator->fails()) { + // return response()->json([ + // 'success' => false, + // 'error' => $validator->errors()->first() + // ], 400); + // } + + // $deviceId = $request->input('device_id'); + // $startTs = $request->input('startTs') ?: null; + // $endTs = $request->input('endTs') ?: null; + + // try { + // $deviceWithTelemetry = Device::with([ + // 'deviceProfile', + // 'timeseriesKeys' => function ($query) { + // $query->select('key_name', 'display_name', 'device_profile_xid', 'display_on_dashboard','display_on_dashboard'); + // } + // ]) + // ->where('id', $deviceId) + // ->firstOrFail(); + + // $telemetryResponse = $this->customerInfoService->getTelemetryDataDevice( + // $deviceWithTelemetry, + // $deviceWithTelemetry->timeseriesKeys->pluck('key_name')->toArray(), + // $startTs, + // $endTs, + // $token + // ); + + // if (!is_array($telemetryResponse)) { + // throw new \Exception("Invalid telemetry data format received from service"); + // } + + // $telemetry = collect($telemetryResponse) + // ->flatMap(function ($items, $keyName) use ($deviceWithTelemetry) { + // $displayName = $deviceWithTelemetry->timeseriesKeys + // ->firstWhere('key_name', $keyName)?->display_name ?? $keyName; + + // return collect($items)->map(function ($item) use ($keyName, $displayName) { + // return [ + // 'key_name' => $keyName, + // 'timestamp' => $item['ts'] ?? null, + // 'value' => $item['value'] ?? null, + // 'display_name' => $displayName, + // 'display_on_dashboard' + // ]; + // }); + // }) + // ->values() + // ->all(); + + // return response()->json([ + // 'success' => true, + // 'telemetry' => [ + // 'device_id' => (string) $deviceWithTelemetry->id, + // 'device_name' => $deviceWithTelemetry->name, + // 'device_profile_name' => $deviceWithTelemetry->deviceProfile->name, + // 'device_profile_id' => (string) $deviceWithTelemetry->device_profile_id, + // 'telemetry_data' => $telemetry, + // ] + // ], 200); + // } catch (\Illuminate\Database\Eloquent\ModelNotFoundException $e) { + // return response()->json([ + // 'success' => false, + // 'error' => 'Device not found' + // ], 404); + // } catch (\Exception $e) { + // return response()->json([ + // 'success' => false, + // 'error' => 'Failed to fetch telemetry data', + // 'details' => config('app.debug') ? $e->getMessage() : null + // ], 503); + // } + // } catch (\Exception $e) { + // return response()->json([ + // 'success' => false, + // 'error' => 'Internal server error', + // 'details' => config('app.debug') ? $e->getMessage() : null + // ], 500); + // } + // } + public function telemetryDataDevice(Request $request) { try { - // Read and validate token first $token = readHeaderToken(); if (!$token) { return response()->json([ @@ -134,36 +232,57 @@ class TelemetryController extends Controller $deviceWithTelemetry = Device::with([ 'deviceProfile', 'timeseriesKeys' => function ($query) { - $query->select('key_name', 'display_name', 'device_profile_xid'); + $query->where(function ($q) { + $q->where('display_on_dashboard', true) + ->orWhere('display_on_popup', true); + }) + ->select('key_name', 'display_name', 'device_profile_xid', 'display_on_dashboard', 'display_on_popup'); } ]) ->where('id', $deviceId) ->firstOrFail(); - // Pass the token to the service method + $displayKeys = $deviceWithTelemetry->timeseriesKeys->pluck('key_name')->toArray(); + + if (empty($displayKeys)) { + return response()->json([ + 'success' => true, + 'telemetry' => [ + 'device_id' => (string) $deviceWithTelemetry->id, + 'device_name' => $deviceWithTelemetry->name, + 'device_profile_name' => $deviceWithTelemetry->deviceProfile->name, + 'device_profile_id' => (string) $deviceWithTelemetry->device_profile_id, + 'telemetry_data' => [], + ] + ], 200); + } + $telemetryResponse = $this->customerInfoService->getTelemetryDataDevice( $deviceWithTelemetry, - $deviceWithTelemetry->timeseriesKeys->pluck('key_name')->toArray(), + $displayKeys, $startTs, $endTs, - $token // Added token parameter + $token ); if (!is_array($telemetryResponse)) { throw new \Exception("Invalid telemetry data format received from service"); } - $telemetry = collect($telemetryResponse) - ->flatMap(function ($items, $keyName) use ($deviceWithTelemetry) { - $displayName = $deviceWithTelemetry->timeseriesKeys - ->firstWhere('key_name', $keyName)?->display_name ?? $keyName; + $filteredResponse = array_intersect_key($telemetryResponse, array_flip($displayKeys)); - return collect($items)->map(function ($item) use ($keyName, $displayName) { + $telemetry = collect($filteredResponse) + ->flatMap(function ($items, $keyName) use ($deviceWithTelemetry) { + $keyData = $deviceWithTelemetry->timeseriesKeys->firstWhere('key_name', $keyName); + + return collect($items)->map(function ($item) use ($keyName, $keyData) { return [ 'key_name' => $keyName, 'timestamp' => $item['ts'] ?? null, 'value' => $item['value'] ?? null, - 'display_name' => $displayName, + 'display_name' => $keyData->display_name ?? $keyName, + 'display_on_dashboard' => $keyData->display_on_dashboard ?? false, + 'display_on_popup' => $keyData->display_on_popup ?? false ]; }); }) @@ -200,63 +319,63 @@ class TelemetryController extends Controller ], 500); } } - public function telemetryDataDeviceDiagnostic(Request $request, $deviceId) - { - $devices = Device::with('deviceProfile') - ->where('id', $deviceId) - ->get(); + // public function telemetryDataDeviceDiagnostic(Request $request, $deviceId) + // { + // $devices = Device::with('deviceProfile') + // ->where('id', $deviceId) + // ->get(); - if ($devices->isEmpty()) { - return response()->json(['error' => 'No devices found'], 404); - } + // if ($devices->isEmpty()) { + // return response()->json(['error' => 'No devices found'], 404); + // } - $startTs = $request->has('start_date') ? strtotime($request->start_date) * 1000 : null; - $endTs = $request->has('end_date') ? strtotime($request->end_date) * 1000 : null; + // $startTs = $request->has('start_date') ? strtotime($request->start_date) * 1000 : null; + // $endTs = $request->has('end_date') ? strtotime($request->end_date) * 1000 : null; - $response = []; + // $response = []; - foreach ($devices as $device) { - $telemetry = []; + // foreach ($devices as $device) { + // $telemetry = []; - $keyNames = TimeseriesKeyMaster::where('device_profile_xid', $device->device_profile_id) - ->pluck('key_name', 'display_name') - ->toArray(); + // $keyNames = TimeseriesKeyMaster::where('device_profile_xid', $device->device_profile_id) + // ->pluck('key_name', 'display_name') + // ->toArray(); - $telemetryResponse = $this->customerInfoService->getTelemetryDataDeviceDiagonostic($device, $keyNames, $startTs, $endTs); + // $telemetryResponse = $this->customerInfoService->getTelemetryDataDeviceDiagonostic($device, $keyNames, $startTs, $endTs); - foreach ($keyNames as $keyName) { - if (isset($telemetryResponse[$keyName])) { - foreach ($telemetryResponse[$keyName] as $item) { - $timestamp = $item['ts'] ?? null; + // foreach ($keyNames as $keyName) { + // if (isset($telemetryResponse[$keyName])) { + // foreach ($telemetryResponse[$keyName] as $item) { + // $timestamp = $item['ts'] ?? null; - // ✅ Filter telemetry by timestamp range - if ($timestamp && $timestamp >= $startTs && $timestamp <= $endTs) { - $telemetry[] = [ - 'key_name' => $keyName, - 'timestamp' => $timestamp, - 'start_date' => $startTs, - 'end_date' => $endTs, - 'value' => $item['value'] ?? null, - 'display_name' => $keyName, - ]; - } - } - } - } + // // ✅ Filter telemetry by timestamp range + // if ($timestamp && $timestamp >= $startTs && $timestamp <= $endTs) { + // $telemetry[] = [ + // 'key_name' => $keyName, + // 'timestamp' => $timestamp, + // 'start_date' => $startTs, + // 'end_date' => $endTs, + // 'value' => $item['value'] ?? null, + // 'display_name' => $keyName, + // ]; + // } + // } + // } + // } - if (!empty($telemetry)) { - $response[] = [ - 'device_id' => (string) $device->id, - 'device_name' => $device->name, - 'device_profile_name' => (string) $device->deviceProfile->name, - 'device_profile_id' => (string) $device->device_profile_id, - 'telemetry' => $telemetry, - ]; - } - } + // if (!empty($telemetry)) { + // $response[] = [ + // 'device_id' => (string) $device->id, + // 'device_name' => $device->name, + // 'device_profile_name' => (string) $device->deviceProfile->name, + // 'device_profile_id' => (string) $device->device_profile_id, + // 'telemetry' => $telemetry, + // ]; + // } + // } - return response()->json(['telemetry' => $response]); - } + // return response()->json(['telemetry' => $response]); + // } // public function telemetryDataDeviceDiagnostic(Request $request, $deviceId) // { @@ -322,4 +441,4 @@ class TelemetryController extends Controller // return response()->json(['telemetry' => $response]); // } -} \ No newline at end of file +} diff --git a/app/Http/Controllers/APIS/CustomerApi/UserAssetLinkController.php b/app/Http/Controllers/APIS/CustomerApi/UserAssetLinkController.php index 6193881..69562a5 100644 --- a/app/Http/Controllers/APIS/CustomerApi/UserAssetLinkController.php +++ b/app/Http/Controllers/APIS/CustomerApi/UserAssetLinkController.php @@ -25,125 +25,432 @@ class UserAssetLinkController extends Controller $this->adminService = $adminService; } - public function index() - { - try { - $token = readHeaderToken(); + // public function index() + // { + // try { + // $token = readHeaderToken(); - // Fetch user with assets and device counts - $user = User::with(['assets.devices']) - ->withCount([ - 'assets as active_devices_count' => function ($query) { - $query->whereHas('devices', function ($q) { - $q->where('active', 1); - }); - }, - 'assets as inactive_devices_count' => function ($query) { - $query->whereHas('devices', function ($q) { - $q->where('active', 0); - }); - } - ]) - ->where('id', $token['sub']) - ->first(); + // // Fetch user with assets and device counts + // $user = User::with(['assets.devices']) + // ->withCount([ + // 'assets as active_devices_count' => function ($query) { + // $query->whereHas('devices', function ($q) { + // $q->where('active', 1); + // }); + // }, + // 'assets as inactive_devices_count' => function ($query) { + // $query->whereHas('devices', function ($q) { + // $q->where('active', 0); + // }); + // } + // ]) + // ->where('id', $token['sub']) + // ->first(); - if (!$user) { - return response()->json(['error' => 'User not found'], 404); - } + // if (!$user) { + // return response()->json(['error' => 'User not found'], 404); + // } - $bearerToken = $this->adminService->getToken(); - $apiBaseUrl = env('THINGSBOARD_URL', 'http://65.0.131.117:8080'); + // $bearerToken = $this->adminService->getToken(); + // $apiBaseUrl = env('THINGSBOARD_URL', 'http://65.0.131.117:8080'); - // Log::info('Devices: ' . json_encode($user->assets->flatMap->devices)); - foreach ($user->assets->flatMap->devices as $device) { - $device->health_status = null; - $device->online = null; + // // Log::info('Devices: ' . json_encode($user->assets->flatMap->devices)); + // foreach ($user->assets->flatMap->devices as $device) { + // $device->health_status = null; + // $device->online = null; - // Fetch device details from API using customer_id - $deviceResponse = Http::withHeaders([ - 'accept' => 'application/json', - 'Authorization' => 'Bearer ' . $bearerToken, - ])->get("$apiBaseUrl/api/customer/{$device->customer_id}/deviceInfos", [ - 'pageSize' => 100, - 'page' => 0 - ]); + // // Fetch device details from API using customer_id + // $deviceResponse = Http::withHeaders([ + // 'accept' => 'application/json', + // 'Authorization' => 'Bearer ' . $bearerToken, + // ])->get("$apiBaseUrl/api/customer/{$device->customer_id}/deviceInfos", [ + // 'pageSize' => 100, + // 'page' => 0 + // ]); - if (!$deviceResponse->successful()) { - Log::error("Failed to fetch device info for Customer ID: {$device->customer_id}, Error: " . $deviceResponse->body()); - continue; + // if (!$deviceResponse->successful()) { + // Log::error("Failed to fetch device info for Customer ID: {$device->customer_id}, Error: " . $deviceResponse->body()); + // continue; + // } + + // $deviceData = collect($deviceResponse->json()['data'] ?? []); + // $matchingDevice = $deviceData->firstWhere('id.id', $device->id); + + // // Explicitly set online status for all devices + // $device->online = $matchingDevice['active'] ?? null; + + // // Fetch telemetry data for the device + // $telemetryResponse = Http::withHeaders([ + // 'accept' => 'application/json', + // 'Authorization' => 'Bearer ' . $bearerToken, + // ])->get("$apiBaseUrl/api/plugins/telemetry/DEVICE/{$device->id}/values/timeseries", [ + // 'useStrictDataTypes' => 'false' + // ]); + + // if (!$telemetryResponse->successful()) { + // Log::error("Failed to fetch telemetry for Device ID: {$device->id}, Error: " . $telemetryResponse->body()); + // continue; + // } + + // $telemetryData = $telemetryResponse->json(); + // // Log::info('Telemetry data: ' . json_encode($telemetryData)); + + // $engineName = $telemetryData['Engine_Name'][0]['value'] ?? null; + + // // Extract values from telemetry data + // $mechanicalHealthValue1 = isset($telemetryData['MechanicalHealth_value'][0]['value']) + // ? (float) $telemetryData['MechanicalHealth_value'][0]['value'] + // : null; + + // $engineEfficiencyValue1 = isset($telemetryData['EngineEfficiency_value'][0]['value']) + // ? (float) $telemetryData['EngineEfficiency_value'][0]['value'] + // : null; + + // $engineEfficiencyValue4 = isset($telemetryData['EngineEfficiency_valueInHealth'][0]['value']) + // ? (float) $telemetryData['EngineEfficiency_valueInHealth'][0]['value'] + // : null; + + // $powerLossValue1 = isset($telemetryData['PowerLoss_value'][0]['value']) + // ? (float) $telemetryData['PowerLoss_value'][0]['value'] + // : null; + + // // Default health status + // $healthStatusColor = '#0EC23E'; // Green + + // if ($engineName === "Torque") { + // $healthStatusColor = '#0EC23E'; // Green + // } elseif ( + // ($mechanicalHealthValue1 > 0 && $mechanicalHealthValue1 < 31) || + // ($engineEfficiencyValue1 > 0 && $engineEfficiencyValue1 < 31) || + // ($engineEfficiencyValue4 > 0 && $engineEfficiencyValue4 < 31) || + // ($powerLossValue1 > 0 && $powerLossValue1 < 31) + // ) { + // $healthStatusColor = '#EF7F30'; // Red + // } elseif ( + // ($mechanicalHealthValue1 > 30 && $mechanicalHealthValue1 < 71) || + // ($engineEfficiencyValue1 > 30 && $engineEfficiencyValue1 < 71) || + // ($engineEfficiencyValue4 > 30 && $engineEfficiencyValue4 < 71) || + // ($powerLossValue1 > 30 && $powerLossValue1 < 71) + // ) { + // $healthStatusColor = '#FFC164'; // Yellow + // } + + // $device->health_status = "
"; + // } + + + // return response()->json($user); + // } catch (Exception $e) { + // Log::error('Error fetching telemetry data: ' . $e->getMessage()); + // return response()->json(['error' => 'Failed to fetch data'], 500); + // } + // } + + +// public function index() +// { +// try { +// $token = readHeaderToken(); + +// // Fetch user with assets and device counts +// $user = User::with(['assets.devices']) +// ->withCount([ +// 'assets as active_devices_count' => function ($query) { +// $query->whereHas('devices', function ($q) { +// $q->where('active', 1); +// }); +// }, +// 'assets as inactive_devices_count' => function ($query) { +// $query->whereHas('devices', function ($q) { +// $q->where('active', 0); +// }); +// } +// ]) +// ->where('id', $token['sub']) +// ->first(); + +// if (!$user) { +// return response()->json(['error' => 'User not found'], 404); +// } + +// $bearerToken = $this->adminService->getToken(); +// $apiBaseUrl = env('THINGSBOARD_URL', 'http://65.0.131.117:8080'); + +// foreach ($user->assets->flatMap->devices as $device) { +// $device->health_status = null; +// $device->online = null; +// $device->has_alarm = false; // Initialize alarm status to false + +// // Fetch device details from API using customer_id +// $deviceResponse = Http::withHeaders([ +// 'accept' => 'application/json', +// 'Authorization' => 'Bearer ' . $bearerToken, +// ])->get("$apiBaseUrl/api/customer/{$device->customer_id}/deviceInfos", [ +// 'pageSize' => 100, +// 'page' => 0 +// ]); + +// if (!$deviceResponse->successful()) { +// Log::error("Failed to fetch device info for Customer ID: {$device->customer_id}, Error: " . $deviceResponse->body()); +// continue; +// } + +// $deviceData = collect($deviceResponse->json()['data'] ?? []); +// $matchingDevice = $deviceData->firstWhere('id.id', $device->id); + +// // Set online status +// $device->online = $matchingDevice['active'] ?? null; + +// // Fetch alarm data for the device (last 24 hours) +// $twentyFourHoursAgo = (time() - 86400) * 1000; // 24 hours ago in milliseconds +// $currentTime = time() * 1000; // current time in milliseconds + +// $alarmResponse = Http::withHeaders([ +// 'accept' => 'application/json', +// 'Authorization' => 'Bearer ' . $bearerToken, +// ])->get("$apiBaseUrl/api/alarm/DEVICE/{$device->id}", [ +// 'pageSize' => 100, +// 'page' => 0, +// 'startTime' => $twentyFourHoursAgo, +// 'endTime' => $currentTime, +// 'fetchOriginator' => 'false', +// 'searchStatus' => 'ACTIVE,ACKNOWLEDGED,CLEARED' +// ]); + +// if ($alarmResponse->successful()) { +// $alarms = $alarmResponse->json()['data'] ?? []; +// $device->has_alarm = !empty($alarms); // Set to true if any alarms exist +// } else { +// Log::error("Failed to fetch alarms for Device ID: {$device->id}, Error: " . $alarmResponse->body()); +// } + +// // Fetch telemetry data for the device +// $telemetryResponse = Http::withHeaders([ +// 'accept' => 'application/json', +// 'Authorization' => 'Bearer ' . $bearerToken, +// ])->get("$apiBaseUrl/api/plugins/telemetry/DEVICE/{$device->id}/values/timeseries", [ +// 'useStrictDataTypes' => 'false' +// ]); + +// if (!$telemetryResponse->successful()) { +// Log::error("Failed to fetch telemetry for Device ID: {$device->id}, Error: " . $telemetryResponse->body()); +// continue; +// } + +// $telemetryData = $telemetryResponse->json(); + +// $engineName = $telemetryData['Engine_Name'][0]['value'] ?? null; + +// // Extract values from telemetry data +// $mechanicalHealthValue1 = isset($telemetryData['MechanicalHealth_value'][0]['value']) +// ? (float) $telemetryData['MechanicalHealth_value'][0]['value'] +// : null; + +// $engineEfficiencyValue1 = isset($telemetryData['EngineEfficiency_value'][0]['value']) +// ? (float) $telemetryData['EngineEfficiency_value'][0]['value'] +// : null; + +// $engineEfficiencyValue4 = isset($telemetryData['EngineEfficiency_valueInHealth'][0]['value']) +// ? (float) $telemetryData['EngineEfficiency_valueInHealth'][0]['value'] +// : null; + +// $powerLossValue1 = isset($telemetryData['PowerLoss_value'][0]['value']) +// ? (float) $telemetryData['PowerLoss_value'][0]['value'] +// : null; + +// // Default health status +// $healthStatusColor = '#0EC23E'; // Green + +// if ($engineName === "Torque") { +// $healthStatusColor = '#0EC23E'; // Green +// } elseif ( +// ($mechanicalHealthValue1 > 0 && $mechanicalHealthValue1 < 31) || +// ($engineEfficiencyValue1 > 0 && $engineEfficiencyValue1 < 31) || +// ($engineEfficiencyValue4 > 0 && $engineEfficiencyValue4 < 31) || +// ($powerLossValue1 > 0 && $powerLossValue1 < 31) +// ) { +// $healthStatusColor = '#EF7F30'; // Red +// } elseif ( +// ($mechanicalHealthValue1 > 30 && $mechanicalHealthValue1 < 71) || +// ($engineEfficiencyValue1 > 30 && $engineEfficiencyValue1 < 71) || +// ($engineEfficiencyValue4 > 30 && $engineEfficiencyValue4 < 71) || +// ($powerLossValue1 > 30 && $powerLossValue1 < 71) +// ) { +// $healthStatusColor = '#FFC164'; // Yellow +// } + +// $device->health_status = "
"; +// } + +// return response()->json($user); +// } catch (Exception $e) { +// Log::error('Error fetching telemetry data: ' . $e->getMessage()); +// return response()->json(['error' => 'Failed to fetch data'], 500); +// } +// } + +public function index() +{ + try { + $token = readHeaderToken(); + + // Fetch user with assets and device counts + $user = User::with(['assets.devices']) + ->withCount([ + 'assets as active_devices_count' => function ($query) { + $query->whereHas('devices', function ($q) { + $q->where('active', 1); + }); + }, + 'assets as inactive_devices_count' => function ($query) { + $query->whereHas('devices', function ($q) { + $q->where('active', 0); + }); } + ]) + ->where('id', $token['sub']) + ->first(); - $deviceData = collect($deviceResponse->json()['data'] ?? []); - $matchingDevice = $deviceData->firstWhere('id.id', $device->id); - - // Explicitly set online status for all devices - $device->online = $matchingDevice['active'] ?? null; - - // Fetch telemetry data for the device - $telemetryResponse = Http::withHeaders([ - 'accept' => 'application/json', - 'Authorization' => 'Bearer ' . $bearerToken, - ])->get("$apiBaseUrl/api/plugins/telemetry/DEVICE/{$device->id}/values/timeseries", [ - 'useStrictDataTypes' => 'false' - ]); - - if (!$telemetryResponse->successful()) { - Log::error("Failed to fetch telemetry for Device ID: {$device->id}, Error: " . $telemetryResponse->body()); - continue; - } - - $telemetryData = $telemetryResponse->json(); - // Log::info('Telemetry data: ' . json_encode($telemetryData)); - - $engineName = $telemetryData['Engine_Name'][0]['value'] ?? null; - - // Extract values from telemetry data - $mechanicalHealthValue1 = isset($telemetryData['MechanicalHealth_value'][0]['value']) - ? (float) $telemetryData['MechanicalHealth_value'][0]['value'] - : null; - - $engineEfficiencyValue1 = isset($telemetryData['EngineEfficiency_value'][0]['value']) - ? (float) $telemetryData['EngineEfficiency_value'][0]['value'] - : null; - - $engineEfficiencyValue4 = isset($telemetryData['EngineEfficiency_valueInHealth'][0]['value']) - ? (float) $telemetryData['EngineEfficiency_valueInHealth'][0]['value'] - : null; - - $powerLossValue1 = isset($telemetryData['PowerLoss_value'][0]['value']) - ? (float) $telemetryData['PowerLoss_value'][0]['value'] - : null; - - // Default health status - $healthStatusColor = '#0EC23E'; // Green - - if ($engineName === "Torque") { - $healthStatusColor = '#0EC23E'; // Green - } elseif ( - ($mechanicalHealthValue1 > 0 && $mechanicalHealthValue1 < 31) || - ($engineEfficiencyValue1 > 0 && $engineEfficiencyValue1 < 31) || - ($engineEfficiencyValue4 > 0 && $engineEfficiencyValue4 < 31) || - ($powerLossValue1 > 0 && $powerLossValue1 < 31) - ) { - $healthStatusColor = '#EF7F30'; // Red - } elseif ( - ($mechanicalHealthValue1 > 30 && $mechanicalHealthValue1 < 71) || - ($engineEfficiencyValue1 > 30 && $engineEfficiencyValue1 < 71) || - ($engineEfficiencyValue4 > 30 && $engineEfficiencyValue4 < 71) || - ($powerLossValue1 > 30 && $powerLossValue1 < 71) - ) { - $healthStatusColor = '#FFC164'; // Yellow - } - - $device->health_status = "
"; - } - - - return response()->json($user); - } catch (Exception $e) { - Log::error('Error fetching telemetry data: ' . $e->getMessage()); - return response()->json(['error' => 'Failed to fetch data'], 500); + if (!$user) { + return response()->json(['error' => 'User not found'], 404); } + + $bearerToken = $this->adminService->getToken(); + $apiBaseUrl = env('THINGSBOARD_URL', 'http://65.0.131.117:8080'); + + foreach ($user->assets->flatMap->devices as $device) { + $device->health_status = null; + $device->online = null; + $device->has_alarm = false; + + // 1. First check if device exists in ThingsBoard + $deviceCheckResponse = Http::withHeaders([ + 'accept' => 'application/json', + 'Authorization' => 'Bearer ' . $bearerToken, + ])->get("$apiBaseUrl/api/device/{$device->id}"); + + if (!$deviceCheckResponse->successful()) { + Log::warning("Device not found in ThingsBoard: {$device->id}"); + continue; // Skip this device + } + + // Fetch device details from API using customer_id + $deviceResponse = Http::withHeaders([ + 'accept' => 'application/json', + 'Authorization' => 'Bearer ' . $bearerToken, + ])->get("$apiBaseUrl/api/customer/{$device->customer_id}/deviceInfos", [ + 'pageSize' => 100, + 'page' => 0 + ]); + + if (!$deviceResponse->successful()) { + Log::error("Failed to fetch device info for Customer ID: {$device->customer_id}, Error: " . $deviceResponse->body()); + continue; + } + + $deviceData = collect($deviceResponse->json()['data'] ?? []); + $matchingDevice = $deviceData->firstWhere('id.id', $device->id); + + // Set online status + $device->online = $matchingDevice['active'] ?? null; + + // Fetch alarm data for the device (last 24 hours) + $twentyFourHoursAgo = (time() - 86400) * 1000; + $currentTime = time() * 1000; + + // Fixed: Use separate API calls for each alarm status + $alarmStatuses = ['ACTIVE', 'ACKNOWLEDGED', 'CLEARED']; + $hasAlarm = false; + + foreach ($alarmStatuses as $status) { + $alarmResponse = Http::withHeaders([ + 'accept' => 'application/json', + 'Authorization' => 'Bearer ' . $bearerToken, + ])->get("$apiBaseUrl/api/alarm/DEVICE/{$device->id}", [ + 'pageSize' => 100, + 'page' => 0, + 'startTime' => $twentyFourHoursAgo, + 'endTime' => $currentTime, + 'fetchOriginator' => 'false', + 'searchStatus' => $status + ]); + + if ($alarmResponse->successful()) { + $alarms = $alarmResponse->json()['data'] ?? []; + if (!empty($alarms)) { + $hasAlarm = true; + break; // No need to check other statuses if we found an alarm + } + } else { + Log::error("Failed to fetch $status alarms for Device ID: {$device->id}, Error: " . $alarmResponse->body()); + } + } + + $device->has_alarm = $hasAlarm; + + // Fetch telemetry data for the device + $telemetryResponse = Http::withHeaders([ + 'accept' => 'application/json', + 'Authorization' => 'Bearer ' . $bearerToken, + ])->get("$apiBaseUrl/api/plugins/telemetry/DEVICE/{$device->id}/values/timeseries", [ + 'useStrictDataTypes' => 'false' + ]); + + if (!$telemetryResponse->successful()) { + Log::error("Failed to fetch telemetry for Device ID: {$device->id}, Error: " . $telemetryResponse->body()); + continue; + } + + $telemetryData = $telemetryResponse->json(); + + $engineName = $telemetryData['Engine_Name'][0]['value'] ?? null; + + // Extract values from telemetry data + $mechanicalHealthValue1 = isset($telemetryData['MechanicalHealth_value'][0]['value']) + ? (float) $telemetryData['MechanicalHealth_value'][0]['value'] + : null; + + $engineEfficiencyValue1 = isset($telemetryData['EngineEfficiency_value'][0]['value']) + ? (float) $telemetryData['EngineEfficiency_value'][0]['value'] + : null; + + $engineEfficiencyValue4 = isset($telemetryData['EngineEfficiency_valueInHealth'][0]['value']) + ? (float) $telemetryData['EngineEfficiency_valueInHealth'][0]['value'] + : null; + + $powerLossValue1 = isset($telemetryData['PowerLoss_value'][0]['value']) + ? (float) $telemetryData['PowerLoss_value'][0]['value'] + : null; + + // Default health status + $healthStatusColor = '#0EC23E'; // Green + + if ($engineName === "Torque") { + $healthStatusColor = '#0EC23E'; // Green + } elseif ( + ($mechanicalHealthValue1 > 0 && $mechanicalHealthValue1 < 31) || + ($engineEfficiencyValue1 > 0 && $engineEfficiencyValue1 < 31) || + ($engineEfficiencyValue4 > 0 && $engineEfficiencyValue4 < 31) || + ($powerLossValue1 > 0 && $powerLossValue1 < 31) + ) { + $healthStatusColor = '#EF7F30'; // Red + } elseif ( + ($mechanicalHealthValue1 > 30 && $mechanicalHealthValue1 < 71) || + ($engineEfficiencyValue1 > 30 && $engineEfficiencyValue1 < 71) || + ($engineEfficiencyValue4 > 30 && $engineEfficiencyValue4 < 71) || + ($powerLossValue1 > 30 && $powerLossValue1 < 71) + ) { + $healthStatusColor = '#FFC164'; // Yellow + } + + $device->health_status = "
"; + } + + return response()->json($user); + } catch (Exception $e) { + Log::error('Error fetching telemetry data: ' . $e->getMessage()); + return response()->json(['error' => 'Failed to fetch data'], 500); } } +} diff --git a/app/Services/CustomerInfoService.php b/app/Services/CustomerInfoService.php index 060d02d..7845d46 100644 --- a/app/Services/CustomerInfoService.php +++ b/app/Services/CustomerInfoService.php @@ -355,4 +355,4 @@ class CustomerInfoService } } -} \ No newline at end of file +}