Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file modified .specify/scripts/bash/check-prerequisites.sh
100644 → 100755
Empty file.
116 changes: 114 additions & 2 deletions src/IoTHub.Portal.Infrastructure/Jobs/SyncDevicesJob.cs
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,109 @@
this.deviceTagValueRepository.Delete(deviceTagEntity.Id);
}

_ = this.mapper.Map(lorawanDevice, lorawanDeviceEntity);
// Update only properties that are present in the Twin
// Base device properties (always updated from Twin)
lorawanDeviceEntity.Name = lorawanDevice.Name;
lorawanDeviceEntity.DeviceModelId = lorawanDevice.DeviceModelId;
lorawanDeviceEntity.Version = lorawanDevice.Version;
lorawanDeviceEntity.IsConnected = lorawanDevice.IsConnected;
lorawanDeviceEntity.IsEnabled = lorawanDevice.IsEnabled;
lorawanDeviceEntity.StatusUpdatedTime = lorawanDevice.StatusUpdatedTime;
lorawanDeviceEntity.LastActivityTime = lorawanDevice.LastActivityTime;
lorawanDeviceEntity.LayerId = lorawanDevice.LayerId;
lorawanDeviceEntity.Tags = lorawanDevice.Tags;

// Update LoRaWAN properties only if they exist in the Twin's desired properties
// OTAA/ABP authentication settings
if (twin.Properties.Desired.Contains(nameof(LoRaDeviceDetails.AppEUI)))
lorawanDeviceEntity.AppEUI = lorawanDevice.AppEUI;

Check failure on line 152 in src/IoTHub.Portal.Infrastructure/Jobs/SyncDevicesJob.cs

View workflow job for this annotation

GitHub Actions / Analyze Csharp

Fix formatting (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0055)

Check failure on line 152 in src/IoTHub.Portal.Infrastructure/Jobs/SyncDevicesJob.cs

View workflow job for this annotation

GitHub Actions / Unit tests

Fix formatting (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0055)

if (twin.Properties.Desired.Contains(nameof(LoRaDeviceDetails.AppKey)))
lorawanDeviceEntity.AppKey = lorawanDevice.AppKey;

Check failure on line 155 in src/IoTHub.Portal.Infrastructure/Jobs/SyncDevicesJob.cs

View workflow job for this annotation

GitHub Actions / Analyze Csharp

Fix formatting (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0055)

Check failure on line 155 in src/IoTHub.Portal.Infrastructure/Jobs/SyncDevicesJob.cs

View workflow job for this annotation

GitHub Actions / Unit tests

Fix formatting (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0055)

if (twin.Properties.Desired.Contains(nameof(LoRaDeviceDetails.AppSKey)))
lorawanDeviceEntity.AppSKey = lorawanDevice.AppSKey;

Check failure on line 158 in src/IoTHub.Portal.Infrastructure/Jobs/SyncDevicesJob.cs

View workflow job for this annotation

GitHub Actions / Analyze Csharp

Fix formatting (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0055)

Check failure on line 158 in src/IoTHub.Portal.Infrastructure/Jobs/SyncDevicesJob.cs

View workflow job for this annotation

GitHub Actions / Unit tests

Fix formatting (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0055)

if (twin.Properties.Desired.Contains(nameof(LoRaDeviceDetails.NwkSKey)))
lorawanDeviceEntity.NwkSKey = lorawanDevice.NwkSKey;

Check failure on line 161 in src/IoTHub.Portal.Infrastructure/Jobs/SyncDevicesJob.cs

View workflow job for this annotation

GitHub Actions / Analyze Csharp

Fix formatting (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0055)

Check failure on line 161 in src/IoTHub.Portal.Infrastructure/Jobs/SyncDevicesJob.cs

View workflow job for this annotation

GitHub Actions / Unit tests

Fix formatting (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0055)

if (twin.Properties.Desired.Contains(nameof(LoRaDeviceDetails.DevAddr)))
lorawanDeviceEntity.DevAddr = lorawanDevice.DevAddr;

Check failure on line 164 in src/IoTHub.Portal.Infrastructure/Jobs/SyncDevicesJob.cs

View workflow job for this annotation

GitHub Actions / Analyze Csharp

Fix formatting (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0055)

Check failure on line 164 in src/IoTHub.Portal.Infrastructure/Jobs/SyncDevicesJob.cs

View workflow job for this annotation

GitHub Actions / Unit tests

Fix formatting (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0055)

// Update UseOTAA based on AppEUI presence in Twin

