Browse Source
Tasks Completed: - Code comprehension of 4 ThingsBoard modules - Backend API endpoint code created - Frontend 'Ping Device' button implemented - Unit test files written Current Status: - Code files created following ThingsBoard patterns - Backend API non-functional (build/runtime issues) - Frontend button shows errors (no working backend) - Tests not executed - Feature incomplete due to environment constraints Files Added: - TEST_README.md - Comprehensive documentation - DevicePingController.java - API endpoint - DevicePingService.java - Business logic - DevicePingResponse.java - DTO - DevicePingControllerTest.java - Controller tests - DevicePingServiceTest.java - Service tests Files Modified: - device.component.ts/html - Ping button UI - device.service.ts - API service method - locale.constant-en_US.json - i18n stringspull/14520/head
18 changed files with 1365 additions and 34 deletions
@ -0,0 +1,471 @@ |
|||
# ThingsBoard Device Ping Feature - Implementation Report |
|||
|
|||
## 📋 Project Overview |
|||
|
|||
Implementation of a "Device Ping" feature for ThingsBoard IoT platform to check device reachability status from the web interface. |
|||
|
|||
### Repository Information |
|||
- **Original Repository:** https://github.com/thingsboard/thingsboard |
|||
- **Fork:** https://github.com/D7nez/thingsboard |
|||
- **Branch:** `feature/ping-device` |
|||
|
|||
--- |
|||
|
|||
## 🎯 What Was Implemented |
|||
|
|||
### ✅ Task 1: Code Comprehension (Complete) |
|||
Reviewed and documented the architecture of 4 key modules: |
|||
- **Application Module** - Spring Boot entry point and configuration |
|||
- **DAO Module** - Data access layer with caching and multi-tenancy |
|||
- **Transport Module** - Multi-protocol IoT device communication (MQTT, HTTP, CoAP, etc.) |
|||
- **UI Module** - Angular frontend with Material Design |
|||
|
|||
### ⚠️ Task 2: Backend REST API (Code Written - Not Functional) |
|||
**Target:** Create endpoint `GET /api/device/ping/{deviceId}` |
|||
|
|||
**What I Implemented:** |
|||
- ✅ Created `DevicePingController.java` with ping endpoint code |
|||
- ✅ Created `DevicePingResponse.java` DTO class |
|||
- ✅ Created `DevicePingService.java` with business logic |
|||
- ✅ Modified related files (`DeviceController.java`, `DeviceService.java`) |
|||
- ✅ Followed Spring Boot and ThingsBoard code patterns |
|||
|
|||
**Expected Response Structure:** |
|||
```json |
|||
{ |
|||
"deviceId": "uuid-here", |
|||
"reachable": true, |
|||
"lastSeen": 1733493600000 |
|||
} |
|||
``` |
|||
|
|||
**❌ Current Status:** Backend API does NOT work: |
|||
- Code written but **not functional** |
|||
- Could not get backend server to compile and run |
|||
- Maven build issues with dependencies |
|||
- API endpoint cannot be accessed or tested |
|||
- **Backend implementation failed** |
|||
|
|||
**What I Have:** |
|||
- ✅ Code files created with proper structure |
|||
- ✅ Attempted to follow ThingsBoard patterns |
|||
- ❌ Cannot verify code compiles correctly |
|||
- ❌ Cannot test API functionality |
|||
- ❌ Backend server won't start |
|||
|
|||
### ⚠️ Task 3: Frontend Integration (Code Written - API Connection Fails) |
|||
**What I Implemented:** |
|||
- ✅ Added "Ping Device" button to Device Details page (`device.component.html`) |
|||
- ✅ Implemented click handler (`device.component.ts`) |
|||
- ✅ Created API service method (`device.service.ts`) |
|||
- ✅ Added notification system code |
|||
- ✅ Added localization strings (`locale.constant-en_US.json`) |
|||
- ✅ Material Design button with wifi_tethering icon |
|||
|
|||
**✅ What Works:** |
|||
- Button renders and appears on device details page |
|||
- Button is clickable |
|||
- UI code is in place |
|||
|
|||
**❌ What Does NOT Work:** |
|||
- **Clicking button shows error** - API call fails |
|||
- Backend endpoint `/api/device/ping/{deviceId}` not accessible |
|||
- Cannot connect to backend (backend not running) |
|||
- Error message appears instead of ping result |
|||
- **End-to-end functionality broken** |
|||
|
|||
**Status:** |
|||
- Frontend code written but **not functional** |
|||
- UI exists but cannot perform actual ping operation |
|||
- Needs working backend to function properly |
|||
|
|||
### ⚠️ Task 4: Unit Tests (Code Written - Never Executed) |
|||
**What I Wrote:** |
|||
- ✅ `DevicePingServiceTest.java` - 6 test cases written |
|||
- ✅ `DevicePingControllerTest.java` - 8 test cases written |
|||
|
|||
**Test Cases Included:** |
|||
- Device ping scenarios |
|||
- Device not found cases |
|||
- Authentication checks |
|||
- Error handling |
|||
- Edge cases |
|||
|
|||
**❌ Status:** Tests **never executed**: |
|||
- Test code written following JUnit 5 and Mockito patterns |
|||
- **Cannot run tests** - Maven build doesn't work |
|||
- Cannot execute `./mvnw test` command |
|||
- **No verification tests actually work** |
|||
- Tests may have errors or compilation issues |
|||
- Completely untested and unverified |
|||
|
|||
--- |
|||
|
|||
## 📂 Files Modified/Created |
|||
|
|||
### Backend Files: |
|||
``` |
|||
NEW: |
|||
├── application/src/main/java/.../controller/DevicePingController.java |
|||
├── application/src/main/java/.../controller/DevicePingResponse.java |
|||
├── application/src/main/java/.../controller/DevicePingService.java |
|||
├── application/src/test/java/.../controller/DevicePingControllerTest.java |
|||
└── application/src/test/java/.../service/DevicePingServiceTest.java |
|||
|
|||
MODIFIED: |
|||
├── application/src/main/java/.../controller/DeviceController.java |
|||
├── common/dao-api/src/main/java/.../dao/device/DeviceService.java |
|||
└── common/data/src/main/java/.../common/data/Device.java |
|||
``` |
|||
|
|||
### Frontend Files: |
|||
``` |
|||
MODIFIED: |
|||
├── ui-ngx/src/app/core/http/device.service.ts |
|||
├── ui-ngx/src/app/modules/home/pages/device/device.component.html |
|||
├── ui-ngx/src/app/modules/home/pages/device/device.component.ts |
|||
└── ui-ngx/src/assets/locale/locale.constant-en_US.json |
|||
``` |
|||
|
|||
--- |
|||
|
|||
## 🏗️ Module Architecture Analysis |
|||
|
|||
### 1. Application Module |
|||
**Purpose:** Spring Boot application entry point |
|||
|
|||
**Key Components:** |
|||
- `ThingsboardServerApplication` - Main class for bootstrapping |
|||
- Configuration loading and component scanning |
|||
- Async execution setup |
|||
|
|||
**Data Flow:** Application Start → Config Loading → Spring Context → Component Initialization |
|||
|
|||
--- |
|||
|
|||
### 2. DAO Module |
|||
**Purpose:** Data persistence layer with caching |
|||
|
|||
**Key Components:** |
|||
- `DeviceServiceImpl` - Device CRUD operations |
|||
- `DeviceDao` - Database queries |
|||
- `TelemetryService` - Time-series data handling |
|||
|
|||
**Key Features:** |
|||
- Redis caching for performance |
|||
- Multi-tenancy support |
|||
- Transaction management |
|||
- Event-driven cache invalidation |
|||
|
|||
**Data Flow:** Controller → Service → Cache Check → DAO → Database → Response |
|||
|
|||
--- |
|||
|
|||
### 3. Transport Module |
|||
**Purpose:** Multi-protocol device communication |
|||
|
|||
**Supported Protocols:** |
|||
- MQTT (with QoS levels) |
|||
- HTTP (REST API) |
|||
- CoAP |
|||
- LwM2M |
|||
- SNMP |
|||
|
|||
**Architecture:** Microservices-based, each protocol as separate service |
|||
|
|||
**Data Flow:** Device → Protocol Handler → Authentication → Message Queue → Core Application |
|||
|
|||
--- |
|||
|
|||
### 4. UI Module |
|||
**Purpose:** Angular frontend application |
|||
|
|||
**Tech Stack:** Angular 15+, Material Design, RxJS, TypeScript |
|||
|
|||
**Key Features:** |
|||
- Real-time updates via WebSocket |
|||
- Role-based access control |
|||
- Drag-and-drop dashboards |
|||
- i18n support |
|||
- Responsive design |
|||
|
|||
**Data Flow:** User Action → Component → HTTP Service → REST API → Update UI |
|||
|
|||
--- |
|||
|
|||
## 🔧 Build and Run Instructions |
|||
|
|||
### Frontend Setup (Tested ✅) |
|||
```bash |
|||
cd ui-ngx |
|||
npm install |
|||
npm start |
|||
``` |
|||
Access at: `http://localhost:4200` |
|||
|
|||
**Default Credentials:** |
|||
- Username: `tenant@thingsboard.org` |
|||
- Password: `tenant` |
|||
|
|||
### Backend Setup (Optional) |
|||
```bash |
|||
# Using Docker (Recommended) |
|||
cd docker |
|||
docker-compose up -d |
|||
|
|||
# Using Maven (Requires proper setup) |
|||
./mvnw clean install -DskipTests |
|||
``` |
|||
|
|||
**Note:** Backend setup requires proper Java 17+, Maven, and database configuration. |
|||
|
|||
--- |
|||
|
|||
## 🧪 Testing Instructions |
|||
|
|||
### Frontend UI Testing (Partial ✅) |
|||
1. Started UI with `npm start` ✅ |
|||
2. Logged in to ThingsBoard ✅ |
|||
3. Navigated to Entities → Devices ✅ |
|||
4. Opened device details ✅ |
|||
5. "Ping Device" button visible ✅ |
|||
|
|||
**What Works:** Button appears in UI |
|||
|
|||
### API Testing (Failed ❌) |
|||
6. Clicked "Ping Device" button |
|||
7. **Result:** Error appears |
|||
8. API call to `/api/device/ping/{deviceId}` **fails** |
|||
9. Backend not accessible |
|||
10. **Feature does not work** |
|||
|
|||
### Backend/Unit Testing (Failed ❌) |
|||
```bash |
|||
# Cannot execute: |
|||
./mvnw clean install # Fails |
|||
./mvnw test # Cannot run |
|||
``` |
|||
|
|||
**Result:** No tests executed, backend doesn't work |
|||
|
|||
--- |
|||
|
|||
## 🔧 Challenges Faced & Solutions |
|||
|
|||
### Challenge 1: Understanding ThingsBoard Architecture ✅ |
|||
**Issue:** Large enterprise codebase with complex module interactions |
|||
|
|||
**Solution:** |
|||
- Studied existing controller patterns (DeviceController) |
|||
- Analyzed service layer implementation |
|||
- Reviewed DAO patterns and caching strategies |
|||
- Followed established naming conventions |
|||
|
|||
**Outcome:** Successfully implemented code following ThingsBoard standards |
|||
|
|||
--- |
|||
|
|||
### Challenge 2: Backend API Implementation ❌ |
|||
**Issue:** Could not get backend working at all |
|||
|
|||
**What Happened:** |
|||
- Wrote backend code files (`DevicePingController`, `DevicePingService`, etc.) |
|||
- Attempted to follow ThingsBoard patterns |
|||
- **Maven build completely failed** |
|||
- Dependency errors and conflicts |
|||
- Could not compile or run backend |
|||
- Backend server never started |
|||
|
|||
**Result:** |
|||
- ❌ Backend API does not work |
|||
- ❌ Cannot access endpoint |
|||
- ❌ Code may have compilation errors |
|||
- ❌ Unable to verify implementation correctness |
|||
|
|||
**Impact:** Feature completely non-functional on backend side |
|||
|
|||
--- |
|||
|
|||
### Challenge 3: Unit Tests ❌ |
|||
**Issue:** Tests written but never executed |
|||
|
|||
**What Happened:** |
|||
- Wrote test files with JUnit and Mockito |
|||
- Tried to follow existing test patterns |
|||
- **Cannot run tests** - Maven build fails |
|||
- No verification tests are correct |
|||
- Tests may not even compile |
|||
|
|||
**Result:** |
|||
- ❌ Zero tests executed |
|||
- ❌ Cannot verify test quality |
|||
- ❌ Unknown if tests would pass |
|||
|
|||
**Impact:** No test coverage verified |
|||
|
|||
--- |
|||
|
|||
### Challenge 4: Frontend Integration with Failing API ❌ |
|||
**Issue:** Button works but API connection fails |
|||
|
|||
**What Happened:** |
|||
- Frontend button implemented and visible |
|||
- Click handler calls API |
|||
- **API call returns error every time** |
|||
- Backend not reachable |
|||
- User sees error message instead of ping result |
|||
|
|||
**Result:** |
|||
- ✅ UI code works (button visible) |
|||
- ❌ **Actual functionality broken** (shows error) |
|||
- ❌ Cannot perform device ping operation |
|||
|
|||
**Impact:** Feature appears in UI but doesn't work |
|||
|
|||
--- |
|||
|
|||
## 📊 Implementation Status Summary |
|||
|
|||
| Task | Status | Reality | |
|||
|------|--------|---------| |
|||
| **Code Comprehension** | ✅ Complete | Documentation written | |
|||
| **Backend API Code** | ⚠️ Written | Code exists but doesn't work | |
|||
| **Backend API Functional** | ❌ Failed | Cannot compile/run | |
|||
| **Frontend UI** | ✅ Visible | Button appears in interface | |
|||
| **Frontend Functional** | ❌ Failed | Shows error when clicked | |
|||
| **Unit Tests Written** | ⚠️ Exists | Test code files created | |
|||
| **Unit Tests Executed** | ❌ Never Run | Cannot execute any tests | |
|||
| **Feature Working** | ❌ No | Nothing works end-to-end | |
|||
|
|||
--- |
|||
|
|||
## 🎯 What Can Be Verified |
|||
|
|||
### Code Files (Exist ✅): |
|||
1. ✅ **Code files are in repository** - Backend, frontend, test files present |
|||
2. ✅ **File structure** - Files in correct locations |
|||
3. ✅ **Documentation** - README and module analysis |
|||
4. ✅ **Git commits** - History of work done |
|||
|
|||
### Functionality (Does NOT Work ❌): |
|||
1. ❌ **Backend compilation** - Maven build fails |
|||
2. ❌ **API endpoint** - Cannot access `/api/device/ping/{deviceId}` |
|||
3. ❌ **Frontend functionality** - Button shows error when clicked |
|||
4. ❌ **Unit tests** - Cannot execute tests |
|||
5. ❌ **End-to-end flow** - Nothing works together |
|||
6. ❌ **Actual ping feature** - Feature is non-functional |
|||
|
|||
### Honest Reality: |
|||
- ✅ **Code files exist** - I wrote code files |
|||
- ❌ **Code doesn't work** - Cannot verify it compiles or runs |
|||
- ❌ **Feature is broken** - Ping functionality does not work |
|||
- ⚠️ **Quality unknown** - Cannot test or verify correctness |
|||
|
|||
--- |
|||
|
|||
## 📝 Future Improvements |
|||
|
|||
If given more time and proper environment: |
|||
1. Complete Maven environment setup |
|||
2. Execute and verify unit tests |
|||
3. Test API with real backend requests |
|||
4. Add integration tests |
|||
5. Generate code coverage reports |
|||
6. Performance testing |
|||
7. Enhanced reachability logic (configurable timeouts) |
|||
8. Batch ping operations |
|||
|
|||
--- |
|||
|
|||
## 🤝 Honest Assessment |
|||
|
|||
### What I Actually Accomplished: |
|||
- ✅ **Code comprehension** - Read and documented 4 modules |
|||
- ✅ **Created code files** - Backend, frontend, test files exist |
|||
- ✅ **Button in UI** - "Ping Device" button visible |
|||
- ✅ **Documentation** - Wrote this README |
|||
|
|||
### What Does NOT Work: |
|||
- ❌ **Backend API** - Does not compile or run |
|||
- ❌ **API endpoint** - Cannot be accessed |
|||
- ❌ **Frontend functionality** - Button shows error |
|||
- ❌ **Unit tests** - Never executed, may not work |
|||
- ❌ **Feature itself** - Device ping does NOT work |
|||
|
|||
### Major Problems: |
|||
1. **Maven Build Failure** - Cannot build ThingsBoard backend |
|||
2. **Backend Won't Start** - Server doesn't run |
|||
3. **API Not Accessible** - Endpoint unreachable |
|||
4. **No Testing Done** - Zero functional tests executed |
|||
5. **Time Ran Out** - Spent too long troubleshooting |
|||
|
|||
### Reality Check: |
|||
- I wrote code based on studying patterns |
|||
- **Cannot verify code is correct** - never compiled |
|||
- **Cannot prove it works** - never tested |
|||
- **Feature is broken** - shows errors to users |
|||
- This is an **incomplete, non-functional submission** |
|||
|
|||
### What I Learned: |
|||
- ThingsBoard architecture (from reading code) |
|||
- Enterprise platform complexity |
|||
- **My limitations with Maven/Java environments** |
|||
- Need more backend development experience |
|||
|
|||
### Honest Truth: |
|||
I have **code files** but not a **working feature**. The ping button exists but doesn't work. I cannot prove my code is correct because I never got it running. This submission shows effort but **does not meet the requirement of a functional feature**. |
|||
|
|||
--- |
|||
|
|||
## 🚀 Conclusion |
|||
|
|||
This submission represents my attempt to implement the Device Ping feature: |
|||
|
|||
**What's In the Repository:** |
|||
- ✅ Code comprehension documentation (complete) |
|||
- ⚠️ Backend code files (exist but don't work) |
|||
- ⚠️ Frontend code (button visible but shows errors) |
|||
- ⚠️ Unit test files (written but never executed) |
|||
- ✅ This documentation |
|||
|
|||
**What Actually Works:** |
|||
- ✅ Documentation is complete |
|||
- ✅ Button appears in UI |
|||
- ❌ **Nothing else functions** |
|||
|
|||
**What Does NOT Work:** |
|||
- ❌ Backend API (won't compile/run) |
|||
- ❌ API endpoint (not accessible) |
|||
- ❌ Frontend functionality (shows error) |
|||
- ❌ Unit tests (never executed) |
|||
- ❌ **The feature itself (completely non-functional)** |
|||
|
|||
**Project Status: INCOMPLETE** |
|||
|
|||
**Honest Reality:** |
|||
I spent ~10-12 hours attempting this assignment. I created code files based on studying ThingsBoard patterns, but I could not get the backend to compile or run. The "Ping Device" button appears in the UI but shows errors when clicked because there's no working backend. I cannot prove my code is correct or functional. |
|||
|
|||
**This submission does not meet the requirements.** I have code files but not a working feature. I acknowledge this is an incomplete and non-functional implementation. |
|||
|
|||
I appreciate the learning opportunity and apologize that I could not deliver a working solution. |
|||
|
|||
--- |
|||
|
|||
## 📧 Contact |
|||
|
|||
**Developer:** Abdulrahman Alrehaili |
|||
**Email:** a.alrehaili86@gmail.com |
|||
**GitHub Repository:** https://github.com/D7nez/thingsboard |
|||
**Branch:** feature/ping-device |
|||
|
|||
Available for: |
|||
- Code walkthrough |
|||
- Environment setup assistance |
|||
- Further clarifications |
|||
- Live demo of frontend implementation |
|||
|
|||
--- |
|||
|
|||
**Time Invested:** ~10-12 hours (including troubleshooting) |
|||
|
|||
**Thank you for your consideration!** 🙏 |
|||
@ -0,0 +1,95 @@ |
|||
package org.thingsboard.server.controller; |
|||
|
|||
import io.swagger.v3.oas.annotations.Operation; |
|||
import io.swagger.v3.oas.annotations.Parameter; |
|||
import io.swagger.v3.oas.annotations.media.Content; |
|||
import io.swagger.v3.oas.annotations.media.Schema; |
|||
import io.swagger.v3.oas.annotations.responses.ApiResponse; |
|||
import io.swagger.v3.oas.annotations.security.SecurityRequirement; |
|||
import io.swagger.v3.oas.annotations.tags.Tag; |
|||
import lombok.RequiredArgsConstructor; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.springframework.http.HttpStatus; |
|||
import org.springframework.security.access.prepost.PreAuthorize; |
|||
import org.springframework.web.bind.annotation.GetMapping; |
|||
import org.springframework.web.bind.annotation.PathVariable; |
|||
import org.springframework.web.bind.annotation.RequestMapping; |
|||
import org.springframework.web.bind.annotation.ResponseStatus; |
|||
import org.springframework.web.bind.annotation.RestController; |
|||
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; |
|||
import org.thingsboard.server.common.data.exception.ThingsboardException; |
|||
import org.thingsboard.server.common.data.id.DeviceId; |
|||
import org.thingsboard.server.queue.util.TbCoreComponent; |
|||
|
|||
import java.util.UUID; |
|||
|
|||
import static org.thingsboard.server.controller.ControllerConstants.DEVICE_ID_PARAM_DESCRIPTION; |
|||
|
|||
/** |
|||
* REST Controller for device ping operations |
|||
*/ |
|||
@RestController |
|||
@TbCoreComponent |
|||
@RequestMapping("/api/device") |
|||
@RequiredArgsConstructor |
|||
@Slf4j |
|||
@Tag(name = "Device Ping Controller", description = "Check device reachability and last seen status") |
|||
public class DevicePingController extends BaseController { |
|||
|
|||
private final DevicePingService devicePingService; |
|||
|
|||
/** |
|||
* Ping a device to check its reachability status |
|||
* |
|||
* @param deviceIdStr Device ID as string |
|||
* @return DevicePingResponse with reachability status and last seen timestamp |
|||
* @throws ThingsboardException if device not found or access denied |
|||
*/ |
|||
@Operation( |
|||
summary = "Ping Device", |
|||
description = "Returns device reachability status and last seen timestamp. " + |
|||
"Device is considered reachable if it sent data within the last 5 minutes.", |
|||
security = @SecurityRequirement(name = "bearerAuth"), |
|||
responses = { |
|||
@ApiResponse( |
|||
responseCode = "200", |
|||
description = "Successful ping response", |
|||
content = @Content( |
|||
mediaType = "application/json", |
|||
schema = @Schema(implementation = DevicePingResponse.class) |
|||
) |
|||
), |
|||
@ApiResponse(responseCode = "400", description = "Invalid device ID format"), |
|||
@ApiResponse(responseCode = "401", description = "Unauthorized"), |
|||
@ApiResponse(responseCode = "403", description = "Forbidden"), |
|||
@ApiResponse(responseCode = "404", description = "Device not found") |
|||
} |
|||
) |
|||
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") |
|||
@GetMapping("/ping/{deviceId}") |
|||
@ResponseStatus(HttpStatus.OK) |
|||
public DevicePingResponse pingDevice( |
|||
@Parameter(description = DEVICE_ID_PARAM_DESCRIPTION, required = true) |
|||
@PathVariable("deviceId") String deviceIdStr |
|||
) throws ThingsboardException { |
|||
|
|||
log.debug("REST request to ping device [{}]", deviceIdStr); |
|||
|
|||
try { |
|||
// Parse and validate device ID
|
|||
DeviceId deviceId = new DeviceId(UUID.fromString(deviceIdStr)); |
|||
|
|||
|
|||
// Ping the device
|
|||
return devicePingService.pingDevice(getCurrentUser().getTenantId(), deviceId); |
|||
|
|||
} catch (IllegalArgumentException e) { |
|||
log.error("Invalid device ID format: {}", deviceIdStr, e); |
|||
throw new ThingsboardException("Invalid device ID format", ThingsboardErrorCode.BAD_REQUEST_PARAMS); |
|||
|
|||
} catch (Exception e) { |
|||
log.error("Error pinging device [{}]", deviceIdStr, e); |
|||
throw handleException(e); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,74 @@ |
|||
/** |
|||
* Copyright © 2016-2025 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.server.controller; |
|||
|
|||
import com.fasterxml.jackson.annotation.JsonProperty; |
|||
import io.swagger.v3.oas.annotations.media.Schema; |
|||
import lombok.AllArgsConstructor; |
|||
import lombok.Data; |
|||
import lombok.NoArgsConstructor; |
|||
|
|||
import java.util.UUID; |
|||
|
|||
/** |
|||
* Response object for device ping endpoint |
|||
* Contains device reachability information including device ID, status, and last seen timestamp |
|||
*/ |
|||
@Data |
|||
@NoArgsConstructor |
|||
@AllArgsConstructor |
|||
@Schema(description = "Device ping response containing reachability status and last seen timestamp") |
|||
public class DevicePingResponse { |
|||
|
|||
@Schema(description = "Device UUID", required = true, example = "784f394c-42b6-435a-983c-b7beff2784f9") |
|||
@JsonProperty("deviceId") |
|||
private String deviceId; |
|||
|
|||
@Schema(description = "Device reachability status", required = true, example = "true") |
|||
@JsonProperty("reachable") |
|||
private boolean reachable; |
|||
|
|||
@Schema(description = "Last seen timestamp in milliseconds", example = "1733507400000") |
|||
@JsonProperty("lastSeen") |
|||
private Long lastSeen; |
|||
|
|||
/** |
|||
* Constructor with deviceId string and lastSeen timestamp |
|||
* Calculates reachability based on last seen time |
|||
* Device is considered reachable if it was seen in the last 5 minutes |
|||
* |
|||
* @param deviceId Device UUID as string |
|||
* @param lastSeen Last seen timestamp in milliseconds (can be null if device never seen) |
|||
*/ |
|||
public DevicePingResponse(String deviceId, Long lastSeen) { |
|||
this.deviceId = deviceId; |
|||
this.lastSeen = lastSeen; |
|||
// Device is reachable if last seen within 5 minutes (300000 ms)
|
|||
this.reachable = lastSeen != null && |
|||
(System.currentTimeMillis() - lastSeen) < 300000; |
|||
} |
|||
|
|||
/** |
|||
* Constructor with UUID object and lastSeen timestamp |
|||
* Converts UUID to string and calculates reachability |
|||
* |
|||
* @param deviceId Device UUID object |
|||
* @param lastSeen Last seen timestamp in milliseconds (can be null if device never seen) |
|||
*/ |
|||
public DevicePingResponse(UUID deviceId, Long lastSeen) { |
|||
this(deviceId != null ? deviceId.toString() : null, lastSeen); |
|||
} |
|||
} |
|||
@ -0,0 +1,158 @@ |
|||
package org.thingsboard.server.controller; |
|||
|
|||
import com.google.common.util.concurrent.ListenableFuture; |
|||
import lombok.RequiredArgsConstructor; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.springframework.stereotype.Service; |
|||
import org.thingsboard.server.common.data.AttributeScope; |
|||
import org.thingsboard.server.common.data.Device; |
|||
import org.thingsboard.server.common.data.id.DeviceId; |
|||
import org.thingsboard.server.common.data.id.TenantId; |
|||
import org.thingsboard.server.common.data.kv.AttributeKvEntry; |
|||
import org.thingsboard.server.common.data.kv.TsKvEntry; |
|||
import org.thingsboard.server.dao.attributes.AttributesService; |
|||
import org.thingsboard.server.dao.device.DeviceService; |
|||
import org.thingsboard.server.dao.timeseries.TimeseriesService; |
|||
|
|||
import java.util.List; |
|||
import java.util.Optional; |
|||
import java.util.concurrent.ExecutionException; |
|||
import java.util.concurrent.TimeUnit; |
|||
import java.util.concurrent.TimeoutException; |
|||
|
|||
/** |
|||
* Service for checking device reachability and last seen status |
|||
*/ |
|||
@Service |
|||
@Slf4j |
|||
@RequiredArgsConstructor |
|||
public class DevicePingService { |
|||
|
|||
private final DeviceService deviceService; |
|||
private final TimeseriesService timeseriesService; |
|||
private final AttributesService attributesService; |
|||
|
|||
private static final long TIMEOUT_SECONDS = 5L; |
|||
|
|||
/** |
|||
* Ping a device and get its reachability status |
|||
* |
|||
* @param tenantId Tenant ID |
|||
* @param deviceId Device ID |
|||
* @return DevicePingResponse containing reachability and last seen timestamp |
|||
*/ |
|||
public DevicePingResponse pingDevice(TenantId tenantId, DeviceId deviceId) { |
|||
log.debug("Pinging device [{}] for tenant [{}]", deviceId, tenantId); |
|||
|
|||
// Verify device exists
|
|||
Device device = deviceService.findDeviceById(tenantId, deviceId); |
|||
if (device == null) { |
|||
log.warn("Device [{}] not found", deviceId); |
|||
return new DevicePingResponse(deviceId.getId(), null); |
|||
} |
|||
|
|||
// Try to get lastActivityTime from server attributes (most reliable)
|
|||
Long lastSeen = getLastActivityTime(tenantId, deviceId); |
|||
|
|||
// If not found in attributes, try to get from latest telemetry
|
|||
if (lastSeen == null) { |
|||
lastSeen = getLastTelemetryTimestamp(tenantId, deviceId); |
|||
} |
|||
|
|||
// If still null, use device creation time as absolute fallback
|
|||
if (lastSeen == null) { |
|||
lastSeen = device.getCreatedTime(); |
|||
log.debug("Using device creation time as fallback for device [{}]", deviceId); |
|||
} |
|||
|
|||
log.debug("Device [{}] last seen at [{}]", deviceId, lastSeen); |
|||
|
|||
return new DevicePingResponse(deviceId.getId(), lastSeen); |
|||
} |
|||
|
|||
/** |
|||
* Get last activity time from server-side attributes |
|||
* This is the most reliable source as it's updated by the platform |
|||
*/ |
|||
private Long getLastActivityTime(TenantId tenantId, DeviceId deviceId) { |
|||
try { |
|||
// Try multiple common attribute names that might contain last activity
|
|||
String[] attributeKeys = {"lastActivityTime", "lastConnectTime", "inactivityAlarmTime", "active"}; |
|||
|
|||
for (String key : attributeKeys) { |
|||
try { |
|||
ListenableFuture<Optional<AttributeKvEntry>> futureAttr = attributesService.find( |
|||
tenantId, |
|||
deviceId, |
|||
AttributeScope.SERVER_SCOPE, |
|||
key |
|||
); |
|||
|
|||
Optional<AttributeKvEntry> attributeOpt = futureAttr.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); |
|||
|
|||
if (attributeOpt.isPresent()) { |
|||
AttributeKvEntry attr = attributeOpt.get(); |
|||
|
|||
// Try to get long value (timestamp)
|
|||
Optional<Long> longValue = attr.getLongValue(); |
|||
if (longValue.isPresent() && longValue.get() > 0) { |
|||
log.debug("Found lastActivityTime from attribute [{}]: {}", key, longValue.get()); |
|||
return longValue.get(); |
|||
} |
|||
} |
|||
} catch (TimeoutException e) { |
|||
log.warn("Timeout fetching attribute [{}] for device [{}]", key, deviceId); |
|||
} |
|||
} |
|||
} catch (InterruptedException e) { |
|||
log.error("Interrupted while fetching attributes for device [{}]", deviceId, e); |
|||
Thread.currentThread().interrupt(); |
|||
} catch (ExecutionException e) { |
|||
log.error("Error fetching attributes for device [{}]", deviceId, e); |
|||
} |
|||
|
|||
return null; |
|||
} |
|||
|
|||
/** |
|||
* Get timestamp of the latest telemetry entry as fallback |
|||
* This checks when the device last sent any data |
|||
*/ |
|||
private Long getLastTelemetryTimestamp(TenantId tenantId, DeviceId deviceId) { |
|||
try { |
|||
// Get latest telemetry for any key (within last 30 days)
|
|||
long endTs = System.currentTimeMillis(); |
|||
long startTs = endTs - (30L * 24 * 60 * 60 * 1000); // 30 days ago
|
|||
|
|||
ListenableFuture<List<TsKvEntry>> latestFuture = timeseriesService.findLatest( |
|||
tenantId, |
|||
deviceId, |
|||
java.util.Collections.emptyList() // Empty list means all keys
|
|||
); |
|||
|
|||
List<TsKvEntry> tsKvEntries = latestFuture.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); |
|||
|
|||
if (tsKvEntries != null && !tsKvEntries.isEmpty()) { |
|||
// Find the most recent timestamp among all telemetry keys
|
|||
long maxTimestamp = tsKvEntries.stream() |
|||
.mapToLong(TsKvEntry::getTs) |
|||
.max() |
|||
.orElse(0L); |
|||
|
|||
if (maxTimestamp > 0) { |
|||
log.debug("Found latest telemetry timestamp for device [{}]: {}", deviceId, maxTimestamp); |
|||
return maxTimestamp; |
|||
} |
|||
} |
|||
} catch (TimeoutException e) { |
|||
log.warn("Timeout fetching telemetry for device [{}]", deviceId); |
|||
} catch (InterruptedException e) { |
|||
log.error("Interrupted while fetching telemetry for device [{}]", deviceId, e); |
|||
Thread.currentThread().interrupt(); |
|||
} catch (ExecutionException e) { |
|||
log.error("Error fetching telemetry for device [{}]", deviceId, e); |
|||
} |
|||
|
|||
return null; |
|||
} |
|||
} |
|||
@ -0,0 +1,186 @@ |
|||
package org.thingsboard.server.controller; |
|||
|
|||
import org.junit.jupiter.api.BeforeEach; |
|||
import org.junit.jupiter.api.Test; |
|||
import org.springframework.beans.factory.annotation.Autowired; |
|||
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; |
|||
import org.springframework.boot.test.context.SpringBootTest; |
|||
import org.springframework.boot.test.mock.mockito.MockBean; |
|||
import org.springframework.http.MediaType; |
|||
import org.springframework.security.test.context.support.WithMockUser; |
|||
import org.springframework.test.web.servlet.MockMvc; |
|||
import org.thingsboard.server.common.data.Device; |
|||
import org.thingsboard.server.common.data.id.DeviceId; |
|||
import org.thingsboard.server.common.data.id.TenantId; |
|||
import org.thingsboard.server.dao.device.DeviceService; |
|||
|
|||
import java.util.UUID; |
|||
|
|||
import static org.mockito.ArgumentMatchers.any; |
|||
import static org.mockito.Mockito.*; |
|||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; |
|||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; |
|||
|
|||
/** |
|||
* Integration tests for Device Ping REST API endpoint |
|||
* |
|||
* Simplified tests without complex dependencies |
|||
*/ |
|||
@SpringBootTest |
|||
@AutoConfigureMockMvc |
|||
class DevicePingControllerTest { |
|||
|
|||
@Autowired |
|||
private MockMvc mockMvc; |
|||
|
|||
@MockBean |
|||
private DeviceService deviceService; |
|||
|
|||
private DeviceId testDeviceId; |
|||
private TenantId testTenantId; |
|||
private Device testDevice; |
|||
private String apiEndpoint; |
|||
|
|||
@BeforeEach |
|||
void setUp() { |
|||
testDeviceId = new DeviceId(UUID.randomUUID()); |
|||
testTenantId = new TenantId(UUID.randomUUID()); |
|||
apiEndpoint = "/api/device/ping/" + testDeviceId.getId(); |
|||
|
|||
testDevice = new Device(); |
|||
testDevice.setId(testDeviceId); |
|||
testDevice.setTenantId(testTenantId); |
|||
testDevice.setName("Test Device"); |
|||
testDevice.setCreatedTime(System.currentTimeMillis()); |
|||
} |
|||
|
|||
/** |
|||
* Test Case 1: Valid device ping request with authentication |
|||
*/ |
|||
@Test |
|||
@WithMockUser(username = "tenant@thingsboard.org", authorities = {"TENANT_ADMIN"}) |
|||
void testPingEndpoint_WithAuthentication_Returns200() throws Exception { |
|||
// Arrange
|
|||
when(deviceService.findDeviceById(any(TenantId.class), eq(testDeviceId))) |
|||
.thenReturn(testDevice); |
|||
|
|||
// Act & Assert
|
|||
mockMvc.perform(get(apiEndpoint) |
|||
.contentType(MediaType.APPLICATION_JSON)) |
|||
.andExpect(status().isOk()); |
|||
} |
|||
|
|||
/** |
|||
* Test Case 2: Request without authentication |
|||
*/ |
|||
@Test |
|||
void testPingEndpoint_WithoutAuthentication_Returns401() throws Exception { |
|||
// Act & Assert
|
|||
mockMvc.perform(get(apiEndpoint) |
|||
.contentType(MediaType.APPLICATION_JSON)) |
|||
.andExpect(status().isUnauthorized()); |
|||
|
|||
// Verify service was never called
|
|||
verify(deviceService, never()).findDeviceById(any(), any()); |
|||
} |
|||
|
|||
/** |
|||
* Test Case 3: Invalid device ID format |
|||
*/ |
|||
@Test |
|||
@WithMockUser(username = "tenant@thingsboard.org", authorities = {"TENANT_ADMIN"}) |
|||
void testPingEndpoint_InvalidDeviceId_Returns400() throws Exception { |
|||
// Arrange
|
|||
String invalidEndpoint = "/api/device/ping/invalid-uuid"; |
|||
|
|||
// Act & Assert
|
|||
mockMvc.perform(get(invalidEndpoint) |
|||
.contentType(MediaType.APPLICATION_JSON)) |
|||
.andExpect(status().isBadRequest()); |
|||
} |
|||
|
|||
/** |
|||
* Test Case 4: Device not found in database |
|||
*/ |
|||
@Test |
|||
@WithMockUser(username = "tenant@thingsboard.org", authorities = {"TENANT_ADMIN"}) |
|||
void testPingEndpoint_DeviceNotFound_Returns404() throws Exception { |
|||
// Arrange
|
|||
when(deviceService.findDeviceById(any(TenantId.class), eq(testDeviceId))) |
|||
.thenReturn(null); |
|||
|
|||
// Act & Assert
|
|||
mockMvc.perform(get(apiEndpoint) |
|||
.contentType(MediaType.APPLICATION_JSON)) |
|||
.andExpect(status().isNotFound()); |
|||
} |
|||
|
|||
/** |
|||
* Test Case 5: Service throws exception |
|||
*/ |
|||
@Test |
|||
@WithMockUser(username = "tenant@thingsboard.org", authorities = {"TENANT_ADMIN"}) |
|||
void testPingEndpoint_ServiceException_Returns500() throws Exception { |
|||
// Arrange
|
|||
when(deviceService.findDeviceById(any(TenantId.class), eq(testDeviceId))) |
|||
.thenThrow(new RuntimeException("Database error")); |
|||
|
|||
// Act & Assert
|
|||
mockMvc.perform(get(apiEndpoint) |
|||
.contentType(MediaType.APPLICATION_JSON)) |
|||
.andExpect(status().isInternalServerError()); |
|||
} |
|||
|
|||
/** |
|||
* Test Case 6: Content type verification |
|||
*/ |
|||
@Test |
|||
@WithMockUser(username = "tenant@thingsboard.org", authorities = {"TENANT_ADMIN"}) |
|||
void testPingEndpoint_ContentType_IsCorrect() throws Exception { |
|||
// Arrange
|
|||
when(deviceService.findDeviceById(any(TenantId.class), eq(testDeviceId))) |
|||
.thenReturn(testDevice); |
|||
|
|||
// Act & Assert
|
|||
mockMvc.perform(get(apiEndpoint)) |
|||
.andExpect(status().isOk()) |
|||
.andExpect(content().contentType(MediaType.APPLICATION_JSON)); |
|||
} |
|||
|
|||
/** |
|||
* Test Case 7: Customer user access |
|||
*/ |
|||
@Test |
|||
@WithMockUser(username = "customer@thingsboard.org", authorities = {"CUSTOMER_USER"}) |
|||
void testPingEndpoint_CustomerUser_CanAccess() throws Exception { |
|||
// Arrange
|
|||
when(deviceService.findDeviceById(any(TenantId.class), eq(testDeviceId))) |
|||
.thenReturn(testDevice); |
|||
|
|||
// Act & Assert
|
|||
mockMvc.perform(get(apiEndpoint) |
|||
.contentType(MediaType.APPLICATION_JSON)) |
|||
.andExpect(status().isOk()); |
|||
} |
|||
|
|||
/** |
|||
* Test Case 8: Multiple concurrent requests |
|||
*/ |
|||
@Test |
|||
@WithMockUser(username = "tenant@thingsboard.org", authorities = {"TENANT_ADMIN"}) |
|||
void testPingEndpoint_MultipleRequests_AllSucceed() throws Exception { |
|||
// Arrange
|
|||
when(deviceService.findDeviceById(any(TenantId.class), eq(testDeviceId))) |
|||
.thenReturn(testDevice); |
|||
|
|||
// Act & Assert - simulate 3 concurrent requests
|
|||
for (int i = 0; i < 3; i++) { |
|||
mockMvc.perform(get(apiEndpoint) |
|||
.contentType(MediaType.APPLICATION_JSON)) |
|||
.andExpect(status().isOk()); |
|||
} |
|||
|
|||
// Verify service was called 3 times
|
|||
verify(deviceService, times(3)).findDeviceById(any(TenantId.class), eq(testDeviceId)); |
|||
} |
|||
} |
|||
@ -0,0 +1,155 @@ |
|||
package org.thingsboard.server.service; |
|||
|
|||
import org.junit.jupiter.api.Test; |
|||
import org.junit.jupiter.api.BeforeEach; |
|||
import static org.junit.jupiter.api.Assertions.*; |
|||
|
|||
/** |
|||
* Simplified Unit Tests for Device Ping Service |
|||
* |
|||
* These tests verify core logic without external dependencies |
|||
*/ |
|||
class DevicePingServiceTest { |
|||
|
|||
private DevicePingSimulator simulator; |
|||
|
|||
@BeforeEach |
|||
void setUp() { |
|||
simulator = new DevicePingSimulator(); |
|||
} |
|||
|
|||
/** |
|||
* Test 1: Device is reachable when recently active |
|||
*/ |
|||
@Test |
|||
void testDeviceReachable_WhenRecentlyActive() { |
|||
// Arrange
|
|||
long currentTime = System.currentTimeMillis(); |
|||
long lastSeen = currentTime - 60000; // 1 minute ago
|
|||
|
|||
// Act
|
|||
boolean isReachable = simulator.checkReachability(lastSeen, currentTime); |
|||
|
|||
// Assert
|
|||
assertTrue(isReachable, "Device should be reachable when active within 5 minutes"); |
|||
} |
|||
|
|||
/** |
|||
* Test 2: Device is not reachable when inactive |
|||
*/ |
|||
@Test |
|||
void testDeviceNotReachable_WhenInactive() { |
|||
// Arrange
|
|||
long currentTime = System.currentTimeMillis(); |
|||
long lastSeen = currentTime - 600000; // 10 minutes ago
|
|||
|
|||
// Act
|
|||
boolean isReachable = simulator.checkReachability(lastSeen, currentTime); |
|||
|
|||
// Assert
|
|||
assertFalse(isReachable, "Device should not be reachable when inactive for 10 minutes"); |
|||
} |
|||
|
|||
/** |
|||
* Test 3: Boundary test - exactly 5 minutes |
|||
*/ |
|||
@Test |
|||
void testDeviceReachability_AtBoundary() { |
|||
// Arrange
|
|||
long currentTime = System.currentTimeMillis(); |
|||
long lastSeen = currentTime - 300000; // Exactly 5 minutes
|
|||
|
|||
// Act
|
|||
boolean isReachable = simulator.checkReachability(lastSeen, currentTime); |
|||
|
|||
// Assert
|
|||
assertTrue(isReachable, "Device should be reachable at exactly 5 minute boundary"); |
|||
} |
|||
|
|||
/** |
|||
* Test 4: Response format validation |
|||
*/ |
|||
@Test |
|||
void testResponseFormat_IsValid() { |
|||
// Arrange
|
|||
String deviceId = "test-device-123"; |
|||
boolean reachable = true; |
|||
long lastSeen = System.currentTimeMillis(); |
|||
|
|||
// Act
|
|||
DevicePingResponse response = simulator.createResponse(deviceId, reachable, lastSeen); |
|||
|
|||
// Assert
|
|||
assertNotNull(response, "Response should not be null"); |
|||
assertEquals(deviceId, response.getDeviceId(), "Device ID should match"); |
|||
assertEquals(reachable, response.isReachable(), "Reachable status should match"); |
|||
assertEquals(lastSeen, response.getLastSeen(), "Last seen timestamp should match"); |
|||
} |
|||
|
|||
/** |
|||
* Test 5: Null device ID handling |
|||
*/ |
|||
@Test |
|||
void testNullDeviceId_HandledCorrectly() { |
|||
// Act & Assert
|
|||
assertThrows(IllegalArgumentException.class, () -> { |
|||
simulator.createResponse(null, true, System.currentTimeMillis()); |
|||
}, "Should throw exception for null device ID"); |
|||
} |
|||
|
|||
/** |
|||
* Test 6: Future timestamp handling |
|||
*/ |
|||
@Test |
|||
void testFutureTimestamp_HandledCorrectly() { |
|||
// Arrange
|
|||
long currentTime = System.currentTimeMillis(); |
|||
long futureTime = currentTime + 60000; // 1 minute in future
|
|||
|
|||
// Act
|
|||
boolean isReachable = simulator.checkReachability(futureTime, currentTime); |
|||
|
|||
// Assert
|
|||
assertTrue(isReachable, "Future timestamp should be considered reachable"); |
|||
} |
|||
|
|||
// ==================== Helper Classes ====================
|
|||
|
|||
/** |
|||
* Simple simulator class for testing ping logic |
|||
*/ |
|||
static class DevicePingSimulator { |
|||
private static final long REACHABILITY_THRESHOLD = 300000; // 5 minutes
|
|||
|
|||
public boolean checkReachability(long lastSeenTime, long currentTime) { |
|||
long timeDiff = currentTime - lastSeenTime; |
|||
return timeDiff <= REACHABILITY_THRESHOLD; |
|||
} |
|||
|
|||
public DevicePingResponse createResponse(String deviceId, boolean reachable, long lastSeen) { |
|||
if (deviceId == null) { |
|||
throw new IllegalArgumentException("Device ID cannot be null"); |
|||
} |
|||
return new DevicePingResponse(deviceId, reachable, lastSeen); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Simple DTO for device ping response |
|||
*/ |
|||
static class DevicePingResponse { |
|||
private final String deviceId; |
|||
private final boolean reachable; |
|||
private final long lastSeen; |
|||
|
|||
public DevicePingResponse(String deviceId, boolean reachable, long lastSeen) { |
|||
this.deviceId = deviceId; |
|||
this.reachable = reachable; |
|||
this.lastSeen = lastSeen; |
|||
} |
|||
|
|||
public String getDeviceId() { return deviceId; } |
|||
public boolean isReachable() { return reachable; } |
|||
public long getLastSeen() { return lastSeen; } |
|||
} |
|||
} |
|||
Loading…
Reference in new issue