Skip to content

Commit 87bee9c

Browse files
Copilotkbeaugrand
andcommitted
Add planning artifacts for device import overwrite analysis
Co-authored-by: kbeaugrand <9513635+kbeaugrand@users.noreply.github.com>
1 parent 2fa8406 commit 87bee9c

6 files changed

Lines changed: 1242 additions & 57 deletions

File tree

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# IoT-Hub-Portal Development Guidelines
2+
3+
Auto-generated from all feature plans. Last updated: 2026-01-30
4+
5+
## Active Technologies
6+
7+
- C# / .NET 8.0 (027-analyze-issue-3251)
8+
9+
## Project Structure
10+
11+
```text
12+
src/
13+
tests/
14+
```
15+
16+
## Commands
17+
18+
# Add commands for C# / .NET 8.0
19+
20+
## Code Style
21+
22+
C# / .NET 8.0: Follow standard conventions
23+
24+
## Recent Changes
25+
26+
- 027-analyze-issue-3251: Added C# / .NET 8.0
27+
28+
<!-- MANUAL ADDITIONS START -->
29+
<!-- MANUAL ADDITIONS END -->
Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
# API Contract: Device Import Endpoint
2+
3+
**Endpoint**: `POST /api/admin/devices/_import`
4+
**Purpose**: Import devices from CSV file
5+
**Status**: No changes - this documents existing behavior with bug fix applied
6+
7+
## Request
8+
9+
### HTTP Method
10+
`POST`
11+
12+
### Headers
13+
```
14+
Content-Type: multipart/form-data
15+
Authorization: Bearer {jwt_token}
16+
```
17+
18+
### Body
19+
Form data with single file upload:
20+
- **Field name**: `file`
21+
- **File type**: CSV (text/csv)
22+
- **Max size**: (configured by server)
23+
24+
### CSV Format
25+
26+
**Mandatory columns**:
27+
- `Id`: Device identifier (16 hex characters, uppercase)
28+
- `Name`: Device name (free text)
29+
- `ModelId`: Device model GUID
30+
31+
**Optional tag columns** (format: `TAG:{tagName}`):
32+
- `TAG:supportLoRaFeatures`: "true" for LoRaWAN devices, empty/"false" for standard
33+
- `TAG:{anyCustomTag}`: Custom device tags from device tag configuration
34+
35+
**Optional LoRaWAN property columns** (format: `PROPERTY:{propertyName}`):
36+
37+
Authentication (OTAA):
38+
- `PROPERTY:AppKey`: Application Key (hex string)
39+
- `PROPERTY:AppEUI`: Application EUI (hex string)
40+
41+
Authentication (ABP):
42+
- `PROPERTY:AppSKey`: Application Session Key (hex string)
43+
- `PROPERTY:NwkSKey`: Network Session Key (hex string)
44+
- `PROPERTY:DevAddr`: Device Address (hex string)
45+
46+
Connection:
47+
- `PROPERTY:GatewayID`: Preferred gateway identifier
48+
49+
Configuration:
50+
- `PROPERTY:ClassType`: LoRaWAN class ("A", "B", or "C")
51+
- `PROPERTY:PreferredWindow`: Preferred receive window (1 or 2)
52+
- `PROPERTY:Deduplication`: Deduplication mode ("None", "Drop", or "Mark")
53+
- `PROPERTY:Downlink`: Enable downlinks ("true" or "false")
54+
55+
Advanced OTAA Settings:
56+
- `PROPERTY:RX1DROffset`: RX1 data rate offset (integer 0-7)
57+
- `PROPERTY:RX2DataRate`: RX2 data rate (integer 0-15)
58+
- `PROPERTY:RXDelay`: RX delay in seconds (integer 0-15)
59+
60+
Advanced ABP Settings:
61+
- `PROPERTY:ABPRelaxMode`: Enable ABP relaxed mode ("true" or "false")
62+
- `PROPERTY:FCntUpStart`: Uplink frame counter start (integer)
63+
- `PROPERTY:FCntDownStart`: Downlink frame counter start (integer)
64+
- `PROPERTY:FCntResetCounter`: Frame counter reset value (integer)
65+
- `PROPERTY:Supports32BitFCnt`: Support 32-bit frame counters ("true" or "false")
66+
67+
Other:
68+
- `PROPERTY:KeepAliveTimeout`: Keep-alive timeout in seconds (integer)
69+
- `PROPERTY:SensorDecoder`: Sensor decoder API URL (string)
70+
71+
### Example CSV Content
72+
```csv
73+
Id,Name,ModelId,TAG:supportLoRaFeatures,TAG:location,PROPERTY:AppKey,PROPERTY:AppEUI,PROPERTY:ClassType,PROPERTY:PreferredWindow,PROPERTY:Downlink
74+
ABCD1234ABCD1234,Sensor-01,550e8400-e29b-41d4-a716-446655440000,true,Building-A,FEDCBA9876543210FEDCBA9876543210,0102030405060708,A,1,true
75+
EFGH5678EFGH5678,Sensor-02,550e8400-e29b-41d4-a716-446655440000,true,Building-B,0123456789ABCDEF0123456789ABCDEF,0807060504030201,C,2,false
76+
```
77+
78+
## Response
79+
80+
### Success Response (200 OK)
81+
```json
82+
{
83+
"message": "Import completed",
84+
"totalProcessed": 2,
85+
"errors": []
86+
}
87+
```
88+
89+
Or with partial success:
90+
```json
91+
{
92+
"message": "Import completed with errors",
93+
"totalProcessed": 5,
94+
"errors": [
95+
{
96+
"lineNumber": 3,
97+
"deviceId": "BADDEVICE123456",
98+
"isErrorMessage": true,
99+
"message": "The parameter Id cannot be null or empty"
100+
},
101+
{
102+
"lineNumber": 7,
103+
"deviceId": "1234567890ABCDEF",
104+
"isErrorMessage": true,
105+
"message": "The device identifier must contain 16 hexadecimal characters"
106+
}
107+
]
108+
}
109+
```
110+
111+
**Response Model**:
112+
```csharp
113+
public class ImportResultLine
114+
{
115+
public string DeviceId { get; set; }
116+
public int LineNumber { get; set; }
117+
public bool IsErrorMessage { get; set; }
118+
public string Message { get; set; }
119+
}
120+
```
121+
122+
### Error Responses
123+
124+
**400 Bad Request** - Invalid file format
125+
```json
126+
{
127+
"title": "Bad Request",
128+
"status": 400,
129+
"detail": "Invalid file format: The submitted file should be a comma-separated values (CSV) file. A template file showing the mandatory fields is available to download on the portal."
130+
}
131+
```
132+
133+
**401 Unauthorized** - Missing or invalid authentication
134+
```json
135+
{
136+
"title": "Unauthorized",
137+
"status": 401
138+
}
139+
```
140+
141+
**403 Forbidden** - User lacks required permission
142+
```json
143+
{
144+
"title": "Forbidden",
145+
"status": 403,
146+
"detail": "User does not have permission to import devices"
147+
}
148+
```
149+
150+
## Validation Rules
151+
152+
### Device ID Validation
153+
- **Required**: Yes
154+
- **Format**: Exactly 16 hexadecimal characters (0-9, A-F)
155+
- **Regex**: `^[A-Z0-9]{16}$`
156+
- **Error**: "The parameter Id cannot be null or empty" or "The device identifier must contain 16 hexadecimal characters"
157+
158+
### Device Name Validation
159+
- **Required**: Yes
160+
- **Error**: "The parameter Name cannot be null or empty"
161+
162+
### Model ID Validation
163+
- **Required**: Yes
164+
- **Must exist**: Must reference an existing device model in the database
165+
- **Error**: "The parameter ModelId cannot be null or empty" or "Device model not found"
166+
167+
### LoRaWAN Authentication Validation
168+
For LoRaWAN devices (`TAG:supportLoRaFeatures = true`):
169+
- **OTAA mode**: Requires `PROPERTY:AppKey` AND `PROPERTY:AppEUI`
170+
- **ABP mode**: Requires `PROPERTY:AppSKey` AND `PROPERTY:NwkSKey` AND `PROPERTY:DevAddr`
171+
- **Detection**: If AppKey and AppEUI are provided → OTAA; if empty → ABP mode expected
172+
173+
### Property Type Validation
174+
- **ClassType**: Must be "A", "B", or "C" (case-sensitive)
175+
- **Deduplication**: Must be "None", "Drop", or "Mark"
176+
- **Boolean fields**: "true"/"false" (case-insensitive)
177+
- **Integer fields**: Valid integer within range (e.g., RX1DROffset: 0-7)
178+
179+
## Behavior Changes (Bug Fix)
180+
181+
### Before Fix
182+
- Only authentication properties were persisted to Azure IoT Hub
183+
- Configuration properties (ClassType, PreferredWindow, Deduplication, etc.) were stored in local database only
184+
- Synchronization job would overwrite local database with incomplete data from IoT Hub
185+
- **Result**: Imported configuration properties were lost after sync
186+
187+
### After Fix
188+
- **All LoRaWAN properties** are persisted to Azure IoT Hub as desired properties
189+
- Local database remains in sync with IoT Hub
190+
- Synchronization job refreshes local database from IoT Hub without data loss
191+
- **Result**: Imported configuration properties are preserved
192+
193+
## Security Considerations
194+
195+
### Authorization
196+
- Requires authenticated user with `AdminRole` permission
197+
- Policy: `Policies.AdminAccess` (defined in `IoTHub.Portal.Shared.Security.Policies`)
198+
199+
### Data Validation
200+
- All CSV input is validated before processing
201+
- Invalid rows are logged but do not stop processing of valid rows
202+
- Error messages do not expose sensitive system information
203+
204+
### Rate Limiting
205+
- (Not currently implemented - consider for production)
206+
207+
## Related Endpoints
208+
209+
### Export Devices
210+
`GET /api/admin/devices/_export` - Export all devices to CSV
211+
212+
### Download Template
213+
`GET /api/admin/devices/_template` - Download CSV template with headers
214+
215+
## Testing Contract
216+
217+
### Happy Path Test Cases
218+
1. **New LoRaWAN device (OTAA)**: All required fields + all optional LoRaWAN fields
219+
2. **New LoRaWAN device (ABP)**: ABP auth fields + configuration fields
220+
3. **Update existing device**: Same device ID with new configuration
221+
4. **Mixed standard and LoRaWAN**: CSV with both device types
222+
223+
### Error Test Cases
224+
1. **Missing required field**: Id, Name, or ModelId empty
225+
2. **Invalid device ID format**: Less than 16 chars, invalid characters
226+
3. **Invalid enum value**: ClassType = "D", Deduplication = "Invalid"
227+
4. **Incomplete authentication**: AppKey without AppEUI
228+
5. **Invalid file format**: Not a CSV, missing headers
229+
230+
### Edge Cases
231+
1. **Empty CSV**: Only headers, no data rows
232+
2. **CSV with only old columns**: No new LoRaWAN configuration columns (should use defaults)
233+
3. **CSV with extra unknown columns**: Should be ignored
234+
4. **Duplicate device IDs in CSV**: Last entry wins (update behavior)
235+
236+
## Notes
237+
238+
- The import is idempotent: importing the same device twice updates it
239+
- Existing devices are updated, not replaced (version tracking maintained)
240+
- Tags and labels are completely replaced on update (delete + recreate)
241+
- Device Twin version in IoT Hub is incremented on each update
242+
- Import is transactional per-device (one device failure doesn't rollback others)
243+
244+
## Change Log
245+
246+
**2026-01-30**: Bug fix - Added persistence of all LoRaWAN configuration properties to IoT Hub during import

0 commit comments

Comments
 (0)