Check failure on line 166 in src/IoTHub.Portal.Infrastructure/Jobs/SyncDevicesJob.cs

View workflow job for this annotation

GitHub Actions / Analyze Csharp

Fix formatting (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0055)

Check failure on line 166 in src/IoTHub.Portal.Infrastructure/Jobs/SyncDevicesJob.cs

View workflow job for this annotation

GitHub Actions / Unit tests

Fix formatting (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0055)
// Only update if AppEUI exists in Twin to avoid overwriting database value

Check failure on line 167 in src/IoTHub.Portal.Infrastructure/Jobs/SyncDevicesJob.cs

View workflow job for this annotation

GitHub Actions / Analyze Csharp

Fix formatting (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0055)

Check failure on line 167 in src/IoTHub.Portal.Infrastructure/Jobs/SyncDevicesJob.cs

View workflow job for this annotation

GitHub Actions / Unit tests

Fix formatting (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0055)
if (twin.Properties.Desired.Contains(nameof(LoRaDeviceDetails.AppEUI)))
lorawanDeviceEntity.UseOTAA = lorawanDevice.UseOTAA;

// Other LoRaWAN configuration properties (only update if present in Twin)
if (twin.Properties.Desired.Contains(nameof(LoRaDeviceDetails.SensorDecoder)))
lorawanDeviceEntity.SensorDecoder = lorawanDevice.SensorDecoder;

Check failure on line 173 in src/IoTHub.Portal.Infrastructure/Jobs/SyncDevicesJob.cs

View workflow job for this annotation

GitHub Actions / Analyze Csharp

Fix formatting (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0055)

Check failure on line 173 in src/IoTHub.Portal.Infrastructure/Jobs/SyncDevicesJob.cs

View workflow job for this annotation

GitHub Actions / Unit tests

Fix formatting (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0055)

if (twin.Properties.Desired.Contains(nameof(LoRaDeviceDetails.ClassType)))
lorawanDeviceEntity.ClassType = lorawanDevice.ClassType;

Check failure on line 176 in src/IoTHub.Portal.Infrastructure/Jobs/SyncDevicesJob.cs

View workflow job for this annotation

GitHub Actions / Analyze Csharp

Fix formatting (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0055)

Check failure on line 176 in src/IoTHub.Portal.Infrastructure/Jobs/SyncDevicesJob.cs

View workflow job for this annotation

GitHub Actions / Unit tests

Fix formatting (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0055)

if (twin.Properties.Desired.Contains(nameof(LoRaDeviceDetails.PreferredWindow)))
lorawanDeviceEntity.PreferredWindow = lorawanDevice.PreferredWindow;

Check failure on line 179 in src/IoTHub.Portal.Infrastructure/Jobs/SyncDevicesJob.cs

View workflow job for this annotation

GitHub Actions / Analyze Csharp

Fix formatting (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0055)

Check failure on line 179 in src/IoTHub.Portal.Infrastructure/Jobs/SyncDevicesJob.cs

View workflow job for this annotation

GitHub Actions / Unit tests

Fix formatting (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0055)

if (twin.Properties.Desired.Contains(nameof(LoRaDeviceDetails.Deduplication)))
lorawanDeviceEntity.Deduplication = lorawanDevice.Deduplication;

if (twin.Properties.Desired.Contains(nameof(LoRaDeviceDetails.RX1DROffset)))
lorawanDeviceEntity.RX1DROffset = lorawanDevice.RX1DROffset;

if (twin.Properties.Desired.Contains(nameof(LoRaDeviceDetails.RX2DataRate)))
lorawanDeviceEntity.RX2DataRate = lorawanDevice.RX2DataRate;

if (twin.Properties.Desired.Contains(nameof(LoRaDeviceDetails.RXDelay)))
lorawanDeviceEntity.RXDelay = lorawanDevice.RXDelay;

if (twin.Properties.Desired.Contains(nameof(LoRaDeviceDetails.ABPRelaxMode)))
lorawanDeviceEntity.ABPRelaxMode = lorawanDevice.ABPRelaxMode;

if (twin.Properties.Desired.Contains(nameof(LoRaDeviceDetails.FCntUpStart)))
lorawanDeviceEntity.FCntUpStart = lorawanDevice.FCntUpStart;

if (twin.Properties.Desired.Contains(nameof(LoRaDeviceDetails.FCntDownStart)))
lorawanDeviceEntity.FCntDownStart = lorawanDevice.FCntDownStart;

if (twin.Properties.Desired.Contains(nameof(LoRaDeviceDetails.FCntResetCounter)))
lorawanDeviceEntity.FCntResetCounter = lorawanDevice.FCntResetCounter;

if (twin.Properties.Desired.Contains(nameof(LoRaDeviceDetails.Supports32BitFCnt)))
lorawanDeviceEntity.Supports32BitFCnt = lorawanDevice.Supports32BitFCnt;

if (twin.Properties.Desired.Contains(nameof(LoRaDeviceDetails.KeepAliveTimeout)))
lorawanDeviceEntity.KeepAliveTimeout = lorawanDevice.KeepAliveTimeout;

if (twin.Properties.Desired.Contains(nameof(LoRaDeviceDetails.Downlink)))
lorawanDeviceEntity.Downlink = lorawanDevice.Downlink;

// Update reported properties only if they exist in Twin (as they come from the device)
// AlreadyLoggedInOnce is set based on DevAddr presence in reported properties
if (twin.Properties.Reported.Contains("DevAddr"))
lorawanDeviceEntity.AlreadyLoggedInOnce = lorawanDevice.AlreadyLoggedInOnce;

if (twin.Properties.Reported.Contains(nameof(LoRaDeviceDetails.GatewayID)))
lorawanDeviceEntity.GatewayID = lorawanDevice.GatewayID;

if (twin.Properties.Reported.Contains(nameof(LoRaDeviceDetails.DataRate)))
lorawanDeviceEntity.DataRate = lorawanDevice.DataRate;

if (twin.Properties.Reported.Contains(nameof(LoRaDeviceDetails.TxPower)))
lorawanDeviceEntity.TxPower = lorawanDevice.TxPower;

if (twin.Properties.Reported.Contains(nameof(LoRaDeviceDetails.NbRep)))
lorawanDeviceEntity.NbRep = lorawanDevice.NbRep;

if (twin.Properties.Reported.Contains(nameof(LoRaDeviceDetails.ReportedRX2DataRate)))
lorawanDeviceEntity.ReportedRX2DataRate = lorawanDevice.ReportedRX2DataRate;

if (twin.Properties.Reported.Contains(nameof(LoRaDeviceDetails.ReportedRX1DROffset)))
lorawanDeviceEntity.ReportedRX1DROffset = lorawanDevice.ReportedRX1DROffset;

if (twin.Properties.Reported.Contains(nameof(LoRaDeviceDetails.ReportedRXDelay)))
lorawanDeviceEntity.ReportedRXDelay = lorawanDevice.ReportedRXDelay;
Comment on lines +137 to +238
Copy link

Copilot AI Jan 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The mapper's Twin->LorawanDevice mapping uses helper methods like GetDesiredPropertyAsIntegerValue and GetDesiredPropertyAsEnum that return nullable values. When these properties are not present in the Twin, the mapper would set them to null or default values. However, the new implementation only updates properties if they're present in the Twin. This means that if a property WAS in the Twin before but is now removed, it won't be cleared from the database - it will keep the old value. Consider whether properties should be explicitly set to null/default when they're removed from the Twin, or document this as expected behavior.

Copilot uses AI. Check for mistakes.

this.lorawanDeviceRepository.Update(lorawanDeviceEntity);
}
}
Expand All @@ -157,7 +259,17 @@
this.deviceTagValueRepository.Delete(deviceTagEntity.Id);
}

_ = this.mapper.Map(device, deviceEntity);
// Update only core properties from Twin
deviceEntity.Name = device.Name;
deviceEntity.DeviceModelId = device.DeviceModelId;
deviceEntity.Version = device.Version;
deviceEntity.IsConnected = device.IsConnected;
deviceEntity.IsEnabled = device.IsEnabled;
deviceEntity.StatusUpdatedTime = device.StatusUpdatedTime;
deviceEntity.LastActivityTime = device.LastActivityTime;
deviceEntity.LayerId = device.LayerId;
deviceEntity.Tags = device.Tags;

this.deviceRepository.Update(deviceEntity);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -450,5 +450,131 @@ public async Task Execute_MissingModelId_ShouldNotFail()
// Assert
MockRepository.VerifyAll();
}

[Test]
public async Task Execute_ExistingLorawanDeviceWithMissingTwinProperties_DatabaseValuesPreserved()
{
// Arrange
var mockJobExecutionContext = MockRepository.Create<IJobExecutionContext>();

var expectedDeviceModel = Fixture.Create<DeviceModel>();
expectedDeviceModel.SupportLoRaFeatures = true;

// Twin with only core properties and AppKey/AppEUI (OTAA)
var expectedTwinDevice = new Twin
{
DeviceId = Fixture.Create<string>(),
Tags = new TwinCollection
{
["modelId"] = expectedDeviceModel.Id,
["deviceName"] = "UpdatedDeviceName"
},
Version = 2
};
expectedTwinDevice.Properties.Desired["AppKey"] = "NewAppKey";
expectedTwinDevice.Properties.Desired["AppEUI"] = "NewAppEUI";

// Existing device with additional LoRaWAN properties that should be preserved
var existingLorawanDevice = new LorawanDevice
{
Id = expectedTwinDevice.DeviceId,
Name = "OldDeviceName",
Version = 1,
AppKey = "OldAppKey",
AppEUI = "OldAppEUI",
UseOTAA = false, // Should be updated when AppEUI is in Twin
AlreadyLoggedInOnce = true, // Should be preserved (DevAddr not in reported properties)
SensorDecoder = "ExistingSensorDecoder", // Should be preserved
ClassType = ClassType.C, // Should be preserved
Deduplication = DeduplicationMode.Mark, // Should be preserved
RX1DROffset = 5, // Should be preserved
KeepAliveTimeout = 120, // Should be preserved
GatewayID = "ExistingGatewayID", // Should be preserved (not in reported properties)
Tags = new List<DeviceTagValue>
{
new()
{
Id = Fixture.Create<string>(),
Name = "existingTag",
Value = "existingValue"
}
}
};

_ = this.mockDeviceService
.Setup(x => x.GetAllDevice(
It.IsAny<string>(),
It.IsAny<string>(),
It.Is<string>(x => x == "LoRa Concentrator"),
It.IsAny<string>(),
It.IsAny<bool?>(),
It.IsAny<bool?>(),
It.IsAny<Dictionary<string, string>>(),
It.Is<int>(x => x == 100)))
.ReturnsAsync(new PaginationResult<Twin>
{
Items = new List<Twin>
{
expectedTwinDevice
},
TotalItems = 1
});

_ = this.mockDeviceModelRepository
.Setup(x => x.GetByIdAsync(expectedDeviceModel.Id))
.ReturnsAsync(expectedDeviceModel);

_ = this.mockLorawanDeviceRepository.Setup(repository => repository.GetByIdAsync(expectedTwinDevice.DeviceId, d => d.Tags))
.ReturnsAsync(existingLorawanDevice);

this.mockDeviceTagValueRepository.Setup(repository => repository.Delete(It.IsAny<string>()))
.Verifiable();

this.mockLorawanDeviceRepository.Setup(repository => repository.Update(It.Is<LorawanDevice>(d =>
// Verify core properties are updated
d.Name == "UpdatedDeviceName" &&
d.Version == 2 &&
// Verify Twin properties are updated
d.AppKey == "NewAppKey" &&
d.AppEUI == "NewAppEUI" &&
d.UseOTAA == true && // Updated because AppEUI is in Twin
Copy link

Copilot AI Jan 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The expression 'A == true' can be simplified to 'A'.

Copilot uses AI. Check for mistakes.
// Verify database-only properties are preserved
d.SensorDecoder == "ExistingSensorDecoder" &&
d.ClassType == ClassType.C &&
d.Deduplication == DeduplicationMode.Mark &&
d.RX1DROffset == 5 &&
d.KeepAliveTimeout == 120 &&
d.GatewayID == "ExistingGatewayID" &&
d.AlreadyLoggedInOnce == true // Preserved because DevAddr not in reported properties
Copy link

Copilot AI Jan 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The expression 'A == true' can be simplified to 'A'.

Copilot uses AI. Check for mistakes.
)))
.Verifiable();

_ = this.mockDeviceRepository.Setup(x => x.GetAllAsync(It.IsAny<Expression<Func<Device, bool>>>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new List<Device>
{
new Device
{
Id = expectedTwinDevice.DeviceId
}
});

_ = this.mockLorawanDeviceRepository.Setup(x => x.GetAllAsync(It.IsAny<Expression<Func<LorawanDevice, bool>>>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new List<LorawanDevice>
{
new LorawanDevice
{
Id = expectedTwinDevice.DeviceId
}
});

_ = this.mockUnitOfWork.Setup(work => work.SaveAsync())
.Returns(Task.CompletedTask);

// Act
await this.syncDevicesJob.Execute(mockJobExecutionContext.Object);

// Assert
MockRepository.VerifyAll();
}
}
}
Loading