Merge azure-iot-pcs-simulation into master (#197)
* Rewrite to use fewer threads and increase throughput * Expose simulation metrics in the status endpoint * When available, return a URL to the Azure Portal metrics of the pre-provisioned IoT Hub * Allow to run a simulation against a custom IoT Hub, passing the connection string * Allow to schedule a simulation, defining start and end time * Improve security by running the service as a non-root user * Support multiple behavior scripts in a device model * Support custom device with custom sensors, behavior and frequency * Allow to override the initial device state when creating a simulation * When a simulation starts, create all the devices in batches * When a simulation is deleted, delete also the devices * Refactor timers to start sending telemetry as soon as possible * Refactor and improve how simulation scripts access state and properties * Change stock models to use AMQP (#189) * Overall improvements to exceptions handling * Disable SDK retry, and change timeout from default (4 minutes) to 10 seconds * Do not retry sending telemetry on failure, skip message * Use IoT Hub S2 SKU limits by default * Upgrade to .NET Core 2.0.3 * Upgrade Azure IoT SDK: Devices 1.4.1 to 1.6.0, Devices Client 1.5.2 to 1.7.0 * Run simulation engine in the same process used for the web service * Move docs, move wiki, new API specs, fix scripts for Windows Bash * Fix the spelling of “ETag” and allow ETag=* * Add internal scripts for increasing/decreasing/random telemetry * Add env vars documentation ENVIRONMENT_VARIABLES.md * Add more optional logging by device and by actor * Use logging level and other logging settings from configuration * Adjust unit test precision to mitigate flaky test * Add system properties to telemetry messages * Removing the squash flag from Docker scripts * Use env vars in launchSettings.json
This commit is contained in:
Родитель
1e7089446a
Коммит
b211cb7805
|
@ -1,3 +1,5 @@
|
|||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
# Auto-detect text files, ensure they use LF.
|
||||
* text=auto eol=lf
|
||||
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
### Custom
|
||||
|
||||
_dev/
|
||||
|
|
10
.travis.yml
10
.travis.yml
|
@ -1,5 +1,5 @@
|
|||
language: csharp
|
||||
dotnet: 2.0.0
|
||||
dotnet: 2.0.3
|
||||
mono: none
|
||||
sudo: false
|
||||
cache:
|
||||
|
@ -10,10 +10,8 @@ before_install:
|
|||
addons:
|
||||
apt:
|
||||
packages:
|
||||
- dotnet-sharedframework-microsoft.netcore.app-1.1.2
|
||||
# This package is used only to address an issue in Travis CI, which would
|
||||
# otherwise install a newer 2.1.x preview which breaks the build.
|
||||
- dotnet-hostfxr-2.0.3
|
||||
script:
|
||||
- "bash ./$CODEBASE/scripts/build"
|
||||
notifications:
|
||||
slack:
|
||||
rooms:
|
||||
- secure: kj0k4OxDg11boMe2uKITH/PHvc3DUEv3pCmEW7LU1Nvyi5kRzmHOF5GDNUqE6RvAzJ2BspzPNfmgBOEqwBui4KBnSLQNFav6p06dEdKzAdfu808izYlIq7kylLbioGDg/rzu1O05nvPJNmBjK7t3X/bG0Wod60mGSb3BW0tqmZAcECi+Rdm1/IYFCRZgBsXcYDR98S4NngGB6wGlCBYCYhmu6pO+W2JL3Yyo2i4p5GWTvUuP+iQkh1t358cbnRadtfOI3PFg7F/wdrbvP/2GYl+SaHyxna2JsgGLgMLiQdEU6ag8xJP86BoOV40lIj3mtX20P1SHckUM1yugI9OGfgn163Rpcv3517mC8oJSN6piNR4KQIAkYRw1w5eMi+xnmmG3AfC8s2d0XxEjruu70I8LmzBpSObpFFKoG3NMxGx6I/ypYgelZKnNGvEv2LnZl7Tv4GhksPQ6PHYGT2E1RS84N+GPcqZmA31Mtzx9B6sVScC7t+h7XRPFhC2Se0/WBsXUvosSPN0s2s2Iz1x+A5Niun7oZGpfJ/mykbWbwoFjGzuzFMbMBzJJpj7a8iQPmoTBUbg29BbCNtw/HF0VM0uddNGuwAyWKfybNgg7vjHs5j2s2+44eWCN2PW0wnT3TNovpgTeB70DMtXVZKHXVgcBtiagwGuEl6as7ngqGoE=
|
||||
|
|
|
@ -169,7 +169,7 @@ Other resources
|
|||
Contributing to the solution
|
||||
============================
|
||||
|
||||
Please follow our [contribution guidelines](CONTRIBUTING.md). We love PRs too.
|
||||
Please follow our [contribution guidelines](docs/CONTRIBUTING.md). We love PRs too.
|
||||
|
||||
Troubleshooting
|
||||
===============
|
||||
|
|
|
@ -53,7 +53,7 @@ namespace Services.Test.Concurrency
|
|||
* The test takes about 1 minute, so it is disabled by default.
|
||||
*/
|
||||
//[Fact]
|
||||
[Fact(Skip="Test used only while debugging"), Trait(Constants.TYPE, Constants.UNIT_TEST), Trait(Constants.SPEED, Constants.SLOW_TEST)]
|
||||
[Fact(Skip="Skipping test used only while debugging"), Trait(Constants.TYPE, Constants.UNIT_TEST), Trait(Constants.SPEED, Constants.SLOW_TEST)]
|
||||
public void ItPausesWhenNeeded_DebuggingTest()
|
||||
{
|
||||
log.WriteLine("Starting test at " + DateTimeOffset.UtcNow.ToString("HH:mm:ss.fff"));
|
||||
|
|
|
@ -393,7 +393,7 @@ namespace Services.Test.Concurrency
|
|||
* with a limit of 20 events/second.
|
||||
*/
|
||||
//[Fact]
|
||||
[Fact(Skip = "Test used only while debugging"), Trait(Constants.TYPE, Constants.UNIT_TEST), Trait(Constants.SPEED, Constants.SLOW_TEST)]
|
||||
[Fact(Skip = "Skipping test used only while debugging"), Trait(Constants.TYPE, Constants.UNIT_TEST), Trait(Constants.SPEED, Constants.SLOW_TEST)]
|
||||
public void ItObtainsTheDesiredFrequency_DebuggingTest()
|
||||
{
|
||||
log.WriteLine("Starting test at " + DateTimeOffset.UtcNow.ToString("HH:mm:ss.fff"));
|
||||
|
|
|
@ -0,0 +1,70 @@
|
|||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Moq;
|
||||
using System.Collections.Generic;
|
||||
using DeviceModel = Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Models.DeviceModel;
|
||||
using Xunit.Abstractions;
|
||||
using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Diagnostics;
|
||||
using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services;
|
||||
using static Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Models.Simulation;
|
||||
using Xunit;
|
||||
using Services.Test.helpers;
|
||||
using System;
|
||||
using static Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Models.DeviceModel;
|
||||
|
||||
namespace Services.Test
|
||||
{
|
||||
public class DeviceModelsGenerationTest
|
||||
{
|
||||
/// <summary>The test logger</summary>
|
||||
private readonly ITestOutputHelper log;
|
||||
|
||||
private Mock<ILogger> logger;
|
||||
private DeviceModelsGeneration target;
|
||||
|
||||
public DeviceModelsGenerationTest(ITestOutputHelper log)
|
||||
{
|
||||
this.logger = new Mock<ILogger>();
|
||||
this.target = new DeviceModelsGeneration(this.logger.Object);
|
||||
}
|
||||
|
||||
[Fact, Trait(Constants.TYPE, Constants.UNIT_TEST)]
|
||||
public void OverrideWithoutMessageSchema()
|
||||
{
|
||||
// Arrange
|
||||
var source = new DeviceModel
|
||||
{
|
||||
Telemetry = new List<DeviceModelMessage>
|
||||
{
|
||||
new DeviceModelMessage()
|
||||
{
|
||||
Interval = TimeSpan.Zero,
|
||||
MessageTemplate = string.Empty,
|
||||
MessageSchema = new DeviceModelMessageSchema()
|
||||
{
|
||||
Name = "sensor-01",
|
||||
Format = DeviceModelMessageSchemaFormat.JSON,
|
||||
Fields = new Dictionary<string, DeviceModelMessageSchemaType>()
|
||||
{
|
||||
{ "temp", new DeviceModelMessageSchemaType() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var overrideInfo = new DeviceModelOverride();
|
||||
|
||||
// Act
|
||||
var result = this.target.Generate(source, overrideInfo);
|
||||
|
||||
// Assert
|
||||
Assert.NotEmpty(result.Telemetry);
|
||||
for (var i = 0; i < result.Telemetry.Count; i++)
|
||||
{
|
||||
Assert.Equal(source.Telemetry[i].MessageTemplate, result.Telemetry[i].MessageTemplate);
|
||||
Assert.Equal(source.Telemetry[i].MessageSchema.Name, result.Telemetry[i].MessageSchema.Name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,134 @@
|
|||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using Microsoft.Azure.Devices.Client;
|
||||
using Microsoft.Azure.Devices.Shared;
|
||||
using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services;
|
||||
using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Diagnostics;
|
||||
using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Models;
|
||||
using Moq;
|
||||
using Services.Test.helpers;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
using SdkClient = Microsoft.Azure.Devices.Client.DeviceClient;
|
||||
|
||||
namespace Services.Test
|
||||
{
|
||||
public class DevicePropertiesRequestTest
|
||||
{
|
||||
private const string DEVICE_ID = "01";
|
||||
private const string KEY1 = "Key1";
|
||||
private const string KEY2 = "Key2";
|
||||
private const string VALUE1 = "Value1";
|
||||
private const string VALUE2 = "Value2";
|
||||
|
||||
private Mock<IDeviceClient> client;
|
||||
private SdkClient sdkClient;
|
||||
private Mock<ILogger> logger;
|
||||
|
||||
private IDevicePropertiesRequest target;
|
||||
|
||||
public DevicePropertiesRequestTest(ITestOutputHelper log)
|
||||
{
|
||||
this.sdkClient = GetSdkClient();
|
||||
|
||||
this.client = new Mock<IDeviceClient>();
|
||||
this.logger = new Mock<ILogger>();
|
||||
|
||||
this.target = new DeviceProperties(sdkClient, this.logger.Object);
|
||||
}
|
||||
|
||||
[Fact, Trait(Constants.TYPE, Constants.UNIT_TEST)]
|
||||
public void SmartDictionary_Should_UpdateValue_When_DesiredPropertiesChange()
|
||||
{
|
||||
// Arrange
|
||||
const string NEW_VALUE = "new value";
|
||||
|
||||
ISmartDictionary reportedProps = GetTestProperties();
|
||||
this.target.RegisterChangeUpdateAsync(DEVICE_ID, reportedProps);
|
||||
|
||||
TwinCollection desiredProps = new TwinCollection();
|
||||
desiredProps[KEY1] = NEW_VALUE;
|
||||
|
||||
// Act
|
||||
// Use reflection to invoke private callback
|
||||
MethodInfo methodInfo = this.target.GetType().GetMethod("OnChangeCallback", BindingFlags.Instance | BindingFlags.NonPublic);
|
||||
methodInfo.Invoke(this.target, new object[] { desiredProps, null });
|
||||
|
||||
var result = reportedProps.Get(KEY1);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(result, NEW_VALUE);
|
||||
}
|
||||
|
||||
[Fact, Trait(Constants.TYPE, Constants.UNIT_TEST)]
|
||||
public void SmartDictionary_Should_HaveNewItem_When_NewDesiredPropertyAdded()
|
||||
{
|
||||
// Arrange
|
||||
const string NEW_KEY = "new key";
|
||||
const string NEW_VALUE = "new value";
|
||||
|
||||
ISmartDictionary reportedProps = GetTestProperties();
|
||||
this.target.RegisterChangeUpdateAsync(DEVICE_ID, reportedProps);
|
||||
|
||||
TwinCollection desiredProps = new TwinCollection();
|
||||
desiredProps[NEW_KEY] = NEW_VALUE;
|
||||
|
||||
// Act
|
||||
// Use reflection to invoke private callback
|
||||
MethodInfo methodInfo = this.target.GetType().GetMethod("OnChangeCallback", BindingFlags.Instance | BindingFlags.NonPublic);
|
||||
methodInfo.Invoke(this.target, new object[] { desiredProps, null });
|
||||
|
||||
var result = reportedProps.Get(NEW_KEY);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(result, NEW_VALUE);
|
||||
}
|
||||
|
||||
[Fact, Trait(Constants.TYPE, Constants.UNIT_TEST)]
|
||||
public void SmartDictionary_Should_Not_Update_When_DesiredPropertiesValueIsTheSame()
|
||||
{
|
||||
// Arrange
|
||||
ISmartDictionary reportedProps = GetTestProperties();
|
||||
reportedProps.ResetChanged();
|
||||
Assert.False(reportedProps.Changed);
|
||||
|
||||
this.target.RegisterChangeUpdateAsync(DEVICE_ID, reportedProps);
|
||||
|
||||
TwinCollection desiredProps = new TwinCollection
|
||||
{
|
||||
[KEY1] = VALUE1 // This should be the same value in props
|
||||
};
|
||||
|
||||
// Act
|
||||
// Use reflection to invoke private callback
|
||||
MethodInfo methodInfo = this.target.GetType().GetMethod("OnChangeCallback", BindingFlags.Instance | BindingFlags.NonPublic);
|
||||
methodInfo.Invoke(this.target, new object[] { desiredProps, null });
|
||||
|
||||
// Assert
|
||||
Assert.False(reportedProps.Changed);
|
||||
}
|
||||
|
||||
private SdkClient GetSdkClient()
|
||||
{
|
||||
var connectionString = $"HostName=somehost.azure-devices.net;DeviceId=" + DEVICE_ID + ";SharedAccessKeyName=iothubowner;SharedAccessKey=Test123+Test123456789+TestTestTestTestTest1=";
|
||||
|
||||
SdkClient sdkClient = SdkClient.CreateFromConnectionString(connectionString, TransportType.Mqtt_Tcp_Only);
|
||||
sdkClient.SetRetryPolicy(new NoRetry());
|
||||
|
||||
return sdkClient;
|
||||
}
|
||||
|
||||
private ISmartDictionary GetTestProperties()
|
||||
{
|
||||
SmartDictionary properties = new SmartDictionary();
|
||||
|
||||
properties.Set(KEY1, VALUE1);
|
||||
properties.Set(KEY2, VALUE2);
|
||||
|
||||
return properties;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,78 @@
|
|||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Azure.Devices;
|
||||
using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services;
|
||||
using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Diagnostics;
|
||||
using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Exceptions;
|
||||
using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.IotHub;
|
||||
using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Runtime;
|
||||
using Moq;
|
||||
using Services.Test.helpers;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace Services.Test
|
||||
{
|
||||
public class DevicesTest
|
||||
{
|
||||
/// <summary>The test logger</summary>
|
||||
private readonly ITestOutputHelper log;
|
||||
|
||||
private Mock<IServicesConfig> config;
|
||||
private Mock<IIotHubConnectionStringManager> connectionStringManager;
|
||||
private Mock<ILogger> logger;
|
||||
private readonly Mock<IRegistryManager> registry;
|
||||
private readonly Devices target;
|
||||
|
||||
public DevicesTest(ITestOutputHelper log)
|
||||
{
|
||||
this.log = log;
|
||||
|
||||
this.config = new Mock<IServicesConfig>();
|
||||
this.connectionStringManager = new Mock<IIotHubConnectionStringManager>();
|
||||
this.registry = new Mock<IRegistryManager>();
|
||||
this.logger = new Mock<ILogger>();
|
||||
|
||||
this.target = new Devices(
|
||||
this.config.Object,
|
||||
this.connectionStringManager.Object,
|
||||
this.registry.Object,
|
||||
this.logger.Object);
|
||||
|
||||
this.connectionStringManager
|
||||
.Setup(x => x.GetIotHubConnectionString())
|
||||
.Returns("HostName=iothub-AAAA.azure-devices.net;SharedAccessKeyName=AAAA;SharedAccessKey=AAAA");
|
||||
}
|
||||
|
||||
/**
|
||||
* Any exception while creating a device needs to be rethrown
|
||||
* so that the simulation will retry. Do not return null, otherwise
|
||||
* the device actors will assume a device object is ready to use
|
||||
* and get into an invalid state.
|
||||
*/
|
||||
[Fact, Trait(Constants.TYPE, Constants.UNIT_TEST)]
|
||||
public void ItThrowsWhenCreationTimesOut()
|
||||
{
|
||||
// Case 1: the code uses async, and the exception surfaces explicitly
|
||||
|
||||
// Arrange
|
||||
this.registry.Setup(x => x.AddDeviceAsync(It.IsAny<Device>())).Throws<TaskCanceledException>();
|
||||
|
||||
// Act+Assert
|
||||
Assert.ThrowsAsync<ExternalDependencyException>(
|
||||
() => this.target.CreateAsync("a-device-id")).Wait();
|
||||
|
||||
// Case 2: the code uses Wait(), and the exception is wrapped in AggregateException
|
||||
|
||||
// Arrange
|
||||
var e = new AggregateException(new TaskCanceledException());
|
||||
this.registry.Setup(x => x.AddDeviceAsync(It.IsAny<Device>())).Throws(e);
|
||||
|
||||
// Act+Assert
|
||||
Assert.ThrowsAsync<ExternalDependencyException>(
|
||||
() => this.target.CreateAsync("a-device-id")).Wait();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Diagnostics;
|
||||
using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Exceptions;
|
||||
using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.IotHub;
|
||||
using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Runtime;
|
||||
using Moq;
|
||||
using Services.Test.helpers;
|
||||
using Xunit;
|
||||
|
||||
namespace Services.Test
|
||||
{
|
||||
public class IotHubConnectionStringManagerTest
|
||||
{
|
||||
private readonly Mock<ILogger> logger;
|
||||
private readonly IServicesConfig config;
|
||||
private readonly IotHubConnectionStringManager target;
|
||||
|
||||
public IotHubConnectionStringManagerTest()
|
||||
{
|
||||
this.logger = new Mock<ILogger>();
|
||||
this.config = new ServicesConfig();
|
||||
this.target = new IotHubConnectionStringManager(this.config, this.logger.Object);
|
||||
}
|
||||
|
||||
[Fact, Trait(Constants.TYPE, Constants.UNIT_TEST)]
|
||||
public async Task ItThrowsOnInvalidConnStringFormat()
|
||||
{
|
||||
// Assert
|
||||
await Assert.ThrowsAsync<InvalidIotHubConnectionStringFormatException>(() => this.target.RedactAndStoreAsync("foobar"));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,10 +2,11 @@
|
|||
<PropertyGroup>
|
||||
<TargetFramework>netcoreapp2.0</TargetFramework>
|
||||
<LangVersion>7</LangVersion>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.5.0" />
|
||||
<PackageReference Include="Moq" Version="4.7.145" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.6.2" />
|
||||
<PackageReference Include="Moq" Version="4.8.2" />
|
||||
<PackageReference Include="xunit" Version="2.3.1" />
|
||||
<PackageReference Include="xunit.assert" Version="2.3.1" />
|
||||
<PackageReference Include="xunit.runner.console" Version="2.3.1" />
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Diagnostics;
|
||||
using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Models;
|
||||
using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Runtime;
|
||||
using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Simulation;
|
||||
using Moq;
|
||||
|
@ -21,6 +22,7 @@ namespace Services.Test.Simulation
|
|||
private readonly ITestOutputHelper log;
|
||||
private readonly Mock<IServicesConfig> config;
|
||||
private readonly Mock<ILogger> logger;
|
||||
private readonly Mock<ISmartDictionary> properties;
|
||||
private readonly JavascriptInterpreter target;
|
||||
|
||||
public JavascriptInterpreterTest(ITestOutputHelper log)
|
||||
|
@ -30,6 +32,7 @@ namespace Services.Test.Simulation
|
|||
this.config = new Mock<IServicesConfig>();
|
||||
this.config.SetupGet(x => x.DeviceModelsFolder).Returns("./data/devicemodels/");
|
||||
this.config.SetupGet(x => x.DeviceModelsScriptsFolder).Returns("./data/devicemodels/scripts/");
|
||||
this.properties = new Mock<ISmartDictionary>();
|
||||
|
||||
this.logger = new Mock<ILogger>();
|
||||
this.CaptureApplicationLogs(this.logger);
|
||||
|
@ -41,6 +44,8 @@ namespace Services.Test.Simulation
|
|||
public void ReturnedStateIsIntact()
|
||||
{
|
||||
// Arrange
|
||||
SmartDictionary deviceState = new SmartDictionary();
|
||||
|
||||
var filename = "chiller-01-state.js";
|
||||
var context = new Dictionary<string, object>
|
||||
{
|
||||
|
@ -54,25 +59,33 @@ namespace Services.Test.Simulation
|
|||
["temperature_unit"] = "device-123",
|
||||
["humidity"] = 70.2,
|
||||
["humidity_unit"] = "%",
|
||||
["lights_on"] = false
|
||||
["lights_on"] = false,
|
||||
["pressure"] = 150.0,
|
||||
["pressure_unit"] = "psig"
|
||||
};
|
||||
|
||||
deviceState.SetAll(state);
|
||||
|
||||
// Act
|
||||
Dictionary<string, object> result = this.target.Invoke(filename, context, state);
|
||||
this.target.Invoke(filename, context, deviceState, properties.Object);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(state.Count, result.Count);
|
||||
Assert.IsType<Double>(result["temperature"]);
|
||||
Assert.IsType<string>(result["temperature_unit"]);
|
||||
Assert.IsType<Double>(result["humidity"]);
|
||||
Assert.IsType<string>(result["humidity_unit"]);
|
||||
Assert.IsType<bool>(result["lights_on"]);
|
||||
Assert.Equal(state.Count, deviceState.GetAll().Count);
|
||||
Assert.IsType<Double>(deviceState.Get("temperature"));
|
||||
Assert.IsType<string>(deviceState.Get("temperature_unit"));
|
||||
Assert.IsType<Double>(deviceState.Get("humidity"));
|
||||
Assert.IsType<string>(deviceState.Get("humidity_unit"));
|
||||
Assert.IsType<bool>(deviceState.Get("lights_on"));
|
||||
Assert.IsType<Double>(deviceState.Get("pressure"));
|
||||
Assert.IsType<string>(deviceState.Get("pressure_unit"));
|
||||
}
|
||||
|
||||
[Fact, Trait(Constants.TYPE, Constants.UNIT_TEST)]
|
||||
public void TestJavascriptFiles()
|
||||
{
|
||||
// Arrange
|
||||
SmartDictionary deviceState = new SmartDictionary();
|
||||
|
||||
var files = new List<string>
|
||||
{
|
||||
"chiller-01-state.js",
|
||||
|
@ -96,8 +109,8 @@ namespace Services.Test.Simulation
|
|||
// Act - Assert (no exception should occur)
|
||||
foreach (var file in files)
|
||||
{
|
||||
var result = this.target.Invoke(file, context, null);
|
||||
Assert.NotNull(result);
|
||||
this.target.Invoke(file, context, deviceState, this.properties.Object);
|
||||
Assert.NotNull(deviceState.GetAll());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ using System.Collections.Generic;
|
|||
using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services;
|
||||
using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Diagnostics;
|
||||
using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Exceptions;
|
||||
using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.IotHub;
|
||||
using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Models;
|
||||
using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.StorageAdapter;
|
||||
using Moq;
|
||||
|
@ -26,7 +27,9 @@ namespace Services.Test
|
|||
|
||||
private readonly Mock<IDeviceModels> deviceModels;
|
||||
private readonly Mock<IStorageAdapterClient> storage;
|
||||
private readonly Mock<IDevices> devices;
|
||||
private readonly Mock<ILogger> logger;
|
||||
private readonly Mock<IIotHubConnectionStringManager> connStringManager;
|
||||
private readonly Simulations target;
|
||||
private readonly List<DeviceModel> models;
|
||||
|
||||
|
@ -37,7 +40,8 @@ namespace Services.Test
|
|||
this.deviceModels = new Mock<IDeviceModels>();
|
||||
this.storage = new Mock<IStorageAdapterClient>();
|
||||
this.logger = new Mock<ILogger>();
|
||||
|
||||
this.devices = new Mock<IDevices>();
|
||||
this.connStringManager = new Mock<IIotHubConnectionStringManager>();
|
||||
this.models = new List<DeviceModel>
|
||||
{
|
||||
new DeviceModel { Id = "01" },
|
||||
|
@ -46,7 +50,7 @@ namespace Services.Test
|
|||
new DeviceModel { Id = "AA" }
|
||||
};
|
||||
|
||||
this.target = new Simulations(this.deviceModels.Object, this.storage.Object, this.logger.Object);
|
||||
this.target = new Simulations(this.deviceModels.Object, this.storage.Object, this.connStringManager.Object, this.devices.Object, this.logger.Object);
|
||||
}
|
||||
|
||||
[Fact, Trait(Constants.TYPE, Constants.UNIT_TEST)]
|
||||
|
@ -179,14 +183,14 @@ namespace Services.Test
|
|||
{
|
||||
Id = SIMULATION_ID,
|
||||
Enabled = false,
|
||||
Etag = "oldETag"
|
||||
ETag = "oldETag"
|
||||
};
|
||||
this.target.UpsertAsync(simulation).Wait();
|
||||
|
||||
// Assert
|
||||
this.storage.Verify(
|
||||
x => x.UpdateAsync(STORAGE_COLLECTION, SIMULATION_ID, It.IsAny<string>(), "oldETag"));
|
||||
Assert.Equal("newETag", simulation.Etag);
|
||||
Assert.Equal("newETag", simulation.ETag);
|
||||
}
|
||||
|
||||
[Fact, Trait(Constants.TYPE, Constants.UNIT_TEST)]
|
||||
|
@ -216,6 +220,41 @@ namespace Services.Test
|
|||
Assert.ThrowsAsync<ResourceOutOfDateException>(() => this.target.UpsertAsync(s1Updated));
|
||||
}
|
||||
|
||||
[Fact, Trait(Constants.TYPE, Constants.UNIT_TEST)]
|
||||
public void ThereAreNoNullPropertiesInTheDeviceModel()
|
||||
{
|
||||
// Arrange
|
||||
this.ThereAreSomeDeviceModels();
|
||||
this.ThereAreNoSimulationsInTheStorage();
|
||||
|
||||
// Arrange the simulation data returned by the storage adapter
|
||||
var simulation = new SimulationModel
|
||||
{
|
||||
Id = SIMULATION_ID,
|
||||
ETag = "ETag0",
|
||||
Enabled = true,
|
||||
Version = 1
|
||||
};
|
||||
var updatedValue = new ValueApiModel
|
||||
{
|
||||
Key = SIMULATION_ID,
|
||||
Data = JsonConvert.SerializeObject(simulation),
|
||||
ETag = simulation.ETag
|
||||
};
|
||||
this.storage.Setup(x => x.UpdateAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>()))
|
||||
.ReturnsAsync(updatedValue);
|
||||
|
||||
// Act
|
||||
this.target.UpsertAsync(simulation).Wait();
|
||||
|
||||
// Assert
|
||||
this.storage.Verify(x => x.UpdateAsync(
|
||||
STORAGE_COLLECTION,
|
||||
SIMULATION_ID,
|
||||
It.Is<string>(s => !s.Contains("null")),
|
||||
"ETag0"));
|
||||
}
|
||||
|
||||
private void ThereAreSomeDeviceModels()
|
||||
{
|
||||
this.deviceModels.Setup(x => x.GetList()).Returns(this.models);
|
||||
|
@ -226,7 +265,7 @@ namespace Services.Test
|
|||
this.storage.Setup(x => x.GetAllAsync(STORAGE_COLLECTION)).ReturnsAsync(new ValueListApiModel());
|
||||
// In case the test inserts a record, return a valid storage object
|
||||
this.storage.Setup(x => x.UpdateAsync(STORAGE_COLLECTION, SIMULATION_ID, It.IsAny<string>(), "*"))
|
||||
.ReturnsAsync(new ValueApiModel { Key = SIMULATION_ID, Data = "{}", ETag = "someEtag" });
|
||||
.ReturnsAsync(new ValueApiModel { Key = SIMULATION_ID, Data = "{}", ETag = "someETag" });
|
||||
}
|
||||
|
||||
private void ThereIsAnEnabledSimulationInTheStorage()
|
||||
|
@ -236,7 +275,7 @@ namespace Services.Test
|
|||
Id = SIMULATION_ID,
|
||||
Created = DateTimeOffset.UtcNow.Subtract(TimeSpan.FromDays(10)),
|
||||
Modified = DateTimeOffset.UtcNow.Subtract(TimeSpan.FromDays(10)),
|
||||
Etag = "etag0",
|
||||
ETag = "ETag0",
|
||||
Enabled = true,
|
||||
Version = 1
|
||||
};
|
||||
|
@ -246,7 +285,7 @@ namespace Services.Test
|
|||
{
|
||||
Key = SIMULATION_ID,
|
||||
Data = JsonConvert.SerializeObject(simulation),
|
||||
ETag = simulation.Etag
|
||||
ETag = simulation.ETag
|
||||
};
|
||||
list.Items.Add(value);
|
||||
|
||||
|
|
|
@ -0,0 +1,264 @@
|
|||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Models;
|
||||
using Services.Test.helpers;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
using static Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Models.DeviceModel;
|
||||
|
||||
namespace Services.Test
|
||||
{
|
||||
public class SmartDictionaryTest
|
||||
{
|
||||
private ISmartDictionary target;
|
||||
|
||||
public SmartDictionaryTest(ITestOutputHelper log)
|
||||
{
|
||||
// Initialize device properties
|
||||
this.target = this.GetTestProperties();
|
||||
}
|
||||
|
||||
[Fact, Trait(Constants.TYPE, Constants.UNIT_TEST)]
|
||||
public void Should_Be_Empty_On_Get_All_When_No_Properties_Added()
|
||||
{
|
||||
// Arrange
|
||||
this.target = this.GetEmptyProperties();
|
||||
|
||||
// Act
|
||||
var props = this.target.GetAll();
|
||||
|
||||
// Assert
|
||||
Assert.Empty(props);
|
||||
}
|
||||
|
||||
[Fact, Trait(Constants.TYPE, Constants.UNIT_TEST)]
|
||||
public void Should_Return_All_Test_Properties_On_Get_All_When_Initialized_With_Device_Model()
|
||||
{
|
||||
// Arrange
|
||||
this.target = this.GetTestProperties();
|
||||
var expectedCount = this.GetTestChillerModel().Properties.Count;
|
||||
|
||||
// Act
|
||||
var props = this.target.GetAll();
|
||||
|
||||
// Assert
|
||||
Assert.NotEmpty(props);
|
||||
Assert.Equal(props.Count, expectedCount);
|
||||
Assert.Equal("TestChiller", props["Type"]);
|
||||
Assert.Equal("1.0", props["Firmware"]);
|
||||
Assert.Equal("TestCH101", props["Model"]);
|
||||
Assert.Equal("TestBuilding 2", props["Location"]);
|
||||
Assert.Equal(47.640792, props["Latitude"]);
|
||||
Assert.Equal(-122.126258, props["Longitude"]);
|
||||
}
|
||||
|
||||
[Fact, Trait(Constants.TYPE, Constants.UNIT_TEST)]
|
||||
public void Should_Return_Value_When_Calling_Get_And_Property_Exists()
|
||||
{
|
||||
// Arrange
|
||||
this.target = this.GetTestProperties();
|
||||
var expectedCount = this.GetTestChillerModel().Properties.Count;
|
||||
|
||||
// Act
|
||||
var props = this.target.GetAll();
|
||||
|
||||
// Assert
|
||||
Assert.NotEmpty(props);
|
||||
Assert.Equal(props.Count, expectedCount);
|
||||
Assert.Equal("TestChiller", props["Type"]);
|
||||
Assert.Equal("1.0", props["Firmware"]);
|
||||
Assert.Equal("TestCH101", props["Model"]);
|
||||
Assert.Equal("TestBuilding 2", props["Location"]);
|
||||
Assert.Equal(47.640792, props["Latitude"]);
|
||||
Assert.Equal(-122.126258, props["Longitude"]);
|
||||
}
|
||||
|
||||
[Fact, Trait(Constants.TYPE, Constants.UNIT_TEST)]
|
||||
public void Should_Throw_On_Get_When_Key_Does_Not_Exist()
|
||||
{
|
||||
// Arrange
|
||||
this.target = this.GetTestProperties();
|
||||
const string KEY = "KeyThatDoesNotExist";
|
||||
|
||||
// Act and Assert
|
||||
Assert.Throws<KeyNotFoundException>(() => this.target.Get(KEY));
|
||||
}
|
||||
|
||||
[Fact, Trait(Constants.TYPE, Constants.UNIT_TEST)]
|
||||
public void Should_Return_Copy_From_Get_All()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
// test values
|
||||
const string KEY1 = "key1";
|
||||
const string VALUE1 = "value1";
|
||||
const string NEW_VALUE = "newvalue";
|
||||
|
||||
var dictionary = new Dictionary<string,object>
|
||||
{
|
||||
{ KEY1, VALUE1 }
|
||||
};
|
||||
|
||||
this.target = new SmartDictionary(dictionary);
|
||||
|
||||
// Act
|
||||
|
||||
// modify copy
|
||||
var dictionaryCopy = this.target.GetAll();
|
||||
dictionaryCopy[KEY1] = NEW_VALUE;
|
||||
|
||||
// check original
|
||||
var result = this.target.Get(KEY1);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(result, VALUE1);
|
||||
}
|
||||
|
||||
[Fact, Trait(Constants.TYPE, Constants.UNIT_TEST)]
|
||||
public void Should_Add_Value_When_Set()
|
||||
{
|
||||
// Arrange
|
||||
this.target = this.GetEmptyProperties();
|
||||
const string KEY = "testSetKey";
|
||||
const string VALUE = "testSetValue";
|
||||
|
||||
// Act
|
||||
this.target.Set(KEY, VALUE);
|
||||
var result = this.target.Get(KEY);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(result, VALUE);
|
||||
}
|
||||
|
||||
[Fact, Trait(Constants.TYPE, Constants.UNIT_TEST)]
|
||||
public void Should_Set_Value_With_Key()
|
||||
{
|
||||
// Arrange
|
||||
this.target = this.GetEmptyProperties();
|
||||
const string KEY = "testSetKey";
|
||||
const string VALUE = "testSetValue";
|
||||
this.target.Set(KEY, VALUE);
|
||||
|
||||
// Act
|
||||
var result = this.target.Get(KEY);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(result, VALUE);
|
||||
}
|
||||
|
||||
[Fact, Trait(Constants.TYPE, Constants.UNIT_TEST)]
|
||||
public void Has_Should_Return_True_When_Property_Exists()
|
||||
{
|
||||
// Arrange
|
||||
this.target = this.GetEmptyProperties();
|
||||
const string KEY = "testHasKey";
|
||||
const string VALUE = "testHasValue";
|
||||
this.target.Set(KEY, VALUE);
|
||||
|
||||
// Act
|
||||
var result = this.target.Has(KEY);
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
[Fact, Trait(Constants.TYPE, Constants.UNIT_TEST)]
|
||||
public void Has_Should_Return_False_When_Property_Does_Not_Exist()
|
||||
{
|
||||
// Arrange
|
||||
this.target = this.GetEmptyProperties();
|
||||
const string KEY = "KeyThatDoesNotExist";
|
||||
|
||||
// Act
|
||||
var result = this.target.Has(KEY);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact, Trait(Constants.TYPE, Constants.UNIT_TEST)]
|
||||
public void Changed_Should_Return_True_When_New_Property_Added()
|
||||
{
|
||||
// Arrange
|
||||
this.target = this.GetEmptyProperties();
|
||||
Assert.False(this.target.Changed);
|
||||
|
||||
const string KEY = "testKey";
|
||||
const string VALUE = "testValue";
|
||||
|
||||
// Act
|
||||
this.target.Set(KEY, VALUE);
|
||||
|
||||
// Assert
|
||||
Assert.True(this.target.Changed);
|
||||
}
|
||||
|
||||
[Fact, Trait(Constants.TYPE, Constants.UNIT_TEST)]
|
||||
public void Changed_Should_Be_False_When_Reset()
|
||||
{
|
||||
// Arrange
|
||||
this.target = this.GetTestProperties();
|
||||
|
||||
const string KEY = "testKey";
|
||||
const string VALUE = "testValue";
|
||||
this.target.Set(KEY, VALUE);
|
||||
|
||||
// Act
|
||||
this.target.ResetChanged();
|
||||
|
||||
// Assert
|
||||
Assert.False(this.target.Changed);
|
||||
}
|
||||
|
||||
private SmartDictionary GetEmptyProperties()
|
||||
{
|
||||
return new SmartDictionary();
|
||||
}
|
||||
|
||||
private SmartDictionary GetTestProperties()
|
||||
{
|
||||
return new SmartDictionary(this.GetTestChillerModel().Properties);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the a test chiller model
|
||||
/// </summary>
|
||||
private DeviceModel GetTestChillerModel()
|
||||
{
|
||||
return new DeviceModel()
|
||||
{
|
||||
Id = "TestChiller01",
|
||||
Properties = new Dictionary<string, object>()
|
||||
{
|
||||
{ "TestPropKey", "TestPropValue" },
|
||||
{ "Type", "TestChiller" },
|
||||
{ "Firmware", "1.0" },
|
||||
{ "Model", "TestCH101" },
|
||||
{ "Location", "TestBuilding 2" },
|
||||
{ "Latitude", 47.640792 },
|
||||
{ "Longitude", -122.126258 }
|
||||
},
|
||||
Simulation = new StateSimulation()
|
||||
{
|
||||
InitialState = new Dictionary<string, object>()
|
||||
{
|
||||
{ "testKey", "testValue" },
|
||||
{ "online", true },
|
||||
{ "temperature", 75.0 },
|
||||
{ "temperature_unit", "F" },
|
||||
{ "humidity", 70.0 },
|
||||
{ "humidity_unit", "%" },
|
||||
{ "pressure", 150.0 },
|
||||
{ "pressure_unit", "psig" },
|
||||
{ "simulation_state", "normal_pressure" }
|
||||
},
|
||||
Interval = TimeSpan.Parse("00:00:10")
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
|
@ -20,6 +20,18 @@ namespace Services.Test.helpers
|
|||
}
|
||||
|
||||
public LogLevel LogLevel { get; }
|
||||
public bool DebugIsEnabled { get; }
|
||||
public bool InfoIsEnabled { get; }
|
||||
|
||||
public string FormatDate(long time)
|
||||
{
|
||||
return DateTimeOffset.FromUnixTimeMilliseconds(time).ToString("yyyy-MM-dd HH:mm:ss.fff");
|
||||
}
|
||||
|
||||
public void LogToFile(string filename, string text)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public void Debug(string message, Action context)
|
||||
{
|
||||
|
|
|
@ -1,13 +1,16 @@
|
|||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
/* CODE TEMPORARILY COMMENTED OUT
|
||||
|
||||
using System;
|
||||
|
||||
namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Concurrency
|
||||
{
|
||||
public static class Etags
|
||||
public static class ETags
|
||||
{
|
||||
// A simple string generator, until we have a real storage
|
||||
public static string NewEtag()
|
||||
// A simple string generator, until we have a real storage, used for
|
||||
// optimistic concurrency on data stored on files
|
||||
public static string NewETag()
|
||||
{
|
||||
var v1 = Guid.NewGuid().ToString().Replace("-", "");
|
||||
var v2 = DateTime.UtcNow.Ticks % 1000000;
|
||||
|
@ -15,3 +18,4 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Concurrency
|
|||
}
|
||||
}
|
||||
}
|
||||
*/
|
|
@ -0,0 +1,41 @@
|
|||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
|
||||
namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Concurrency
|
||||
{
|
||||
public class StateLoopSettings
|
||||
{
|
||||
public const int MIN_LOOP_DURATION = 999;
|
||||
}
|
||||
|
||||
public class TelemetryLoopSettings
|
||||
{
|
||||
public const int MIN_LOOP_DURATION = 500;
|
||||
}
|
||||
|
||||
public class ConnectionLoopSettings
|
||||
{
|
||||
public const int MIN_LOOP_DURATION = 600;
|
||||
|
||||
private readonly IRateLimitingConfig ratingConfig;
|
||||
|
||||
public double SchedulableFetches { get; set; }
|
||||
public double SchedulableRegistrations { get; set; }
|
||||
public double SchedulableTaggings { get; set; }
|
||||
|
||||
public ConnectionLoopSettings(IRateLimitingConfig ratingConfig)
|
||||
{
|
||||
this.ratingConfig = ratingConfig;
|
||||
this.NewLoop();
|
||||
}
|
||||
|
||||
public void NewLoop()
|
||||
{
|
||||
// Prioritize connections and registrations, so that devices connect as soon as possible
|
||||
this.SchedulableFetches = Math.Max(1, this.ratingConfig.RegistryOperationsPerMinute / 25);
|
||||
this.SchedulableRegistrations = Math.Max(1, this.ratingConfig.RegistryOperationsPerMinute / 10);
|
||||
this.SchedulableTaggings = Math.Max(1, this.ratingConfig.RegistryOperationsPerMinute / 25);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,31 +1,18 @@
|
|||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Diagnostics;
|
||||
using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Runtime;
|
||||
|
||||
namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Concurrency
|
||||
{
|
||||
public interface IRateLimiting
|
||||
{
|
||||
void SetCancellationToken(CancellationToken token);
|
||||
|
||||
Task<T> LimitConnectionsAsync<T>(Func<Task<T>> func);
|
||||
Task LimitConnectionsAsync(Func<Task> func);
|
||||
|
||||
Task<T> LimitRegistryOperationsAsync<T>(Func<Task<T>> func);
|
||||
Task LimitRegistryOperationsAsync(Func<Task> func);
|
||||
|
||||
Task<T> LimitTwinReadsAsync<T>(Func<Task<T>> func);
|
||||
Task LimitTwinReadsAsync(Func<Task> func);
|
||||
|
||||
Task<T> LimitTwinWritesAsync<T>(Func<Task<T>> func);
|
||||
Task LimitTwinWritesAsync(Func<Task> func);
|
||||
|
||||
Task<T> LimitMessagesAsync<T>(Func<Task<T>> func);
|
||||
Task LimitMessagesAsync(Func<Task> func);
|
||||
long GetPauseForNextConnection();
|
||||
long GetPauseForNextRegistryOperation();
|
||||
long GetPauseForNextTwinRead();
|
||||
long GetPauseForNextTwinWrite();
|
||||
long GetPauseForNextMessage();
|
||||
double GetThroughputForMessages();
|
||||
void ResetCounters();
|
||||
}
|
||||
|
||||
public class RateLimiting : IRateLimiting
|
||||
|
@ -37,13 +24,12 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Concurrency
|
|||
private readonly PerSecondCounter twinReads;
|
||||
private readonly PerSecondCounter twinWrites;
|
||||
private readonly PerSecondCounter messaging;
|
||||
private CancellationToken token;
|
||||
|
||||
// TODO: https://github.com/Azure/device-simulation-dotnet/issues/80
|
||||
//private readonly PerDayCounter messagingDaily;
|
||||
|
||||
public RateLimiting(
|
||||
IRateLimitingConfiguration config,
|
||||
IRateLimitingConfig config,
|
||||
ILogger log)
|
||||
{
|
||||
this.connections = new PerSecondCounter(
|
||||
|
@ -70,81 +56,47 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Concurrency
|
|||
// Parallel.For in the simulation runner.
|
||||
// https://github.com/Azure/device-simulation-dotnet/issues/79
|
||||
log.Info("Rate limiting started. This message should appear only once in the logs.", () => { });
|
||||
|
||||
this.token = CancellationToken.None;
|
||||
}
|
||||
|
||||
public void SetCancellationToken(CancellationToken token)
|
||||
public void ResetCounters()
|
||||
{
|
||||
this.token = token;
|
||||
this.connections.ResetCounter();
|
||||
this.registryOperations.ResetCounter();
|
||||
this.twinReads.ResetCounter();
|
||||
this.twinWrites.ResetCounter();
|
||||
this.messaging.ResetCounter();
|
||||
}
|
||||
|
||||
public async Task<T> LimitConnectionsAsync<T>(Func<Task<T>> func)
|
||||
public long GetPauseForNextConnection()
|
||||
{
|
||||
await this.connections.IncreaseAsync(this.token);
|
||||
return await func.Invoke();
|
||||
return this.connections.GetPause();
|
||||
}
|
||||
|
||||
public async Task LimitConnectionsAsync(Func<Task> func)
|
||||
public long GetPauseForNextRegistryOperation()
|
||||
{
|
||||
await this.connections.IncreaseAsync(this.token);
|
||||
await func.Invoke();
|
||||
return this.registryOperations.GetPause();
|
||||
}
|
||||
|
||||
public async Task<T> LimitRegistryOperationsAsync<T>(Func<Task<T>> func)
|
||||
public long GetPauseForNextTwinRead()
|
||||
{
|
||||
await this.registryOperations.IncreaseAsync(this.token);
|
||||
return await func.Invoke();
|
||||
return this.twinReads.GetPause();
|
||||
}
|
||||
|
||||
public async Task LimitRegistryOperationsAsync(Func<Task> func)
|
||||
public long GetPauseForNextTwinWrite()
|
||||
{
|
||||
await this.registryOperations.IncreaseAsync(this.token);
|
||||
await func.Invoke();
|
||||
return this.twinWrites.GetPause();
|
||||
}
|
||||
|
||||
public async Task<T> LimitTwinReadsAsync<T>(Func<Task<T>> func)
|
||||
public long GetPauseForNextMessage()
|
||||
{
|
||||
await this.twinReads.IncreaseAsync(this.token);
|
||||
return await func.Invoke();
|
||||
// TODO: consider daily quota
|
||||
// https://github.com/Azure/device-simulation-dotnet/issues/80
|
||||
return this.messaging.GetPause();
|
||||
}
|
||||
|
||||
public async Task LimitTwinReadsAsync(Func<Task> func)
|
||||
{
|
||||
await this.twinReads.IncreaseAsync(this.token);
|
||||
await func.Invoke();
|
||||
}
|
||||
|
||||
public async Task<T> LimitTwinWritesAsync<T>(Func<Task<T>> func)
|
||||
{
|
||||
await this.twinWrites.IncreaseAsync(this.token);
|
||||
return await func.Invoke();
|
||||
}
|
||||
|
||||
public async Task LimitTwinWritesAsync(Func<Task> func)
|
||||
{
|
||||
await this.twinWrites.IncreaseAsync(this.token);
|
||||
await func.Invoke();
|
||||
}
|
||||
|
||||
public async Task<T> LimitMessagesAsync<T>(Func<Task<T>> func)
|
||||
{
|
||||
await this.messaging.IncreaseAsync(this.token);
|
||||
|
||||
// TODO: uncomment when https://github.com/Azure/device-simulation-dotnet/issues/80 is done
|
||||
//await this.messagingDaily.IncreaseAsync();
|
||||
|
||||
return await func.Invoke();
|
||||
}
|
||||
|
||||
public async Task LimitMessagesAsync(Func<Task> func)
|
||||
{
|
||||
await this.messaging.IncreaseAsync(this.token);
|
||||
|
||||
// TODO: uncomment when https://github.com/Azure/device-simulation-dotnet/issues/80 is done
|
||||
//await this.messagingDaily.IncreaseAsync();
|
||||
|
||||
await func.Invoke();
|
||||
}
|
||||
/// <summary>
|
||||
/// Get message throughput (messages per second)
|
||||
/// </summary>
|
||||
public double GetThroughputForMessages() => this.messaging.GetThroughputForMessages();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,18 +1,18 @@
|
|||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Runtime
|
||||
namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Concurrency
|
||||
{
|
||||
public interface IRateLimitingConfiguration
|
||||
public interface IRateLimitingConfig
|
||||
{
|
||||
int RegistryOperationsPerMinute { get; set; }
|
||||
int TwinReadsPerSecond { get; set; }
|
||||
int TwinWritesPerSecond { get; set; }
|
||||
int ConnectionsPerSecond { get; set; }
|
||||
int DeviceMessagesPerSecond { get; set; }
|
||||
int DeviceMessagesPerDay { get; set; }
|
||||
int RegistryOperationsPerMinute { get; }
|
||||
int TwinReadsPerSecond { get; }
|
||||
int TwinWritesPerSecond { get; }
|
||||
int ConnectionsPerSecond { get; }
|
||||
int DeviceMessagesPerSecond { get; }
|
||||
int DeviceMessagesPerDay { get; }
|
||||
}
|
||||
|
||||
public class RateLimitingConfiguration : IRateLimitingConfiguration
|
||||
public class RateLimitingConfig : IRateLimitingConfig
|
||||
{
|
||||
public int RegistryOperationsPerMinute { get; set; }
|
||||
public int TwinReadsPerSecond { get; set; }
|
|
@ -33,7 +33,7 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Concurrency
|
|||
public PerDayCounter(int rate, string name, ILogger logger)
|
||||
: base(rate, 86400 * 1000, name, logger)
|
||||
{
|
||||
throw new NotSupportedException("Daily counters are not supported yet due to memory constraints.");
|
||||
throw new NotSupportedException("Daily counters are not supported yet, due to memory constraints.");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -83,7 +83,6 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Concurrency
|
|||
}
|
||||
|
||||
this.name = name;
|
||||
|
||||
this.eventsPerTimeUnit = rate;
|
||||
this.timeUnitLength = timeUnitLength;
|
||||
|
||||
|
@ -92,11 +91,9 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Concurrency
|
|||
this.log.Info("New counter", () => new { name, rate, timeUnitLength });
|
||||
}
|
||||
|
||||
// Increase the counter, taking a pause if the caller is going too fast.
|
||||
// Return a boolean indicating whether a pause was required.
|
||||
public async Task<bool> IncreaseAsync(CancellationToken token)
|
||||
public long GetPause()
|
||||
{
|
||||
long pause;
|
||||
long pauseMsecs;
|
||||
|
||||
this.LogThroughput();
|
||||
|
||||
|
@ -113,7 +110,7 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Concurrency
|
|||
if (this.timestamps.Count < this.eventsPerTimeUnit)
|
||||
{
|
||||
this.timestamps.Enqueue(now);
|
||||
return false;
|
||||
return 0;
|
||||
}
|
||||
|
||||
long when;
|
||||
|
@ -133,51 +130,89 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Concurrency
|
|||
when = oneUnitTimeAgo + (long) this.timeUnitLength;
|
||||
}
|
||||
|
||||
pause = when - now;
|
||||
pauseMsecs = when - now;
|
||||
|
||||
this.timestamps.Enqueue(when);
|
||||
|
||||
// Ignore short pauses
|
||||
if (pause < 1.01)
|
||||
if (pauseMsecs < 1.01)
|
||||
{
|
||||
return false;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// The caller is send too many events, if this happens you
|
||||
// should consider redesigning the simulation logic to run
|
||||
// slower, rather than relying purely on the counter
|
||||
if (pause > 60000)
|
||||
if (pauseMsecs > 60000)
|
||||
{
|
||||
this.log.Warn("Pausing for more than a minute",
|
||||
() => new { this.name, seconds = pause / 1000 });
|
||||
this.log.Info("Pausing for more than a minute",
|
||||
() => new { this.name, seconds = pauseMsecs / 1000 });
|
||||
}
|
||||
else if (pause > 5000)
|
||||
else if (pauseMsecs > 15000)
|
||||
{
|
||||
this.log.Warn("Pausing for several seconds",
|
||||
() => new { this.name, seconds = pause / 1000 });
|
||||
this.log.Info("Pausing for several seconds",
|
||||
() => new { this.name, seconds = pauseMsecs / 1000 });
|
||||
}
|
||||
else
|
||||
{
|
||||
this.log.Info("Pausing", () => new { this.name, millisecs = pause });
|
||||
this.log.Info("Pausing", () => new { this.name, millisecs = pauseMsecs });
|
||||
}
|
||||
|
||||
await Task.Delay((int) pause, token);
|
||||
return pauseMsecs;
|
||||
}
|
||||
|
||||
return true;
|
||||
// Increase the counter, taking a pause if the caller is going too fast.
|
||||
// Return a boolean indicating whether a pause was required.
|
||||
public async Task<bool> IncreaseAsync(CancellationToken token)
|
||||
{
|
||||
var pauseMsecs = this.GetPause();
|
||||
|
||||
if (pauseMsecs > 0)
|
||||
{
|
||||
await Task.Delay((int) pauseMsecs, token);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get messages throughput.
|
||||
/// </summary>
|
||||
/// <returns>Throughput: messages per second</returns>
|
||||
public double GetThroughputForMessages()
|
||||
{
|
||||
double speed = 0;
|
||||
lock (this.timestamps)
|
||||
{
|
||||
if (this.timestamps.Count > 1)
|
||||
{
|
||||
// Time range in milliseconds
|
||||
long time = this.timestamps.Last() - this.timestamps.First();
|
||||
|
||||
// Unit for speed is messages per second
|
||||
speed = (1000 * (double) this.timestamps.Count / time * 10) / 10;
|
||||
}
|
||||
}
|
||||
|
||||
return speed;
|
||||
}
|
||||
|
||||
public void ResetCounter()
|
||||
{
|
||||
lock (this.timestamps)
|
||||
{
|
||||
this.timestamps.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
private void LogThroughput()
|
||||
{
|
||||
if (this.log.LogLevel <= LogLevel.Debug && this.timestamps.Count > 1)
|
||||
if (this.log.LogLevel <= LogLevel.Debug)
|
||||
{
|
||||
double speed = 0;
|
||||
lock (this.timestamps)
|
||||
{
|
||||
long time = this.timestamps.Last() - this.timestamps.First();
|
||||
speed = (int) (1000 * (double) this.timestamps.Count / time * 10) / 10;
|
||||
}
|
||||
this.log.Info(this.name + "/second", () => new { speed });
|
||||
double speed = this.GetThroughputForMessages();
|
||||
this.log.Info(this.name, () => new { speed });
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
/* CODE TEMPORARILY COMMENTED OUT
|
||||
|
||||
using System;
|
||||
using System.Threading;
|
||||
using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Diagnostics;
|
||||
|
@ -82,3 +84,4 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Concurrency
|
|||
}
|
||||
}
|
||||
}
|
||||
*/
|
|
@ -1,14 +0,0 @@
|
|||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
|
||||
namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Concurrency
|
||||
{
|
||||
public class TimerNotInitializedException : Exception
|
||||
{
|
||||
public TimerNotInitializedException()
|
||||
: base("Timer object not initialized. Call 'Setup()' first.")
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,11 +2,11 @@
|
|||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Azure.Devices.Client;
|
||||
using Microsoft.Azure.Devices.Shared;
|
||||
using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Concurrency;
|
||||
using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Diagnostics;
|
||||
using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Models;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
@ -16,16 +16,12 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services
|
|||
public interface IDeviceClient
|
||||
{
|
||||
IoTHubProtocol Protocol { get; }
|
||||
|
||||
Task ConnectAsync();
|
||||
|
||||
Task DisconnectAsync();
|
||||
|
||||
Task SendMessageAsync(string message, DeviceModel.DeviceModelMessageSchema schema);
|
||||
|
||||
Task UpdateTwinAsync(Device device);
|
||||
|
||||
Task RegisterMethodsForDeviceAsync(IDictionary<string, Script> methods, Dictionary<string, object> deviceState);
|
||||
Task RegisterMethodsForDeviceAsync(IDictionary<string, Script> methods, ISmartDictionary deviceState, ISmartDictionary deviceProperties);
|
||||
Task RegisterDesiredPropertiesUpdateAsync(ISmartDictionary deviceProperties);
|
||||
Task UpdatePropertiesAsync(ISmartDictionary deviceProperties);
|
||||
}
|
||||
|
||||
public class DeviceClient : IDeviceClient
|
||||
|
@ -42,7 +38,7 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services
|
|||
private readonly IoTHubProtocol protocol;
|
||||
private readonly Azure.Devices.Client.DeviceClient client;
|
||||
private readonly IDeviceMethods deviceMethods;
|
||||
private readonly IRateLimiting rateLimiting;
|
||||
private readonly IDevicePropertiesRequest propertiesUpdateRequest;
|
||||
private readonly ILogger log;
|
||||
|
||||
private bool connected;
|
||||
|
@ -54,16 +50,15 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services
|
|||
IoTHubProtocol protocol,
|
||||
Azure.Devices.Client.DeviceClient client,
|
||||
IDeviceMethods deviceMethods,
|
||||
IRateLimiting rateLimiting,
|
||||
ILogger logger)
|
||||
{
|
||||
this.deviceId = deviceId;
|
||||
this.protocol = protocol;
|
||||
this.client = client;
|
||||
this.deviceMethods = deviceMethods;
|
||||
this.rateLimiting = rateLimiting;
|
||||
this.log = logger;
|
||||
this.connected = false;
|
||||
|
||||
this.propertiesUpdateRequest = new DeviceProperties(client, this.log);
|
||||
}
|
||||
|
||||
public async Task ConnectAsync()
|
||||
|
@ -72,7 +67,7 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services
|
|||
{
|
||||
// TODO: HTTP clients don't "connect", find out how HTTP connections are measured and throttled
|
||||
// https://github.com/Azure/device-simulation-dotnet/issues/85
|
||||
await this.rateLimiting.LimitConnectionsAsync(() => this.client.OpenAsync());
|
||||
await this.client.OpenAsync();
|
||||
this.connected = true;
|
||||
}
|
||||
}
|
||||
|
@ -89,12 +84,21 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services
|
|||
|
||||
public async Task RegisterMethodsForDeviceAsync(
|
||||
IDictionary<string, Script> methods,
|
||||
Dictionary<string, object> deviceState)
|
||||
ISmartDictionary deviceState,
|
||||
ISmartDictionary deviceProperties)
|
||||
{
|
||||
this.log.Debug("Attempting to register device methods",
|
||||
() => new { this.deviceId });
|
||||
|
||||
await this.deviceMethods.RegisterMethodsAsync(this.deviceId, methods, deviceState);
|
||||
await this.deviceMethods.RegisterMethodsAsync(this.deviceId, methods, deviceState, deviceProperties);
|
||||
}
|
||||
|
||||
public async Task RegisterDesiredPropertiesUpdateAsync(ISmartDictionary deviceProperties)
|
||||
{
|
||||
this.log.Debug("Attempting to register desired property notifications for device",
|
||||
() => new { this.deviceId });
|
||||
|
||||
await this.propertiesUpdateRequest.RegisterChangeUpdateAsync(this.deviceId, deviceProperties);
|
||||
}
|
||||
|
||||
public async Task SendMessageAsync(string message, DeviceModel.DeviceModelMessageSchema schema)
|
||||
|
@ -104,54 +108,99 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services
|
|||
eventMessage.Properties.Add(MESSAGE_SCHEMA_PROPERTY, schema.Name);
|
||||
eventMessage.Properties.Add(CONTENT_PROPERTY, "JSON");
|
||||
|
||||
await this.SendRawMessageAsync(eventMessage);
|
||||
eventMessage.ContentType = "application/json";
|
||||
eventMessage.ContentEncoding = "utf-8";
|
||||
eventMessage.MessageSchema = schema.Name;
|
||||
eventMessage.CreationTimeUtc = DateTime.UtcNow;
|
||||
|
||||
this.log.Debug("SendMessageAsync for device", () => new
|
||||
{
|
||||
this.deviceId
|
||||
});
|
||||
this.log.Debug("Sending message from device",
|
||||
() => new { this.deviceId, Schema = schema.Name });
|
||||
|
||||
await this.SendRawMessageAsync(eventMessage);
|
||||
}
|
||||
|
||||
public async Task UpdateTwinAsync(Device device)
|
||||
/// <summary>
|
||||
/// Updates the reported properties in the device twin on the IoT Hub
|
||||
/// </summary>
|
||||
public async Task UpdatePropertiesAsync(ISmartDictionary properties)
|
||||
{
|
||||
if (!this.connected) await this.ConnectAsync();
|
||||
|
||||
var azureTwin = await this.rateLimiting.LimitTwinReadsAsync(
|
||||
() => this.client.GetTwinAsync());
|
||||
|
||||
// Remove properties
|
||||
var props = azureTwin.Properties.Reported.GetEnumerator();
|
||||
while (props.MoveNext())
|
||||
try
|
||||
{
|
||||
var current = (KeyValuePair<string, object>) props.Current;
|
||||
var reportedProperties = SmartDictionaryToTwinCollection(properties);
|
||||
|
||||
if (!device.Twin.ReportedProperties.ContainsKey(current.Key))
|
||||
await this.client.UpdateReportedPropertiesAsync(reportedProperties);
|
||||
|
||||
this.log.Debug("Update reported properties for device", () => new
|
||||
{
|
||||
this.log.Debug("Removing key", () => new { current.Key });
|
||||
azureTwin.Properties.Reported[current.Key] = null;
|
||||
}
|
||||
this.deviceId,
|
||||
ReportedProperties = reportedProperties
|
||||
});
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
this.log.Error("Update reported properties failed",
|
||||
() => new
|
||||
{
|
||||
Protocol = this.protocol.ToString(),
|
||||
ExceptionMessage = e.Message,
|
||||
Exception = e.GetType().FullName,
|
||||
e.InnerException
|
||||
});
|
||||
}
|
||||
|
||||
// Write properties
|
||||
var reportedProperties = DictionaryToTwinCollection(device.Twin.ReportedProperties);
|
||||
await this.rateLimiting.LimitTwinWritesAsync(
|
||||
() => this.client.UpdateReportedPropertiesAsync(reportedProperties));
|
||||
}
|
||||
|
||||
private async Task SendRawMessageAsync(Message message)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!this.connected) await this.ConnectAsync();
|
||||
|
||||
await this.rateLimiting.LimitMessagesAsync(
|
||||
() => this.client.SendEventAsync(message));
|
||||
await this.client.SendEventAsync(message);
|
||||
|
||||
this.log.Debug("SendRawMessageAsync for device", () => new
|
||||
{
|
||||
this.deviceId
|
||||
});
|
||||
}
|
||||
catch (TimeoutException e)
|
||||
{
|
||||
this.log.Error("Message delivery timed out",
|
||||
() => new
|
||||
{
|
||||
Protocol = this.protocol.ToString(),
|
||||
ExceptionMessage = e.Message,
|
||||
Exception = e.GetType().FullName,
|
||||
e.InnerException
|
||||
});
|
||||
|
||||
throw new TelemetrySendTimeoutException("Message delivery timed out with " + e.Message, e);
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
this.log.Error("Message delivery IOExcepotion",
|
||||
() => new
|
||||
{
|
||||
Protocol = this.protocol.ToString(),
|
||||
ExceptionMessage = e.Message,
|
||||
Exception = e.GetType().FullName,
|
||||
e.InnerException
|
||||
});
|
||||
|
||||
throw new TelemetrySendIOException("Message delivery I/O failed with " + e.Message, e);
|
||||
}
|
||||
catch (AggregateException aggEx) when (aggEx.InnerException != null)
|
||||
{
|
||||
var e = aggEx.InnerException;
|
||||
|
||||
this.log.Error("Message delivery failed",
|
||||
() => new
|
||||
{
|
||||
Protocol = this.protocol.ToString(),
|
||||
ExceptionMessage = e.Message,
|
||||
Exception = e.GetType().FullName,
|
||||
e.InnerException
|
||||
});
|
||||
|
||||
throw new TelemetrySendException("Message delivery failed with " + e.Message, e);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
this.log.Error("Message delivery failed",
|
||||
|
@ -162,20 +211,25 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services
|
|||
Exception = e.GetType().FullName,
|
||||
e.InnerException
|
||||
});
|
||||
|
||||
throw new TelemetrySendException("Message delivery failed with " + e.Message, e);
|
||||
}
|
||||
}
|
||||
|
||||
private static TwinCollection DictionaryToTwinCollection(Dictionary<string, JToken> x)
|
||||
private static TwinCollection SmartDictionaryToTwinCollection(ISmartDictionary dictionary)
|
||||
{
|
||||
var result = new TwinCollection();
|
||||
|
||||
if (x != null)
|
||||
if (dictionary != null)
|
||||
{
|
||||
foreach (KeyValuePair<string, JToken> item in x)
|
||||
var items = dictionary.GetAll();
|
||||
|
||||
foreach (KeyValuePair<string, object> item in items)
|
||||
{
|
||||
try
|
||||
{
|
||||
result[item.Key] = item.Value;
|
||||
// Use JToken for serialization
|
||||
result[item.Key] = JToken.FromObject(item.Value);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
|
|
|
@ -17,7 +17,8 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services
|
|||
Task RegisterMethodsAsync(
|
||||
string deviceId,
|
||||
IDictionary<string, Script> methods,
|
||||
Dictionary<string, object> deviceState);
|
||||
ISmartDictionary deviceState,
|
||||
ISmartDictionary deviceProperties);
|
||||
}
|
||||
|
||||
public class DeviceMethods : IDeviceMethods
|
||||
|
@ -26,8 +27,10 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services
|
|||
private readonly ILogger log;
|
||||
private readonly IScriptInterpreter scriptInterpreter;
|
||||
private IDictionary<string, Script> cloudToDeviceMethods;
|
||||
private Dictionary<string, object> deviceState;
|
||||
private ISmartDictionary deviceState;
|
||||
private ISmartDictionary deviceProperties;
|
||||
private string deviceId;
|
||||
private bool isRegistered;
|
||||
|
||||
public DeviceMethods(
|
||||
Azure.Devices.Client.DeviceClient client,
|
||||
|
@ -38,14 +41,16 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services
|
|||
this.log = logger;
|
||||
this.scriptInterpreter = scriptInterpreter;
|
||||
this.deviceId = string.Empty;
|
||||
this.isRegistered = false;
|
||||
}
|
||||
|
||||
public async Task RegisterMethodsAsync(
|
||||
string deviceId,
|
||||
IDictionary<string, Script> methods,
|
||||
Dictionary<string, object> deviceState)
|
||||
ISmartDictionary deviceState,
|
||||
ISmartDictionary deviceProperties)
|
||||
{
|
||||
if (this.deviceId != string.Empty)
|
||||
if (this.isRegistered)
|
||||
{
|
||||
this.log.Error("Application error, each device must have a separate instance", () => { });
|
||||
throw new Exception("Application error, each device must have a separate instance of " + this.GetType().FullName);
|
||||
|
@ -54,6 +59,7 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services
|
|||
this.deviceId = deviceId;
|
||||
this.cloudToDeviceMethods = methods;
|
||||
this.deviceState = deviceState;
|
||||
this.deviceProperties = deviceProperties;
|
||||
|
||||
this.log.Debug("Setting up methods for device.", () => new
|
||||
{
|
||||
|
@ -74,6 +80,8 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services
|
|||
methodName = item.Key
|
||||
});
|
||||
}
|
||||
|
||||
this.isRegistered = true;
|
||||
}
|
||||
|
||||
public Task<MethodResponse> ExecuteMethodAsync(MethodRequest methodRequest, object userContext)
|
||||
|
@ -134,9 +142,10 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services
|
|||
this.scriptInterpreter.Invoke(
|
||||
this.cloudToDeviceMethods[methodRequest.Name],
|
||||
scriptContext,
|
||||
this.deviceState);
|
||||
this.deviceState,
|
||||
this.deviceProperties);
|
||||
|
||||
this.log.Debug("Executed method for device", () => new { this.deviceId, methodRequest.Name });
|
||||
this.log.Debug("Executed method for device", () => new { this.deviceId, methodRequest.Name, this.deviceState, this.deviceProperties });
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
|
@ -158,4 +167,4 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services
|
|||
scriptContext.Add(item.Key, item.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -22,6 +22,9 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services
|
|||
|
||||
public class DeviceModels : IDeviceModels
|
||||
{
|
||||
// ID used for custom device models, where the list of sensors is provided by the user
|
||||
public const string CUSTOM_DEVICE_MODEL_ID = "custom";
|
||||
|
||||
private const string EXT = ".json";
|
||||
|
||||
private readonly IServicesConfig config;
|
||||
|
|
|
@ -0,0 +1,248 @@
|
|||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Diagnostics;
|
||||
using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Models;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services
|
||||
{
|
||||
public interface IDeviceModelsGeneration
|
||||
{
|
||||
DeviceModel Generate(DeviceModel source, Models.Simulation.DeviceModelOverride overrideInfo);
|
||||
}
|
||||
|
||||
public class DeviceModelsGeneration : IDeviceModelsGeneration
|
||||
{
|
||||
private readonly ILogger log;
|
||||
|
||||
public DeviceModelsGeneration(ILogger logger)
|
||||
{
|
||||
this.log = logger;
|
||||
}
|
||||
|
||||
// Change the device model simulation details, using the override information
|
||||
public DeviceModel Generate(DeviceModel source, Models.Simulation.DeviceModelOverride overrideInfo)
|
||||
{
|
||||
// Generate a clone, so that the original instance remains untouched
|
||||
var result = CloneObject(source);
|
||||
|
||||
if (overrideInfo == null) return result;
|
||||
|
||||
this.UpdateDeviceModelSimulationScriptsCount(overrideInfo.Simulation?.Scripts, result);
|
||||
this.UpdateDeviceModelTelemetryCount(overrideInfo.Telemetry, result);
|
||||
|
||||
this.SetSimulationInitialState(overrideInfo.Simulation?.InitialState, result);
|
||||
this.SetSimulationInterval(overrideInfo.Simulation?.Interval, result);
|
||||
this.SetSimulationScripts(overrideInfo.Simulation?.Scripts, result);
|
||||
this.SetTelemetry(overrideInfo.Telemetry, result);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private void SetSimulationInitialState(Dictionary<string, object> state, DeviceModel result)
|
||||
{
|
||||
if (state == null) return;
|
||||
|
||||
this.log.Info("Overriding initial state of the device", () => new
|
||||
{
|
||||
Original = result.Simulation.InitialState,
|
||||
NewValue = state
|
||||
});
|
||||
|
||||
result.Simulation.InitialState = state;
|
||||
}
|
||||
|
||||
// Redefine the interval at which the device state is generated
|
||||
private void SetSimulationInterval(TimeSpan? interval, DeviceModel result)
|
||||
{
|
||||
if (interval == null || result.Simulation.Interval.ToString("c") == interval.Value.ToString("c"))
|
||||
return;
|
||||
|
||||
this.log.Info("Overriding device state simulation frequency",
|
||||
() => new
|
||||
{
|
||||
Original = result.Simulation.Interval.ToString("c"),
|
||||
NewValue = interval.Value.ToString("c")
|
||||
});
|
||||
|
||||
result.Simulation.Interval = interval.Value;
|
||||
}
|
||||
|
||||
// Reduce or Increase the number of scripts, if required by the override information
|
||||
private void UpdateDeviceModelSimulationScriptsCount(
|
||||
IList<Models.Simulation.DeviceModelSimulationScriptOverride> scripts, DeviceModel result)
|
||||
{
|
||||
if (scripts == null || scripts.Count == 0) return;
|
||||
|
||||
var newCount = scripts.Count;
|
||||
var originalCount = result.Simulation.Scripts.Count;
|
||||
|
||||
if (originalCount < newCount)
|
||||
{
|
||||
this.log.Info("The list of scripts is longer than the original model, " +
|
||||
"the extra scripts will be added to the model",
|
||||
() => new { originalCount, newCount });
|
||||
|
||||
for (int i = 0; i < newCount - originalCount; i++)
|
||||
{
|
||||
result.Simulation.Scripts.Add(new Script());
|
||||
}
|
||||
}
|
||||
|
||||
if (originalCount > newCount)
|
||||
{
|
||||
this.log.Warn("The list of scripts is shorter than the original model, " +
|
||||
"the scripts in excess will be removed from the model",
|
||||
() => new { originalCount, newCount });
|
||||
|
||||
result.Simulation.Scripts = result.Simulation.Scripts.Take(newCount).ToList();
|
||||
}
|
||||
}
|
||||
|
||||
// Reduce or Increase the number of telemetry messages, if required by the override information
|
||||
private void UpdateDeviceModelTelemetryCount(
|
||||
IList<Models.Simulation.DeviceModelTelemetryOverride> telemetry, DeviceModel result)
|
||||
{
|
||||
if (telemetry == null
|
||||
|| telemetry.Count == 0
|
||||
|| telemetry.Count == result.Telemetry.Count) return;
|
||||
|
||||
var newCount = telemetry.Count;
|
||||
var originalCount = result.Telemetry.Count;
|
||||
|
||||
this.log.Info("The lenght of the telemetry list is different from the original model, adding/removing the extra telemetry",
|
||||
() => new { originalCount, newCount });
|
||||
|
||||
if (originalCount < newCount)
|
||||
{
|
||||
// Add the missing elements (empty for now)
|
||||
for (int i = 0; i < newCount - originalCount; i++)
|
||||
{
|
||||
result.Telemetry.Add(new DeviceModel.DeviceModelMessage());
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Remove what's not used
|
||||
result.Telemetry = result.Telemetry.Take(newCount).ToList();
|
||||
}
|
||||
}
|
||||
|
||||
// Redefine the scripts used to generate the device state
|
||||
private void SetSimulationScripts(
|
||||
IList<Models.Simulation.DeviceModelSimulationScriptOverride> scripts, DeviceModel result)
|
||||
{
|
||||
if (scripts == null || scripts.Count == 0) return;
|
||||
|
||||
this.log.Info("Overriding device state simulation scripts",
|
||||
() => new
|
||||
{
|
||||
Original = result.Simulation.Scripts,
|
||||
NewValue = scripts
|
||||
});
|
||||
|
||||
for (var i = 0; i < scripts.Count; i++)
|
||||
{
|
||||
var script = scripts[i];
|
||||
result.Simulation.Scripts[i].Params = script.Params;
|
||||
|
||||
if (!string.IsNullOrEmpty(script.Type))
|
||||
{
|
||||
result.Simulation.Scripts[i].Type = script.Type;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(script.Path))
|
||||
{
|
||||
result.Simulation.Scripts[i].Path = script.Path;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Override the telemetry frequency and content with the information provided
|
||||
private void SetTelemetry(
|
||||
IList<Models.Simulation.DeviceModelTelemetryOverride> telemetry, DeviceModel result)
|
||||
{
|
||||
if (telemetry == null || telemetry.Count == 0) return;
|
||||
|
||||
for (var i = 0; i < telemetry.Count; i++)
|
||||
{
|
||||
var t = telemetry[i];
|
||||
|
||||
if (t.Interval != null
|
||||
&& t.Interval.Value.ToString("c") != result.Telemetry[i].Interval.ToString("c"))
|
||||
{
|
||||
this.log.Info("Changing telemetry frequency",
|
||||
() => new
|
||||
{
|
||||
originalFrequency = result.Telemetry[i].Interval.ToString("c"),
|
||||
newFrequency = t.Interval.Value.ToString("c")
|
||||
});
|
||||
|
||||
result.Telemetry[i].Interval = t.Interval.Value;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(t.MessageTemplate)
|
||||
&& t.MessageTemplate != result.Telemetry[i].MessageTemplate)
|
||||
{
|
||||
this.log.Info("Changing telemetry message template",
|
||||
() => new
|
||||
{
|
||||
originalTemplate = result.Telemetry[i].MessageTemplate,
|
||||
newTemplate = t.MessageTemplate
|
||||
});
|
||||
|
||||
result.Telemetry[i].MessageTemplate = t.MessageTemplate;
|
||||
}
|
||||
|
||||
if (t.MessageSchema != null)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(t.MessageSchema.Name)
|
||||
&& t.MessageSchema.Name != result.Telemetry[i].MessageSchema.Name)
|
||||
{
|
||||
this.log.Info("Changing telemetry message schema name",
|
||||
() => new
|
||||
{
|
||||
originalName = result.Telemetry[i].MessageSchema.Name,
|
||||
newName = t.MessageSchema.Name
|
||||
});
|
||||
|
||||
result.Telemetry[i].MessageSchema.Name = t.MessageSchema.Name;
|
||||
}
|
||||
|
||||
if (t.MessageSchema.Format != null
|
||||
&& t.MessageSchema.Format != result.Telemetry[i].MessageSchema.Format)
|
||||
{
|
||||
this.log.Info("Changing telemetry message schema format",
|
||||
() => new
|
||||
{
|
||||
originalFormat = result.Telemetry[i].MessageSchema.Format,
|
||||
newFormat = t.MessageSchema.Format
|
||||
});
|
||||
|
||||
result.Telemetry[i].MessageSchema.Format = t.MessageSchema.Format.Value;
|
||||
}
|
||||
|
||||
if (t.MessageSchema.Fields != null)
|
||||
{
|
||||
this.log.Info("Changing telemetry message schema fields",
|
||||
() => new
|
||||
{
|
||||
originalFields = result.Telemetry[i].MessageSchema.Fields,
|
||||
newFields = t.MessageSchema.Fields
|
||||
});
|
||||
result.Telemetry[i].MessageSchema.Fields = t.MessageSchema.Fields;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Copy an object by value
|
||||
private static T CloneObject<T>(T source)
|
||||
{
|
||||
return JsonConvert.DeserializeObject<T>(JsonConvert.SerializeObject(source));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,91 @@
|
|||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Azure.Devices.Shared;
|
||||
using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Diagnostics;
|
||||
using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Models;
|
||||
|
||||
namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services
|
||||
{
|
||||
public interface IDevicePropertiesRequest
|
||||
{
|
||||
Task RegisterChangeUpdateAsync(
|
||||
string deviceId,
|
||||
ISmartDictionary deviceProperties);
|
||||
}
|
||||
|
||||
public class DeviceProperties : IDevicePropertiesRequest
|
||||
{
|
||||
private readonly Azure.Devices.Client.DeviceClient client;
|
||||
private readonly ILogger log;
|
||||
private string deviceId;
|
||||
private ISmartDictionary deviceProperties;
|
||||
private bool isRegistered;
|
||||
|
||||
public DeviceProperties(Azure.Devices.Client.DeviceClient client, ILogger logger)
|
||||
{
|
||||
this.client = client;
|
||||
this.log = logger;
|
||||
this.deviceId = string.Empty;
|
||||
this.isRegistered = false;
|
||||
}
|
||||
|
||||
public async Task RegisterChangeUpdateAsync(string deviceId, ISmartDictionary deviceProperties)
|
||||
{
|
||||
if (isRegistered)
|
||||
{
|
||||
this.log.Error("Application error, each device must have a separate instance", () => { });
|
||||
throw new Exception("Application error, each device must have a separate instance of " + this.GetType().FullName);
|
||||
}
|
||||
|
||||
this.deviceId = deviceId;
|
||||
this.deviceProperties = deviceProperties;
|
||||
|
||||
this.log.Debug("Setting up callback for desired properties updates.", () => new { this.deviceId });
|
||||
|
||||
// Set callback that IoT Hub calls whenever the client receives a desired properties state update.
|
||||
await this.client.SetDesiredPropertyUpdateCallbackAsync(OnChangeCallback, null);
|
||||
|
||||
this.log.Debug("Callback for desired properties updates setup successfully", () => new { this.deviceId });
|
||||
|
||||
this.isRegistered = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// When a desired property change is requested, update the internal device state properties
|
||||
/// which will be reported to the hub. If there is a new desired property that does not exist in
|
||||
/// the reported properties, it will be added.
|
||||
/// </summary>
|
||||
private Task OnChangeCallback(TwinCollection desiredProperties, object userContext)
|
||||
{
|
||||
this.log.Info("Desired property update requested", () => new { this.deviceId, desiredProperties });
|
||||
|
||||
// This is where custom code for handling specific desired property changes could be added.
|
||||
// For the purposes of the simulation service, we have chosen to write the desired properties
|
||||
// directly to the reported properties.
|
||||
try
|
||||
{
|
||||
foreach (KeyValuePair<string, object> item in desiredProperties)
|
||||
{
|
||||
// Only update if key doesn't exist or value has changed
|
||||
if (!this.deviceProperties.Has(item.Key) ||
|
||||
(item.Value.ToString() != this.deviceProperties.Get(item.Key).ToString()))
|
||||
{
|
||||
// Update existing property or create new property if key doesn't exist.
|
||||
this.deviceProperties.Set(item.Key, item.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
this.log.Error("Error updating device properties to desired values", () => new { e, this.deviceId, desiredProperties });
|
||||
}
|
||||
|
||||
this.log.Debug("Device properties updated to desired values", () => new { this.deviceId, desiredProperties });
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,13 +1,16 @@
|
|||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Azure.Devices;
|
||||
using Microsoft.Azure.Devices.Common.Exceptions;
|
||||
using Microsoft.Azure.Devices.Shared;
|
||||
using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Concurrency;
|
||||
using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Diagnostics;
|
||||
using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Exceptions;
|
||||
using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.IotHub;
|
||||
using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Models;
|
||||
using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Runtime;
|
||||
using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Simulation;
|
||||
|
@ -18,57 +21,110 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services
|
|||
{
|
||||
public interface IDevices
|
||||
{
|
||||
Task<Tuple<bool, string>> PingRegistryAsync();
|
||||
/// <summary>
|
||||
/// Set the current IoT Hub using either the user provided one or the configuration settings
|
||||
/// </summary>
|
||||
void SetCurrentIotHub();
|
||||
|
||||
/// <summary>
|
||||
/// Get a client for the device
|
||||
/// </summary>
|
||||
IDeviceClient GetClient(Device device, IoTHubProtocol protocol, IScriptInterpreter scriptInterpreter);
|
||||
Task<Device> GetOrCreateAsync(string deviceId, bool loadTwin, CancellationToken cancellationToken);
|
||||
Task<Device> GetAsync(string deviceId, bool loadTwin, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Get the device from the registry
|
||||
/// </summary>
|
||||
Task<Device> GetAsync(string deviceId);
|
||||
|
||||
/// <summary>
|
||||
/// Register the new device
|
||||
/// </summary>
|
||||
Task<Device> CreateAsync(string deviceId);
|
||||
|
||||
/// <summary>
|
||||
/// Add a tag to the device, to say it is a simulated device
|
||||
/// </summary>
|
||||
Task AddTagAsync(string deviceId);
|
||||
|
||||
/// <summary>
|
||||
/// Create a list of devices
|
||||
/// </summary>
|
||||
Task CreateListAsync(IEnumerable<string> deviceIds);
|
||||
|
||||
/// <summary>
|
||||
/// Delete a list of devices
|
||||
/// </summary>
|
||||
Task DeleteListAsync(IEnumerable<string> deviceIds);
|
||||
|
||||
/// <summary>
|
||||
/// Generate a device Id
|
||||
/// </summary>
|
||||
string GenerateId(string deviceModelId, int position);
|
||||
}
|
||||
|
||||
public class Devices : IDevices
|
||||
{
|
||||
// Whether to discard the twin created by the service when a device is created
|
||||
// When discarding the twin, we save one Twin Read operation (i.e. don't need to fetch the ETag)
|
||||
// TODO: when not discarding the twin, use the right ETag and manage conflicts
|
||||
// https://github.com/Azure/device-simulation-dotnet/issues/83
|
||||
private const bool DISCARD_TWIN_ON_CREATION = true;
|
||||
// Simulated devices are marked with a tag "IsSimulated = Y"
|
||||
public const string SIMULATED_TAG_KEY = "IsSimulated";
|
||||
public const string SIMULATED_TAG_VALUE = "Y";
|
||||
|
||||
// The registry might be in an inconsistent state after several requests, this limit
|
||||
// is used to recreate the registry manager instance every once in a while, while starting
|
||||
// the simulation. When the simulation is running the registry is not used anymore.
|
||||
private const uint REGISTRY_LIMIT_REQUESTS = 1000;
|
||||
|
||||
// When working with batches, this is the max size that the batch insert and delete APIs allow
|
||||
private const int REGISTRY_MAX_BATCH_SIZE = 100;
|
||||
|
||||
// When sending telemetry or other operations, wait only for 10 seconds. This setting sets how
|
||||
// throttling affects the application. The default SDK value is 4 minutes, which causes high
|
||||
// CPU usage.
|
||||
private const int SDK_CLIENT_TIMEOUT = 10000;
|
||||
|
||||
// ID prefix of the simulated devices, used with Azure IoT Hub
|
||||
private const string DEVICE_ID_PREFIX = "Simulated.";
|
||||
|
||||
private readonly IIotHubConnectionStringManager connectionStringManager;
|
||||
private readonly ILogger log;
|
||||
private readonly IRateLimiting rateLimiting;
|
||||
private readonly RegistryManager registry;
|
||||
private readonly string ioTHubHostName;
|
||||
|
||||
private readonly bool twinReadsWritesEnabled;
|
||||
private string ioTHubHostName;
|
||||
private IRegistryManager registry;
|
||||
private int registryCount;
|
||||
private bool setupDone;
|
||||
|
||||
public Devices(
|
||||
IRateLimiting rateLimiting,
|
||||
IServicesConfig config,
|
||||
IIotHubConnectionStringManager connStringManager,
|
||||
IRegistryManager registryManager,
|
||||
ILogger logger)
|
||||
{
|
||||
this.rateLimiting = rateLimiting;
|
||||
this.connectionStringManager = connStringManager;
|
||||
this.registry = registryManager;
|
||||
this.log = logger;
|
||||
this.registry = RegistryManager.CreateFromConnectionString(config.IoTHubConnString);
|
||||
this.ioTHubHostName = IotHubConnectionStringBuilder.Create(config.IoTHubConnString).HostName;
|
||||
this.log.Debug("Devices service instantiated", () => new { this.ioTHubHostName });
|
||||
this.twinReadsWritesEnabled = config.TwinReadWriteEnabled;
|
||||
this.registryCount = -1;
|
||||
this.setupDone = false;
|
||||
}
|
||||
|
||||
public async Task<Tuple<bool, string>> PingRegistryAsync()
|
||||
/// <summary>
|
||||
/// Get IoTHub connection string from either the user provided value or the configuration
|
||||
/// </summary>
|
||||
public void SetCurrentIotHub()
|
||||
{
|
||||
try
|
||||
{
|
||||
await this.rateLimiting.LimitRegistryOperationsAsync(
|
||||
() => this.registry.GetDeviceAsync("healthcheck"));
|
||||
return new Tuple<bool, string>(true, "OK");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
this.log.Error("Device registry test failed", () => new { e });
|
||||
return new Tuple<bool, string>(false, e.Message);
|
||||
}
|
||||
string connString = this.connectionStringManager.GetIotHubConnectionString();
|
||||
this.registry = this.registry.CreateFromConnectionString(connString);
|
||||
this.ioTHubHostName = IotHubConnectionStringBuilder.Create(connString).HostName;
|
||||
this.log.Info("Selected active IoT Hub for devices", () => new { this.ioTHubHostName });
|
||||
}
|
||||
|
||||
public IDeviceClient GetClient(
|
||||
Device device,
|
||||
IoTHubProtocol protocol,
|
||||
IScriptInterpreter scriptInterpreter)
|
||||
/// <summary>
|
||||
/// Get a client for the device
|
||||
/// </summary>
|
||||
public IDeviceClient GetClient(Device device, IoTHubProtocol protocol, IScriptInterpreter scriptInterpreter)
|
||||
{
|
||||
this.SetupHub();
|
||||
|
||||
var sdkClient = this.GetDeviceSdkClient(device, protocol);
|
||||
var methods = new DeviceMethods(sdkClient, this.log, scriptInterpreter);
|
||||
|
||||
|
@ -77,121 +133,214 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services
|
|||
protocol,
|
||||
sdkClient,
|
||||
methods,
|
||||
this.rateLimiting,
|
||||
this.log);
|
||||
}
|
||||
|
||||
public async Task<Device> GetOrCreateAsync(string deviceId, bool loadTwin, CancellationToken cancellationToken)
|
||||
/// <summary>
|
||||
/// Get the device from the registry
|
||||
/// </summary>
|
||||
public async Task<Device> GetAsync(string deviceId)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await this.GetAsync(deviceId, loadTwin, cancellationToken);
|
||||
}
|
||||
catch (ResourceNotFoundException)
|
||||
{
|
||||
this.log.Debug("Device not found, will create", () => new { deviceId });
|
||||
return await this.CreateAsync(deviceId, cancellationToken);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
this.log.Error("Unexpected error while retrieving the device", () => new { deviceId, e });
|
||||
throw;
|
||||
}
|
||||
}
|
||||
this.SetupHub();
|
||||
|
||||
this.log.Debug("Fetching device from registry", () => new { deviceId });
|
||||
|
||||
public async Task<Device> GetAsync(string deviceId, bool loadTwin, CancellationToken cancellationToken)
|
||||
{
|
||||
Device result = null;
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
|
||||
try
|
||||
{
|
||||
Azure.Devices.Device device = null;
|
||||
Twin twin = null;
|
||||
|
||||
if (loadTwin)
|
||||
var device = await this.GetRegistry().GetDeviceAsync(deviceId);
|
||||
if (device != null)
|
||||
{
|
||||
var deviceTask = this.rateLimiting.LimitRegistryOperationsAsync(
|
||||
() => this.registry.GetDeviceAsync(deviceId, cancellationToken));
|
||||
|
||||
var twinTask = this.rateLimiting.LimitTwinReadsAsync(
|
||||
() => this.registry.GetTwinAsync(deviceId, cancellationToken));
|
||||
|
||||
await Task.WhenAll(deviceTask, twinTask);
|
||||
|
||||
device = deviceTask.Result;
|
||||
twin = twinTask.Result;
|
||||
result = new Device(device, this.ioTHubHostName);
|
||||
}
|
||||
else
|
||||
{
|
||||
device = await this.rateLimiting.LimitRegistryOperationsAsync(
|
||||
() => this.registry.GetDeviceAsync(deviceId, cancellationToken));
|
||||
}
|
||||
|
||||
if (device != null)
|
||||
{
|
||||
result = new Device(device, twin, this.ioTHubHostName);
|
||||
this.log.Debug("Device not found", () => new { deviceId });
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
if (e.InnerException != null && e.InnerException.GetType() == typeof(TaskCanceledException))
|
||||
{
|
||||
// We get here when the cancellation token is triggered, which is fine
|
||||
this.log.Debug("Get device task canceled", () => new { deviceId, e.Message });
|
||||
return null;
|
||||
var timeSpent = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() - now.ToUnixTimeMilliseconds();
|
||||
this.log.Error("Get device task timed out", () => new { timeSpent, deviceId, e.Message });
|
||||
throw;
|
||||
}
|
||||
|
||||
this.log.Error("Unable to fetch the IoT device", () => new { deviceId, e });
|
||||
throw new ExternalDependencyException("Unable to fetch the IoT device.");
|
||||
}
|
||||
|
||||
if (result == null)
|
||||
{
|
||||
throw new ResourceNotFoundException("The device doesn't exist.");
|
||||
throw new ExternalDependencyException("Unable to fetch the IoT device");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private async Task<Device> CreateAsync(string deviceId, CancellationToken cancellationToken)
|
||||
/// <summary>
|
||||
/// Register the new device
|
||||
/// </summary>
|
||||
public async Task<Device> CreateAsync(string deviceId)
|
||||
{
|
||||
this.SetupHub();
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
|
||||
try
|
||||
{
|
||||
this.log.Debug("Creating device", () => new { deviceId });
|
||||
|
||||
var device = new Azure.Devices.Device(deviceId);
|
||||
device = await this.rateLimiting.LimitRegistryOperationsAsync(
|
||||
() => this.registry.AddDeviceAsync(device, cancellationToken));
|
||||
|
||||
var twin = new Twin();
|
||||
if (!DISCARD_TWIN_ON_CREATION)
|
||||
{
|
||||
this.log.Debug("Fetching device twin", () => new { device.Id });
|
||||
twin = await this.rateLimiting.LimitTwinReadsAsync(() => this.registry.GetTwinAsync(device.Id, cancellationToken));
|
||||
}
|
||||
device = await this.GetRegistry().AddDeviceAsync(device);
|
||||
|
||||
this.log.Debug("Writing device twin an adding the `IsSimulated` Tag",
|
||||
() => new { device.Id, DeviceTwin.SIMULATED_TAG_KEY, DeviceTwin.SIMULATED_TAG_VALUE });
|
||||
twin.Tags[DeviceTwin.SIMULATED_TAG_KEY] = DeviceTwin.SIMULATED_TAG_VALUE;
|
||||
|
||||
// TODO: when not discarding the twin, use the right ETag and manage conflicts
|
||||
// https://github.com/Azure/device-simulation-dotnet/issues/83
|
||||
twin = await this.rateLimiting.LimitTwinWritesAsync(
|
||||
() => this.registry.UpdateTwinAsync(device.Id, twin, "*", cancellationToken));
|
||||
|
||||
return new Device(device, twin, this.ioTHubHostName);
|
||||
return new Device(device, this.ioTHubHostName);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
if (e.InnerException != null && e.InnerException.GetType() == typeof(TaskCanceledException))
|
||||
var timeSpent = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() - now.ToUnixTimeMilliseconds();
|
||||
this.log.Error("Unable to create the device", () => new { timeSpent, deviceId, e });
|
||||
throw new ExternalDependencyException("Unable to create the device", e);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a tag to the device, to say it is a simulated device
|
||||
/// </summary>
|
||||
public async Task AddTagAsync(string deviceId)
|
||||
{
|
||||
this.SetupHub();
|
||||
|
||||
this.log.Debug("Writing device twin and adding the `IsSimulated` Tag",
|
||||
() => new { deviceId, SIMULATED_TAG_KEY, SIMULATED_TAG_VALUE });
|
||||
|
||||
var twin = new Twin
|
||||
{
|
||||
Tags = { [SIMULATED_TAG_KEY] = SIMULATED_TAG_VALUE }
|
||||
};
|
||||
await this.GetRegistry().UpdateTwinAsync(deviceId, twin, "*");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a list of devices
|
||||
/// </summary>
|
||||
public async Task CreateListAsync(IEnumerable<string> deviceIds)
|
||||
{
|
||||
this.SetupHub();
|
||||
|
||||
var batches = this.SplitArray(deviceIds.ToList(), REGISTRY_MAX_BATCH_SIZE).ToArray();
|
||||
|
||||
this.log.Info("Creating devices",
|
||||
() => new { Count = deviceIds.Count(), Batches = batches.Length, REGISTRY_MAX_BATCH_SIZE });
|
||||
|
||||
for (var batchNumber = 0; batchNumber < batches.Length; batchNumber++)
|
||||
{
|
||||
var batch = batches[batchNumber];
|
||||
|
||||
this.log.Info("Creating devices batch",
|
||||
() => new { batchNumber, batchSize = batch.Count() });
|
||||
|
||||
BulkRegistryOperationResult result = await this.registry.AddDevices2Async(
|
||||
batch.Select(id => new Azure.Devices.Device(id)));
|
||||
|
||||
this.log.Info("Devices batch created",
|
||||
() => new { batchNumber, result.IsSuccessful, result.Errors });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Delete a list of devices
|
||||
/// </summary>
|
||||
public async Task DeleteListAsync(IEnumerable<string> deviceIds)
|
||||
{
|
||||
this.SetupHub();
|
||||
|
||||
var batches = this.SplitArray(deviceIds.ToList(), REGISTRY_MAX_BATCH_SIZE).ToArray();
|
||||
|
||||
this.log.Info("Deleting devices",
|
||||
() => new { Count = deviceIds.Count(), Batches = batches.Length, REGISTRY_MAX_BATCH_SIZE });
|
||||
|
||||
try
|
||||
{
|
||||
for (var batchNumber = 0; batchNumber < batches.Length; batchNumber++)
|
||||
{
|
||||
// We get here when the cancellation token is triggered, which is fine
|
||||
this.log.Debug("Get device task canceled", () => new { deviceId, e.Message });
|
||||
return null;
|
||||
var batch = batches[batchNumber];
|
||||
|
||||
this.log.Info("Deleting devices batch",
|
||||
() => new { batchNumber, batchSize = batch.Count() });
|
||||
|
||||
BulkRegistryOperationResult result = await this.registry.RemoveDevices2Async(
|
||||
batch.Select(id => new Azure.Devices.Device(id)),
|
||||
forceRemove: true);
|
||||
|
||||
this.log.Info("Devices batch deleted",
|
||||
() => new { batchNumber, result.IsSuccessful, result.Errors });
|
||||
}
|
||||
}
|
||||
catch (TooManyDevicesException error)
|
||||
{
|
||||
this.log.Error("Failed to delete devices, the batch is too big", () => new { error });
|
||||
throw;
|
||||
}
|
||||
catch (IotHubCommunicationException error)
|
||||
{
|
||||
this.log.Error("Failed to delete devices (IotHubCommunicationException)", () => new { error.InnerException, error });
|
||||
throw;
|
||||
}
|
||||
catch (Exception error)
|
||||
{
|
||||
this.log.Error("Failed to delete devices", () => new { error });
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generate a device Id
|
||||
/// </summary>
|
||||
public string GenerateId(string deviceModelId, int position)
|
||||
{
|
||||
return DEVICE_ID_PREFIX + deviceModelId + "." + position;
|
||||
}
|
||||
|
||||
// This call can throw an exception, which is fine when the exception happens during a method
|
||||
// call. We cannot allow the exception to occur in the constructor though, because it
|
||||
// would break DI.
|
||||
private void SetupHub()
|
||||
{
|
||||
if (this.setupDone) return;
|
||||
this.SetCurrentIotHub();
|
||||
|
||||
this.setupDone = true;
|
||||
}
|
||||
|
||||
// Temporary workaround, see https://github.com/Azure/device-simulation-dotnet/issues/136
|
||||
private IRegistryManager GetRegistry()
|
||||
{
|
||||
if (this.registryCount > REGISTRY_LIMIT_REQUESTS)
|
||||
{
|
||||
this.registry.CloseAsync();
|
||||
|
||||
try
|
||||
{
|
||||
this.registry.Dispose();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
// Errors might occur here due to pending requests, they can be ignored
|
||||
this.log.Debug("Ignoring registry manager Dispose() error", () => new { e });
|
||||
}
|
||||
|
||||
this.log.Error("Unable to fetch the IoT device", () => new { deviceId, e });
|
||||
throw new ExternalDependencyException("Unable to fetch the IoT device.");
|
||||
this.registryCount = -1;
|
||||
}
|
||||
|
||||
if (this.registryCount == -1)
|
||||
{
|
||||
string connString = this.connectionStringManager.GetIotHubConnectionString();
|
||||
this.registry = this.registry.CreateFromConnectionString(connString);
|
||||
this.registry.OpenAsync();
|
||||
}
|
||||
|
||||
this.registryCount++;
|
||||
|
||||
return this.registry;
|
||||
}
|
||||
|
||||
private Azure.Devices.Client.DeviceClient GetDeviceSdkClient(Device device, IoTHubProtocol protocol)
|
||||
|
@ -229,7 +378,19 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services
|
|||
throw new InvalidConfigurationException($"Unable to create a client for the given protocol ({protocol})");
|
||||
}
|
||||
|
||||
sdkClient.SetRetryPolicy(new Azure.Devices.Client.NoRetry());
|
||||
sdkClient.OperationTimeoutInMilliseconds = SDK_CLIENT_TIMEOUT;
|
||||
|
||||
return sdkClient;
|
||||
}
|
||||
|
||||
private IEnumerable<IEnumerable<T>> SplitArray<T>(IReadOnlyCollection<T> array, int size)
|
||||
{
|
||||
var count = (int) Math.Ceiling((float) array.Count / size);
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
yield return array.Skip(i * size).Take(size);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,403 @@
|
|||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
|
||||
namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Diagnostics
|
||||
{
|
||||
public interface IActorsLogger
|
||||
{
|
||||
void Setup(string deviceId, string actorName);
|
||||
void ActorStarted();
|
||||
void ActorStopped();
|
||||
|
||||
void FetchScheduled(long time);
|
||||
void FetchingDevice();
|
||||
void DeviceFetched();
|
||||
void DeviceNotFound();
|
||||
void DeviceFetchFailed();
|
||||
|
||||
void RegistrationScheduled(long time);
|
||||
void RegisteringDevice();
|
||||
void DeviceRegistered();
|
||||
void DeviceRegistrationFailed();
|
||||
|
||||
void DeviceTwinTaggingScheduled(long time);
|
||||
void TaggingDeviceTwin();
|
||||
void DeviceTwinTagged();
|
||||
void DeviceTwinTaggingFailed();
|
||||
|
||||
void DeviceConnectionScheduled(long time);
|
||||
void ConnectingDevice();
|
||||
void DeviceConnected();
|
||||
void DeviceConnectionFailed();
|
||||
|
||||
void TelemetryScheduled(long time);
|
||||
void SendingTelemetry();
|
||||
void TelemetryDelivered();
|
||||
void TelemetryFailed();
|
||||
|
||||
void DevicePropertiesUpdateScheduled(long time, bool isRetry);
|
||||
void UpdatingDeviceProperties();
|
||||
void DevicePropertiesUpdated();
|
||||
void DevicePropertiesUpdateFailed();
|
||||
}
|
||||
|
||||
public class ActorsLogger : IActorsLogger
|
||||
{
|
||||
private const string DATE_FORMAT = "yyyy-MM-dd HH:mm:ss.fff";
|
||||
|
||||
private readonly ILogger log;
|
||||
private readonly string path;
|
||||
private readonly bool enabledInConfig;
|
||||
private bool enabled;
|
||||
private string deviceId;
|
||||
private string actorName;
|
||||
private string actorLogFile;
|
||||
private string deviceLogFile;
|
||||
private string registryLogFile;
|
||||
private string propertiesLogFile;
|
||||
private string connectionsLogFile;
|
||||
private string telemetryLogFile;
|
||||
|
||||
public ActorsLogger(ILoggingConfig config, ILogger logger)
|
||||
{
|
||||
this.enabled = false;
|
||||
this.enabledInConfig = config.ExtraDiagnostics;
|
||||
this.path = config.ExtraDiagnosticsPath.Trim();
|
||||
this.log = logger;
|
||||
}
|
||||
|
||||
public void Setup(string deviceId, string actorName)
|
||||
{
|
||||
this.deviceId = deviceId;
|
||||
this.actorName = actorName;
|
||||
|
||||
this.deviceLogFile = this.path + Path.DirectorySeparatorChar + "_." + this.deviceId + ".log";
|
||||
this.actorLogFile = this.path + Path.DirectorySeparatorChar + "actors." + this.actorName + ".log";
|
||||
this.registryLogFile = this.path + Path.DirectorySeparatorChar + "registry.log";
|
||||
this.propertiesLogFile = this.path + Path.DirectorySeparatorChar + "properties.log";
|
||||
this.connectionsLogFile = this.path + Path.DirectorySeparatorChar + "connections.log";
|
||||
this.telemetryLogFile = this.path + Path.DirectorySeparatorChar + "telemetry.log";
|
||||
|
||||
this.enabled = this.enabledInConfig && !string.IsNullOrEmpty(this.path);
|
||||
|
||||
if (!this.enabled) return;
|
||||
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(this.path);
|
||||
this.Log("Actor configured");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
this.log.Error("Unable to write to " + this.path, () => new { e });
|
||||
this.enabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
public void ActorStarted()
|
||||
{
|
||||
if (!this.enabled) return;
|
||||
|
||||
this.Log("Actor started");
|
||||
}
|
||||
|
||||
public void ActorStopped()
|
||||
{
|
||||
if (!this.enabled) return;
|
||||
|
||||
this.Log("Actor stopped");
|
||||
}
|
||||
|
||||
public void FetchScheduled(long time)
|
||||
{
|
||||
if (!this.enabled) return;
|
||||
|
||||
var msg = DateTimeOffset.FromUnixTimeMilliseconds(time).ToString(DATE_FORMAT);
|
||||
this.Log("Fetch scheduled at: " + msg);
|
||||
this.LogRegistry("Fetch scheduled at: " + msg);
|
||||
}
|
||||
|
||||
public void FetchingDevice()
|
||||
{
|
||||
if (!this.enabled) return;
|
||||
|
||||
this.Log("Fetching device");
|
||||
this.LogRegistry("Fetching device");
|
||||
}
|
||||
|
||||
public void DeviceFetched()
|
||||
{
|
||||
if (!this.enabled) return;
|
||||
|
||||
this.Log("Device fetched");
|
||||
this.LogRegistry("Fetched");
|
||||
}
|
||||
|
||||
public void DeviceNotFound()
|
||||
{
|
||||
if (!this.enabled) return;
|
||||
|
||||
this.Log("Device not found");
|
||||
this.LogRegistry("Not found");
|
||||
}
|
||||
|
||||
public void DeviceFetchFailed()
|
||||
{
|
||||
if (!this.enabled) return;
|
||||
|
||||
this.Log("Device fetch FAILED");
|
||||
this.LogRegistry("Fetch FAILED");
|
||||
}
|
||||
|
||||
public void RegistrationScheduled(long time)
|
||||
{
|
||||
if (!this.enabled) return;
|
||||
|
||||
var msg = DateTimeOffset.FromUnixTimeMilliseconds(time).ToString(DATE_FORMAT);
|
||||
this.Log("Device registration scheduled at: " + msg);
|
||||
this.LogRegistry("Registration scheduled at: " + msg);
|
||||
}
|
||||
|
||||
public void RegisteringDevice()
|
||||
{
|
||||
if (!this.enabled) return;
|
||||
|
||||
this.Log("Registering device");
|
||||
this.LogRegistry("Registering");
|
||||
}
|
||||
|
||||
public void DeviceRegistered()
|
||||
{
|
||||
if (!this.enabled) return;
|
||||
|
||||
this.Log("Device registered");
|
||||
this.LogRegistry("Registered");
|
||||
}
|
||||
|
||||
public void DeviceRegistrationFailed()
|
||||
{
|
||||
if (!this.enabled) return;
|
||||
|
||||
this.Log("Device registration FAILED");
|
||||
this.LogRegistry("Registration FAILED");
|
||||
}
|
||||
|
||||
public void DeviceTwinTaggingScheduled(long time)
|
||||
{
|
||||
if (!this.enabled) return;
|
||||
|
||||
var msg = DateTimeOffset.FromUnixTimeMilliseconds(time).ToString(DATE_FORMAT);
|
||||
this.Log("Twin tagging scheduled at: " + msg);
|
||||
this.LogProperties("Twin tagging scheduled at: " + msg);
|
||||
}
|
||||
|
||||
public void TaggingDeviceTwin()
|
||||
{
|
||||
if (!this.enabled) return;
|
||||
|
||||
this.Log("Tagging twin");
|
||||
this.LogProperties("Tagging");
|
||||
}
|
||||
|
||||
public void DeviceTwinTagged()
|
||||
{
|
||||
if (!this.enabled) return;
|
||||
|
||||
this.Log("Twin tagged");
|
||||
this.LogProperties("Twin tagged");
|
||||
}
|
||||
|
||||
public void DeviceTwinTaggingFailed()
|
||||
{
|
||||
if (!this.enabled) return;
|
||||
|
||||
this.Log("Twin tagging FAILED");
|
||||
this.LogProperties("Twin tag FAILED");
|
||||
}
|
||||
|
||||
public void DeviceConnectionScheduled(long time)
|
||||
{
|
||||
if (!this.enabled) return;
|
||||
|
||||
var msg = DateTimeOffset.FromUnixTimeMilliseconds(time).ToString(DATE_FORMAT);
|
||||
this.Log("Device connection scheduled at: " + msg);
|
||||
this.LogConnection("Connection scheduled at: " + msg);
|
||||
}
|
||||
|
||||
public void ConnectingDevice()
|
||||
{
|
||||
if (!this.enabled) return;
|
||||
|
||||
this.Log("Connecting device");
|
||||
this.LogConnection("Connecting");
|
||||
}
|
||||
|
||||
public void DeviceConnected()
|
||||
{
|
||||
if (!this.enabled) return;
|
||||
|
||||
this.Log("Device connected");
|
||||
this.LogConnection("Connected");
|
||||
}
|
||||
|
||||
public void DeviceConnectionFailed()
|
||||
{
|
||||
if (!this.enabled) return;
|
||||
|
||||
this.Log("Connection FAILED");
|
||||
this.LogConnection("FAILED");
|
||||
}
|
||||
|
||||
public void TelemetryScheduled(long time)
|
||||
{
|
||||
if (!this.enabled) return;
|
||||
|
||||
var msg = DateTimeOffset.FromUnixTimeMilliseconds(time).ToString(DATE_FORMAT);
|
||||
this.Log("Telemetry scheduled at: " + msg);
|
||||
this.LogTelemetry("Scheduled at: " + msg);
|
||||
}
|
||||
|
||||
public void SendingTelemetry()
|
||||
{
|
||||
if (!this.enabled) return;
|
||||
|
||||
this.Log("Sending telemetry");
|
||||
this.LogTelemetry("Sending telemetry");
|
||||
}
|
||||
|
||||
public void TelemetryDelivered()
|
||||
{
|
||||
if (!this.enabled) return;
|
||||
|
||||
this.Log("Telemetry delivered");
|
||||
this.LogTelemetry("Delivered");
|
||||
}
|
||||
|
||||
public void TelemetryFailed()
|
||||
{
|
||||
if (!this.enabled) return;
|
||||
|
||||
this.Log("Telemetry FAILED");
|
||||
this.LogTelemetry("FAILED");
|
||||
}
|
||||
|
||||
public void DevicePropertiesUpdateScheduled(long time, bool isRetry)
|
||||
{
|
||||
if (!this.enabled) return;
|
||||
|
||||
var msg = DateTimeOffset.FromUnixTimeMilliseconds(time).ToString(DATE_FORMAT);
|
||||
|
||||
if (isRetry)
|
||||
{
|
||||
this.Log("Device properties update retry scheduled at: " + msg);
|
||||
this.LogProperties("Retry scheduled at: " + msg);
|
||||
}
|
||||
else
|
||||
{
|
||||
this.Log("Device properties update scheduled at: " + msg);
|
||||
this.LogProperties("Device properties update scheduled at: " + msg);
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdatingDeviceProperties()
|
||||
{
|
||||
if (!this.enabled) return;
|
||||
|
||||
this.Log("Updating device properties");
|
||||
this.LogProperties("Updating");
|
||||
}
|
||||
|
||||
public void DevicePropertiesUpdated()
|
||||
{
|
||||
if (!this.enabled) return;
|
||||
|
||||
this.Log("Device properties updated");
|
||||
this.LogProperties("Updated");
|
||||
}
|
||||
|
||||
public void DevicePropertiesUpdateFailed()
|
||||
{
|
||||
if (!this.enabled) return;
|
||||
|
||||
this.Log("Device properties update FAILED");
|
||||
this.LogProperties("Device properties update FAILED");
|
||||
}
|
||||
|
||||
private void Log(string msg)
|
||||
{
|
||||
if (!this.enabled) return;
|
||||
|
||||
this.DeviceLog(msg);
|
||||
this.ActorLog(msg);
|
||||
}
|
||||
|
||||
private void ActorLog(string msg)
|
||||
{
|
||||
if (!this.enabled) return;
|
||||
|
||||
var now = DateTimeOffset.UtcNow.ToString(DATE_FORMAT);
|
||||
this.WriteToFile(
|
||||
this.actorLogFile,
|
||||
$"{now} - {this.deviceId} - {msg}\n");
|
||||
}
|
||||
|
||||
private void DeviceLog(string msg)
|
||||
{
|
||||
if (!this.enabled) return;
|
||||
|
||||
var now = DateTimeOffset.UtcNow.ToString(DATE_FORMAT);
|
||||
this.WriteToFile(
|
||||
this.deviceLogFile,
|
||||
$"{now} - {this.actorName} - {msg}\n");
|
||||
}
|
||||
|
||||
private void LogRegistry(string msg)
|
||||
{
|
||||
if (!this.enabled) return;
|
||||
|
||||
var now = DateTimeOffset.UtcNow.ToString(DATE_FORMAT);
|
||||
this.WriteToFile(
|
||||
this.registryLogFile,
|
||||
$"{now} - {this.deviceId} - {msg}\n");
|
||||
}
|
||||
|
||||
private void LogProperties(string msg)
|
||||
{
|
||||
if (!this.enabled) return;
|
||||
|
||||
var now = DateTimeOffset.UtcNow.ToString(DATE_FORMAT);
|
||||
this.WriteToFile(
|
||||
this.propertiesLogFile,
|
||||
$"{now} - {this.deviceId} - {msg}\n");
|
||||
}
|
||||
|
||||
private void LogConnection(string msg)
|
||||
{
|
||||
if (!this.enabled) return;
|
||||
|
||||
var now = DateTimeOffset.UtcNow.ToString(DATE_FORMAT);
|
||||
this.WriteToFile(
|
||||
this.connectionsLogFile,
|
||||
$"{now} - {this.deviceId} - {msg}\n");
|
||||
}
|
||||
|
||||
private void LogTelemetry(string msg)
|
||||
{
|
||||
if (!this.enabled) return;
|
||||
|
||||
var now = DateTimeOffset.UtcNow.ToString(DATE_FORMAT);
|
||||
this.WriteToFile(
|
||||
this.telemetryLogFile,
|
||||
$"{now} - {this.deviceId} - {msg}\n");
|
||||
}
|
||||
|
||||
private void WriteToFile(string filename, string text)
|
||||
{
|
||||
if (!this.enabled) return;
|
||||
|
||||
this.log.LogToFile(filename, text);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,23 +1,22 @@
|
|||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
|
||||
namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Diagnostics
|
||||
{
|
||||
public enum LogLevel
|
||||
{
|
||||
Debug = 10,
|
||||
Info = 20,
|
||||
Warn = 30,
|
||||
Error = 40
|
||||
}
|
||||
|
||||
public interface ILogger
|
||||
{
|
||||
LogLevel LogLevel { get; }
|
||||
|
||||
string FormatDate(long time);
|
||||
|
||||
bool DebugIsEnabled { get; }
|
||||
bool InfoIsEnabled { get; }
|
||||
|
||||
// The following 4 methods allow to log a message, capturing the context
|
||||
// (i.e. the method where the log message is generated)
|
||||
|
||||
|
@ -33,21 +32,64 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Diagnostics
|
|||
void Info(string message, Func<object> context);
|
||||
void Warn(string message, Func<object> context);
|
||||
void Error(string message, Func<object> context);
|
||||
|
||||
void LogToFile(string filename, string text);
|
||||
}
|
||||
|
||||
public class Logger : ILogger
|
||||
{
|
||||
private readonly string processId;
|
||||
private readonly LogLevel logLevel;
|
||||
private readonly bool logProcessId;
|
||||
private readonly string dateFormat;
|
||||
private readonly object fileLock;
|
||||
|
||||
public Logger(string processId, LogLevel logLevel)
|
||||
private readonly bool bwEnabled;
|
||||
private readonly bool blackListEnabled;
|
||||
private readonly bool whiteListEnabled;
|
||||
private readonly bool bwPrefixUsed;
|
||||
private readonly HashSet<string> blackList;
|
||||
private readonly HashSet<string> whiteList;
|
||||
private readonly string bwListPrefix;
|
||||
private readonly int bwListPrefixLength;
|
||||
|
||||
public Logger(string processId) :
|
||||
this(processId, new LoggingConfig())
|
||||
{
|
||||
}
|
||||
|
||||
public Logger(string processId, ILoggingConfig config)
|
||||
{
|
||||
this.processId = processId;
|
||||
this.logLevel = logLevel;
|
||||
this.logLevel = config.LogLevel;
|
||||
this.logProcessId = config.LogProcessId;
|
||||
this.dateFormat = config.DateFormat;
|
||||
|
||||
this.blackList = config.BlackList;
|
||||
this.whiteList = config.WhiteList;
|
||||
|
||||
this.blackListEnabled = this.blackList.Count > 0;
|
||||
this.whiteListEnabled = this.whiteList.Count > 0;
|
||||
this.bwEnabled = this.blackListEnabled || this.whiteListEnabled;
|
||||
|
||||
this.bwPrefixUsed = !string.IsNullOrEmpty(config.BwListPrefix);
|
||||
this.bwListPrefix = config.BwListPrefix;
|
||||
this.bwListPrefixLength = config.BwListPrefix.Length;
|
||||
|
||||
this.fileLock = new object();
|
||||
}
|
||||
|
||||
public LogLevel LogLevel => this.logLevel;
|
||||
|
||||
public bool DebugIsEnabled => this.logLevel <= LogLevel.Debug;
|
||||
|
||||
public bool InfoIsEnabled => this.logLevel <= LogLevel.Info;
|
||||
|
||||
public string FormatDate(long time)
|
||||
{
|
||||
return DateTimeOffset.FromUnixTimeMilliseconds(time).ToString(this.dateFormat);
|
||||
}
|
||||
|
||||
// The following 4 methods allow to log a message, capturing the context
|
||||
// (i.e. the method where the log message is generated)
|
||||
public void Debug(string message, Action context)
|
||||
|
@ -116,6 +158,15 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Diagnostics
|
|||
this.Write("ERROR", context.GetMethodInfo(), message);
|
||||
}
|
||||
|
||||
public void LogToFile(string filename, string text)
|
||||
{
|
||||
// Without the lock, some logs would be lost due to contentions
|
||||
lock (this.fileLock)
|
||||
{
|
||||
File.AppendAllText(filename, text);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Log the message and information about the context, cleaning up
|
||||
/// and shortening the class name and method name (e.g. removing
|
||||
|
@ -137,8 +188,35 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Diagnostics
|
|||
methodname = methodname.Split(new[] { '>' }, 2).First();
|
||||
methodname = methodname.Split(new[] { '<' }, 2).Last();
|
||||
|
||||
var time = DateTimeOffset.UtcNow.ToString("yyyy-MM-dd HH:mm:ss.fff");
|
||||
Console.WriteLine($"[{level}][{time}][{this.processId}][{classname}:{methodname}] {text}");
|
||||
// Check blacklisted and whitelisted classes and methods
|
||||
if (this.bwEnabled)
|
||||
{
|
||||
var bwClass = classname;
|
||||
if (this.bwPrefixUsed && bwClass.StartsWith(this.bwListPrefix))
|
||||
{
|
||||
bwClass = bwClass.Substring(this.bwListPrefixLength);
|
||||
}
|
||||
|
||||
if (this.blackListEnabled
|
||||
&& (this.blackList.Contains(bwClass + "." + methodname)
|
||||
|| this.blackList.Contains(bwClass + ".*")))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.whiteListEnabled
|
||||
&& !this.whiteList.Contains(bwClass + "." + methodname)
|
||||
&& !this.whiteList.Contains(bwClass + ".*"))
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
var time = DateTimeOffset.UtcNow.ToString(this.dateFormat);
|
||||
|
||||
Console.WriteLine(this.logProcessId
|
||||
? $"[{level}][{time}][{this.processId}][{classname}:{methodname}] {text}"
|
||||
: $"[{level}][{time}][{classname}:{methodname}] {text}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Diagnostics
|
||||
{
|
||||
public enum LogLevel
|
||||
{
|
||||
Debug = 10,
|
||||
Info = 20,
|
||||
Warn = 30,
|
||||
Error = 40
|
||||
}
|
||||
|
||||
public interface ILoggingConfig
|
||||
{
|
||||
LogLevel LogLevel { get; }
|
||||
bool LogProcessId { get; }
|
||||
bool ExtraDiagnostics { get; }
|
||||
string ExtraDiagnosticsPath { get; }
|
||||
string DateFormat { get; }
|
||||
string BwListPrefix { get; }
|
||||
HashSet<string> BlackList { get; }
|
||||
HashSet<string> WhiteList { get; }
|
||||
}
|
||||
|
||||
public class LoggingConfig : ILoggingConfig
|
||||
{
|
||||
public const LogLevel DEFAULT_LOGLEVEL = LogLevel.Warn;
|
||||
public const string DEFAULT_DATE_FORMAT = "yyyy-MM-dd HH:mm:ss.fff";
|
||||
|
||||
public LogLevel LogLevel { get; set; }
|
||||
public bool LogProcessId { get; set; }
|
||||
public bool ExtraDiagnostics { get; set; }
|
||||
public string ExtraDiagnosticsPath { get; set; }
|
||||
public string DateFormat { get; set; }
|
||||
public string BwListPrefix { get; set; }
|
||||
public HashSet<string> BlackList { get; set; }
|
||||
public HashSet<string> WhiteList { get; set; }
|
||||
|
||||
public LoggingConfig()
|
||||
{
|
||||
this.LogLevel = DEFAULT_LOGLEVEL;
|
||||
this.LogProcessId = true;
|
||||
this.ExtraDiagnostics = false;
|
||||
this.DateFormat = DEFAULT_DATE_FORMAT;
|
||||
this.BwListPrefix = string.Empty;
|
||||
this.BlackList = new HashSet<string>();
|
||||
this.WhiteList = new HashSet<string>();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
|
||||
namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Exceptions
|
||||
{
|
||||
public class InvalidIotHubConnectionStringFormatException : Exception
|
||||
{
|
||||
/// <summary>
|
||||
/// This exception is thrown when the IoTHub connection string provided
|
||||
/// is not properly formatted. The correct format is:
|
||||
/// HostName=[hubname];SharedAccessKeyName=[iothubowner or service];SharedAccessKey=[null or valid key]
|
||||
/// </summary>
|
||||
public InvalidIotHubConnectionStringFormatException() : base()
|
||||
{
|
||||
}
|
||||
|
||||
public InvalidIotHubConnectionStringFormatException(string message) : base(message)
|
||||
{
|
||||
}
|
||||
|
||||
public InvalidIotHubConnectionStringFormatException(string message, Exception innerException) : base(message, innerException)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
|
||||
namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Exceptions
|
||||
{
|
||||
public class IotHubConnectionException : Exception
|
||||
{
|
||||
/// <summary>
|
||||
/// This exception is thrown when a connection to the IoTHub with the
|
||||
/// provided connection string fails.
|
||||
/// </summary>
|
||||
public IotHubConnectionException() : base()
|
||||
{
|
||||
}
|
||||
|
||||
public IotHubConnectionException(string message) : base(message)
|
||||
{
|
||||
}
|
||||
|
||||
public IotHubConnectionException(string message, Exception innerException) : base(message, innerException)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
|
@ -6,8 +6,8 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Exceptions
|
|||
{
|
||||
/// <summary>
|
||||
/// This exception is thrown when a client attempts to update a resource
|
||||
/// providing the wrong Etag value. The client should retrieve the
|
||||
/// resource again, to have the new Etag, and retry.
|
||||
/// providing the wrong ETag value. The client should retrieve the
|
||||
/// resource again, to have the new ETag, and retry.
|
||||
/// </summary>
|
||||
public class ResourceOutOfDateException : Exception
|
||||
{
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
|
||||
namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services
|
||||
{
|
||||
public class TelemetrySendException : Exception
|
||||
{
|
||||
/// <summary>
|
||||
/// This exception is thrown when a device failed sending messages to IoTHub.
|
||||
/// </summary>
|
||||
public TelemetrySendException(string message) : base(message)
|
||||
{
|
||||
}
|
||||
|
||||
public TelemetrySendException(string message, Exception innerException) : base(message, innerException)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
|
||||
namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services
|
||||
{
|
||||
public class TelemetrySendIOException : Exception
|
||||
{
|
||||
/// <summary>
|
||||
/// This exception is thrown when a device fails to send a messages due to a IO exception.
|
||||
/// </summary>
|
||||
public TelemetrySendIOException(string message) : base(message)
|
||||
{
|
||||
}
|
||||
|
||||
public TelemetrySendIOException(string message, Exception innerException) : base(message, innerException)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
|
||||
namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services
|
||||
{
|
||||
public class TelemetrySendTimeoutException : Exception
|
||||
{
|
||||
/// <summary>
|
||||
/// This exception is thrown when a device fails to send a messages due to a timeout, typically due to throttling.
|
||||
/// </summary>
|
||||
public TelemetrySendTimeoutException(string message) : base(message)
|
||||
{
|
||||
}
|
||||
|
||||
public TelemetrySendTimeoutException(string message, Exception innerException) : base(message, innerException)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
|
@ -85,6 +85,7 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Http
|
|||
SetHeaders(request, httpRequest);
|
||||
|
||||
this.log.Debug("Sending request", () => new { httpMethod, request.Uri, request.Options });
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
|
||||
try
|
||||
{
|
||||
|
@ -102,13 +103,14 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Http
|
|||
}
|
||||
catch (HttpRequestException e)
|
||||
{
|
||||
var timeSpent = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() - now.ToUnixTimeMilliseconds();
|
||||
var errorMessage = e.Message;
|
||||
if (e.InnerException != null)
|
||||
{
|
||||
errorMessage += " - " + e.InnerException.Message;
|
||||
}
|
||||
|
||||
this.log.Error("Request failed", () => new { errorMessage, e });
|
||||
this.log.Error("Request failed", () => new { timeSpent, httpMethod.Method, request.Uri, errorMessage, e });
|
||||
|
||||
return new HttpResponse
|
||||
{
|
||||
|
@ -118,7 +120,8 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Http
|
|||
}
|
||||
catch (TaskCanceledException e)
|
||||
{
|
||||
this.log.Error("Request failed", () => new { Message = e.Message + " The request timed out, the endpoint might be unreachable.", e });
|
||||
var timeSpent = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() - now.ToUnixTimeMilliseconds();
|
||||
this.log.Error("Request failed", () => new { timeSpent, httpMethod.Method, request.Uri, Message = e.Message + " The request timed out, the endpoint might be unreachable.", e });
|
||||
|
||||
return new HttpResponse
|
||||
{
|
||||
|
@ -128,7 +131,8 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Http
|
|||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
this.log.Error("Request failed", () => new { e.Message, e });
|
||||
var timeSpent = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() - now.ToUnixTimeMilliseconds();
|
||||
this.log.Error("Request failed", () => new { timeSpent, httpMethod.Method, request.Uri, e.Message, e });
|
||||
|
||||
return new HttpResponse
|
||||
{
|
||||
|
|
|
@ -0,0 +1,377 @@
|
|||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Azure.Devices;
|
||||
using Microsoft.Azure.Devices.Common;
|
||||
using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Diagnostics;
|
||||
using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Exceptions;
|
||||
using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Runtime;
|
||||
|
||||
namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.IotHub
|
||||
{
|
||||
// retrieves Iot Hub connection secret from storage
|
||||
public interface IIotHubConnectionStringManager
|
||||
{
|
||||
string GetIotHubConnectionString();
|
||||
Task<string> RedactAndStoreAsync(string connectionString);
|
||||
Task ValidateConnectionStringAsync(string connectionString);
|
||||
}
|
||||
|
||||
public class IotHubConnectionStringManager : IIotHubConnectionStringManager
|
||||
{
|
||||
private const string CONNSTRING_REGEX = @"^HostName=(?<hostName>.*);SharedAccessKeyName=(?<keyName>.*);SharedAccessKey=(?<key>.*)$";
|
||||
private const string CONNSTRING_REGEX_HOSTNAME = "hostName";
|
||||
private const string CONNSTRING_REGEX_KEYNAME = "keyName";
|
||||
private const string CONNSTRING_REGEX_KEY = "key";
|
||||
private const string CONNSTRING_FILE_NAME = "custom_iothub_key.txt";
|
||||
|
||||
private readonly string connStringFilePath;
|
||||
|
||||
private readonly IServicesConfig config;
|
||||
private readonly ILogger log;
|
||||
|
||||
public IotHubConnectionStringManager(
|
||||
IServicesConfig config,
|
||||
ILogger logger)
|
||||
{
|
||||
this.config = config;
|
||||
this.connStringFilePath = config.IoTHubDataFolder + CONNSTRING_FILE_NAME;
|
||||
this.log = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks storage for which connection string to use.
|
||||
/// If value is null or file doesn't exist, return the
|
||||
/// value stored in the configuration file. Otherwise
|
||||
/// returns value in local storage.
|
||||
/// </summary>
|
||||
/// <returns>Full connection string including secret</returns>
|
||||
public string GetIotHubConnectionString()
|
||||
{
|
||||
// read connection string file from webservice
|
||||
string customIotHub = this.ReadFromFile();
|
||||
|
||||
// check if default hub should be used
|
||||
if (this.IsDefaultHub(customIotHub))
|
||||
{
|
||||
this.log.Info("Using IotHub connection string stored in config.", () => { });
|
||||
return this.config.IoTHubConnString;
|
||||
}
|
||||
|
||||
this.log.Debug("Using IoTHub provided by the client.", () => new { });
|
||||
return customIotHub;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates that the IoTHub Connection String is valid, stores the full
|
||||
/// string with key in a local file, then removes the sensitive key data and
|
||||
/// returns the IoTHub Connection String with an empty string for the SharedAccessKey
|
||||
///
|
||||
/// TODO Encryption for key & storage in documentDb instead of file
|
||||
/// https://github.com/Azure/device-simulation-dotnet/issues/129
|
||||
/// </summary>
|
||||
/// <returns>Redacted connection string (i.e. without SharedAccessKey)</returns>
|
||||
public async Task<string> RedactAndStoreAsync(string connectionString)
|
||||
{
|
||||
// check if environment variable should be used
|
||||
if (this.IsDefaultHub(connectionString))
|
||||
{
|
||||
await this.UseDefaultIotHubAsync();
|
||||
return ServicesConfig.USE_DEFAULT_IOTHUB;
|
||||
}
|
||||
|
||||
// check that connection string is valid and the IotHub exists
|
||||
await this.ValidateConnectionStringAsync(connectionString);
|
||||
|
||||
// find key
|
||||
var key = this.GetKeyFromConnString(connectionString);
|
||||
|
||||
// if key is null, the string has been redacted,
|
||||
// check if hub is in storage
|
||||
if (key.IsNullOrWhiteSpace())
|
||||
{
|
||||
if (this.ConnectionStringIsStored(connectionString))
|
||||
{
|
||||
return connectionString;
|
||||
}
|
||||
else
|
||||
{
|
||||
string message = "Could not connect to IotHub with the connection " +
|
||||
"string provided. Check that the key is valid and " +
|
||||
"that the hub exists.";
|
||||
this.log.Debug(message, () => { });
|
||||
throw new IotHubConnectionException(message);
|
||||
}
|
||||
}
|
||||
|
||||
// store full connection string with key in local file
|
||||
this.WriteToFile(connectionString);
|
||||
|
||||
// redact key from connection string and return
|
||||
return connectionString.Replace(key, "");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if connection string provided has a valid format.
|
||||
/// If format is valid, and the connection string has a non-null
|
||||
/// value for the key, also checks if a connection to the IotHub
|
||||
/// can be made.
|
||||
/// </summary>
|
||||
public async Task ValidateConnectionStringAsync(string connectionString)
|
||||
{
|
||||
// valid if default IotHub
|
||||
if (this.IsDefaultHub(connectionString))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
connectionString = connectionString.Trim();
|
||||
|
||||
// check format of provided string
|
||||
var match = Regex.Match(connectionString, CONNSTRING_REGEX);
|
||||
if (!match.Success)
|
||||
{
|
||||
var message = "Invalid connection string format for IoTHub. " +
|
||||
"The correct format is: HostName=[hubname];SharedAccessKeyName=" +
|
||||
"[iothubowner or service];SharedAccessKey=[null or valid key]";
|
||||
this.log.Error(message, () => { });
|
||||
throw new InvalidIotHubConnectionStringFormatException(message);
|
||||
}
|
||||
|
||||
// if a key is provided, check if IoTHub is valid
|
||||
if (!match.Groups[CONNSTRING_REGEX_KEY].Value.IsNullOrWhiteSpace())
|
||||
{
|
||||
this.ValidateExistingIotHub(connectionString);
|
||||
await this.ValidateReadPermissionsAsync(connectionString);
|
||||
await this.ValidateWritePermissionsAsync(connectionString);
|
||||
}
|
||||
|
||||
this.log.Debug("IotHub connection string provided is valid.", () => { });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if string is intended to be the default IotHub.
|
||||
/// Default hub is used if provided string is null, empty, or default.
|
||||
/// </summary>
|
||||
private bool IsDefaultHub(string connectionString)
|
||||
{
|
||||
return
|
||||
connectionString == null ||
|
||||
connectionString == string.Empty ||
|
||||
string.Equals(
|
||||
connectionString.Trim(),
|
||||
ServicesConfig.USE_DEFAULT_IOTHUB,
|
||||
StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary> Throws if unable to create a registry manager with a valid IotHub. </summary>
|
||||
private void ValidateExistingIotHub(string connectionString)
|
||||
{
|
||||
try
|
||||
{
|
||||
RegistryManager.CreateFromConnectionString(connectionString);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
string message = "Could not connect to IotHub with the connection " +
|
||||
"string provided. Check that the key is valid and " +
|
||||
"that the hub exists.";
|
||||
this.log.Error(message, () => new { e });
|
||||
throw new IotHubConnectionException(message, e);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ValidateReadPermissionsAsync(string connectionString)
|
||||
{
|
||||
var registryManager = RegistryManager.CreateFromConnectionString(connectionString);
|
||||
|
||||
try
|
||||
{
|
||||
await registryManager.GetDevicesAsync(1, CancellationToken.None);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
string message = "Could not read devices with the Iot Hub connection " +
|
||||
"string provided. Check that the policy for the key allows " +
|
||||
"`Registry Read/Write` and `Service Connect` permissions.";
|
||||
this.log.Error(message, () => new { e });
|
||||
throw new IotHubConnectionException(message, e);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private async Task ValidateWritePermissionsAsync(string connectionString)
|
||||
{
|
||||
var registryManager = RegistryManager.CreateFromConnectionString(connectionString);
|
||||
|
||||
string testDeviceId = "test-device-creation-" + DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||
var device = new Device(testDeviceId);
|
||||
|
||||
// To test permissions, try to create a test device.
|
||||
try
|
||||
{
|
||||
await registryManager.AddDeviceAsync(device);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
string message = "Could not create devices with the Iot Hub connection " +
|
||||
"string provided. Check that the policy for the key allows " +
|
||||
"`Registry Read/Write` and `Service Connect` permissions.";
|
||||
this.log.Error(message, () => new { e });
|
||||
throw new IotHubConnectionException(message, e);
|
||||
}
|
||||
|
||||
// Delete the test device that was created.
|
||||
// If test device deletion fails, retry. Throw if unsuccessful.
|
||||
const int MAX_DELETE_RETRY = 3;
|
||||
int deleteRetryCount = 0;
|
||||
Device response;
|
||||
do
|
||||
{
|
||||
try
|
||||
{
|
||||
await registryManager.RemoveDeviceAsync(testDeviceId);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
string message = "Could not delete test device from IotHub. Attempt " +
|
||||
deleteRetryCount + 1 + " of " + MAX_DELETE_RETRY;
|
||||
this.log.Error(message, () => new { testDeviceId, e });
|
||||
throw new IotHubConnectionException(message, e);
|
||||
}
|
||||
|
||||
response = await registryManager.GetDeviceAsync(testDeviceId);
|
||||
|
||||
deleteRetryCount++;
|
||||
|
||||
} while (response != null && deleteRetryCount < MAX_DELETE_RETRY);
|
||||
|
||||
if (response != null)
|
||||
{
|
||||
string message = "Could not delete test device from IotHub.";
|
||||
this.log.Error(message, () => new { testDeviceId });
|
||||
throw new IotHubConnectionException(message);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If simulation uses the pre-provisioned IoT Hub for the service,
|
||||
/// then remove sensitive hub information that is no longer needed
|
||||
/// </summary>
|
||||
private async Task UseDefaultIotHubAsync()
|
||||
{
|
||||
// check if default hub is valid
|
||||
try
|
||||
{
|
||||
this.ValidateExistingIotHub(this.config.IoTHubConnString);
|
||||
await this.ValidateReadPermissionsAsync(this.config.IoTHubConnString);
|
||||
await this.ValidateWritePermissionsAsync(this.config.IoTHubConnString);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
string msg = "Unable to use default IoT Hub. Check that the " +
|
||||
"pre-provisioned hub exists and has the correct permissions.";
|
||||
this.log.Error(msg, () => new { e });
|
||||
throw new IotHubConnectionException(msg, e);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// delete custom IoT Hub string if default hub is being used
|
||||
File.Delete(this.connStringFilePath);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
this.log.Error("Unable to delete connection string file.",
|
||||
() => new { this.connStringFilePath, e });
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private string GetKeyFromConnString(string connectionString)
|
||||
{
|
||||
var match = Regex.Match(connectionString, CONNSTRING_REGEX);
|
||||
|
||||
return match.Groups[CONNSTRING_REGEX_KEY].Value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Takes in a connection string with empty key information.
|
||||
/// Returns true if the key for the redacted string is in storage.
|
||||
/// Returns false if the key for the redacted string is not in storage.
|
||||
/// </summary>
|
||||
private bool ConnectionStringIsStored(string connectionString)
|
||||
{
|
||||
// get stored string from file
|
||||
var storedHubString = this.ReadFromFile();
|
||||
|
||||
if (connectionString.IsNullOrWhiteSpace() ||
|
||||
storedHubString.IsNullOrWhiteSpace())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// parse user provided hub info
|
||||
var userHubMatch = Regex.Match(connectionString, CONNSTRING_REGEX);
|
||||
var userHubHostName = userHubMatch.Groups[CONNSTRING_REGEX_HOSTNAME].Value;
|
||||
var userHubKeyName = userHubMatch.Groups[CONNSTRING_REGEX_KEYNAME].Value;
|
||||
|
||||
// parse stored hub info
|
||||
var storedHubMatch = Regex.Match(storedHubString, CONNSTRING_REGEX);
|
||||
var storedHubHostName = storedHubMatch.Groups[CONNSTRING_REGEX_HOSTNAME].Value;
|
||||
var storedHubKeyName = storedHubMatch.Groups[CONNSTRING_REGEX_KEYNAME].Value;
|
||||
|
||||
return userHubHostName == storedHubHostName &&
|
||||
userHubKeyName == storedHubKeyName;
|
||||
}
|
||||
|
||||
private void WriteToFile(string connectionString)
|
||||
{
|
||||
this.log.Debug("Write IotHub connection string to file.",
|
||||
() => new { this.connStringFilePath });
|
||||
|
||||
try
|
||||
{
|
||||
File.WriteAllText(this.connStringFilePath, connectionString);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
this.log.Error("Unable to write connection string to file.",
|
||||
() => new { this.connStringFilePath, e });
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves connection string from local storage.
|
||||
/// Returns null if file doesn't exist.
|
||||
/// </summary>
|
||||
private string ReadFromFile()
|
||||
{
|
||||
this.log.Debug("Check for IotHub connection string from file.",
|
||||
() => new { this.connStringFilePath });
|
||||
if (File.Exists(this.connStringFilePath))
|
||||
{
|
||||
try
|
||||
{
|
||||
// remove special characters and return string
|
||||
return Regex.Replace(File.ReadAllText(this.connStringFilePath), @"[\r\n\t ]+", "");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
this.log.Error("Unable to read connection string from file.",
|
||||
() => new { this.connStringFilePath, e });
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
this.log.Debug("IotHub connection string file not present.",
|
||||
() => new { this.connStringFilePath });
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,104 @@
|
|||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Azure.Devices;
|
||||
using Microsoft.Azure.Devices.Shared;
|
||||
|
||||
namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.IotHub
|
||||
{
|
||||
/**
|
||||
* Shim interface to allow mocking the IoT Hub SDK registry manager
|
||||
* static methods in the unit tests.
|
||||
*/
|
||||
public interface IRegistryManager
|
||||
{
|
||||
IRegistryManager CreateFromConnectionString(string connString);
|
||||
|
||||
Task<BulkRegistryOperationResult> AddDevices2Async(IEnumerable<Device> devices);
|
||||
|
||||
Task<BulkRegistryOperationResult> RemoveDevices2Async(IEnumerable<Device> devices, bool forceRemove);
|
||||
|
||||
Task OpenAsync();
|
||||
|
||||
Task CloseAsync();
|
||||
|
||||
Task<Device> AddDeviceAsync(Device device);
|
||||
|
||||
Task<Device> GetDeviceAsync(string deviceId);
|
||||
|
||||
Task UpdateTwinAsync(string deviceId, Twin twinPatch, string eTag);
|
||||
|
||||
void Dispose();
|
||||
}
|
||||
|
||||
/**
|
||||
* Shim class to allow mocking the IoT Hub SDK registry manager
|
||||
* static methods in the unit tests. No logic here, only forwarding
|
||||
* methods to the actual class in the SDK.
|
||||
*/
|
||||
public class RegistryManagerShim : IRegistryManager, IDisposable
|
||||
{
|
||||
private readonly RegistryManager registry;
|
||||
|
||||
public RegistryManagerShim()
|
||||
{
|
||||
this.registry = null;
|
||||
}
|
||||
|
||||
public RegistryManagerShim(string connString)
|
||||
{
|
||||
this.registry = RegistryManager.CreateFromConnectionString(connString);
|
||||
}
|
||||
|
||||
public IRegistryManager CreateFromConnectionString(string connString)
|
||||
{
|
||||
return new RegistryManagerShim(connString);
|
||||
}
|
||||
|
||||
public async Task<BulkRegistryOperationResult> AddDevices2Async(
|
||||
IEnumerable<Device> devices)
|
||||
{
|
||||
return await this.registry.AddDevices2Async(devices, CancellationToken.None);
|
||||
}
|
||||
|
||||
public async Task<BulkRegistryOperationResult> RemoveDevices2Async(
|
||||
IEnumerable<Device> devices,
|
||||
bool forceRemove)
|
||||
{
|
||||
return await this.registry.RemoveDevices2Async(devices, forceRemove, CancellationToken.None);
|
||||
}
|
||||
|
||||
public async Task OpenAsync()
|
||||
{
|
||||
await this.registry.OpenAsync();
|
||||
}
|
||||
|
||||
public async Task CloseAsync()
|
||||
{
|
||||
await this.registry.CloseAsync();
|
||||
}
|
||||
|
||||
public async Task<Device> AddDeviceAsync(Device device)
|
||||
{
|
||||
return await this.registry.AddDeviceAsync(device);
|
||||
}
|
||||
|
||||
public async Task<Device> GetDeviceAsync(string deviceId)
|
||||
{
|
||||
return await this.registry.GetDeviceAsync(deviceId);
|
||||
}
|
||||
|
||||
public async Task UpdateTwinAsync(string deviceId, Twin twinPatch, string eTag)
|
||||
{
|
||||
await this.registry.UpdateTwinAsync(deviceId, twinPatch, eTag);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
this.registry.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -9,65 +9,50 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Models
|
|||
{
|
||||
public class Device
|
||||
{
|
||||
public string Etag { get; set; }
|
||||
public string ETag { get; set; }
|
||||
public string Id { get; set; }
|
||||
public int C2DMessageCount { get; set; }
|
||||
public DateTimeOffset LastActivity { get; set; }
|
||||
public bool Connected { get; set; }
|
||||
public bool Enabled { get; set; }
|
||||
public DateTimeOffset LastStatusUpdated { get; set; }
|
||||
public DeviceTwin Twin { get; set; }
|
||||
public string IoTHubHostName { get; set; }
|
||||
public string AuthPrimaryKey { get; set; }
|
||||
|
||||
public Device(
|
||||
string etag,
|
||||
string eTag,
|
||||
string id,
|
||||
int c2DMessageCount,
|
||||
DateTimeOffset lastActivity,
|
||||
bool connected,
|
||||
bool enabled,
|
||||
DateTimeOffset lastStatusUpdated,
|
||||
DeviceTwin twin,
|
||||
string primaryKey,
|
||||
string ioTHubHostName)
|
||||
{
|
||||
this.Etag = etag;
|
||||
this.ETag = eTag;
|
||||
this.Id = id;
|
||||
this.C2DMessageCount = c2DMessageCount;
|
||||
this.LastActivity = lastActivity;
|
||||
this.Connected = connected;
|
||||
this.Enabled = enabled;
|
||||
this.LastStatusUpdated = lastStatusUpdated;
|
||||
this.Twin = twin;
|
||||
this.IoTHubHostName = ioTHubHostName;
|
||||
this.AuthPrimaryKey = primaryKey;
|
||||
}
|
||||
|
||||
public Device(Azure.Devices.Device azureDevice, DeviceTwin twin, string ioTHubHostName) :
|
||||
public Device(Azure.Devices.Device azureDevice, string ioTHubHostName) :
|
||||
this(
|
||||
etag: azureDevice.ETag,
|
||||
eTag: azureDevice.ETag,
|
||||
id: azureDevice.Id,
|
||||
c2DMessageCount: azureDevice.CloudToDeviceMessageCount,
|
||||
lastActivity: azureDevice.LastActivityTime,
|
||||
connected: azureDevice.ConnectionState.Equals(DeviceConnectionState.Connected),
|
||||
enabled: azureDevice.Status.Equals(DeviceStatus.Enabled),
|
||||
lastStatusUpdated: azureDevice.StatusUpdatedTime,
|
||||
twin: twin,
|
||||
ioTHubHostName: ioTHubHostName,
|
||||
primaryKey: azureDevice.Authentication.SymmetricKey.PrimaryKey)
|
||||
{
|
||||
}
|
||||
|
||||
public Device(Azure.Devices.Device azureDevice, Twin azureTwin, string ioTHubHostName) :
|
||||
this(azureDevice, new DeviceTwin(azureTwin), ioTHubHostName)
|
||||
{
|
||||
}
|
||||
|
||||
public Device SetReportedProperty(string key, JToken value)
|
||||
{
|
||||
this.Twin.ReportedProperties[key] = value;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,13 +12,16 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Models
|
|||
{
|
||||
public string Type { get; set; }
|
||||
public string Path { get; set; }
|
||||
public TimeSpan Interval { get; set; }
|
||||
|
||||
// Dynamic object passed in input to the script, the schema is
|
||||
// defined by receiving script
|
||||
public object Params { get; set; }
|
||||
|
||||
public Script()
|
||||
{
|
||||
this.Type = "javascript";
|
||||
this.Path = "scripts" + System.IO.Path.DirectorySeparatorChar;
|
||||
this.Interval = TimeSpan.Zero;
|
||||
this.Params = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -102,12 +105,14 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Models
|
|||
public class StateSimulation
|
||||
{
|
||||
public Dictionary<string, object> InitialState { get; set; }
|
||||
public Script Script { get; set; }
|
||||
public TimeSpan Interval { get; set; }
|
||||
public List<Script> Scripts { get; set; }
|
||||
|
||||
public StateSimulation()
|
||||
{
|
||||
this.InitialState = new Dictionary<string, object>();
|
||||
this.Script = new Script();
|
||||
this.Interval = TimeSpan.Zero;
|
||||
this.Scripts = new List<Script>();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,99 +0,0 @@
|
|||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.Azure.Devices.Shared;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Models
|
||||
{
|
||||
public class DeviceTwin
|
||||
{
|
||||
// Simulated devices are marked with a tag "IsSimulated = Y"
|
||||
public const string SIMULATED_TAG_KEY = "IsSimulated";
|
||||
|
||||
public const string SIMULATED_TAG_VALUE = "Y";
|
||||
|
||||
public string Etag { get; set; }
|
||||
public string DeviceId { get; set; }
|
||||
public bool IsSimulated { get; set; }
|
||||
public Dictionary<string, JToken> DesiredProperties { get; set; }
|
||||
public Dictionary<string, JToken> ReportedProperties { get; set; }
|
||||
public Dictionary<string, JToken> Tags { get; set; }
|
||||
|
||||
public DeviceTwin(Twin twin)
|
||||
{
|
||||
if (twin != null)
|
||||
{
|
||||
this.Etag = twin.ETag;
|
||||
this.DeviceId = twin.DeviceId;
|
||||
this.Tags = TwinCollectionToDictionary(twin.Tags);
|
||||
this.DesiredProperties = TwinCollectionToDictionary(twin.Properties.Desired);
|
||||
this.ReportedProperties = TwinCollectionToDictionary(twin.Properties.Reported);
|
||||
this.IsSimulated = this.Tags.ContainsKey(SIMULATED_TAG_KEY) && this.Tags[SIMULATED_TAG_KEY].ToString() == SIMULATED_TAG_VALUE;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
JValue: string, integer, float, boolean
|
||||
JArray: list, array
|
||||
JObject: dictionary, object
|
||||
|
||||
JValue: JToken, IEquatable<JValue>, IFormattable, IComparable, IComparable<JValue>, IConvertible
|
||||
JArray: JContainer, IList<JToken>, ICollection<JToken>, IEnumerable<JToken>, IEnumerable
|
||||
JObject: JContainer, IDictionary<string, JToken>, ICollection<KeyValuePair<string, JToken>>, IEnumerable<KeyValuePair<string, JToken>>, IEnumerable, INotifyPropertyChanged, ICustomTypeDescriptor, INotifyPropertyChanging
|
||||
JContainer: JToken, IList<JToken>, ICollection<JToken>, IEnumerable<JToken>, IEnumerable, ITypedList, IBindingList, IList, ICollection, INotifyCollectionChanged
|
||||
JToken: IJEnumerable<JToken>, IEnumerable<JToken>, IEnumerable, IJsonLineInfo, ICloneable, IDynamicMetaObjectProvider
|
||||
*/
|
||||
private static Dictionary<string, JToken> TwinCollectionToDictionary(TwinCollection x)
|
||||
{
|
||||
var result = new Dictionary<string, JToken>();
|
||||
|
||||
if (x == null) return result;
|
||||
|
||||
foreach (KeyValuePair<string, object> twin in x)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (twin.Value is JToken)
|
||||
{
|
||||
result.Add(twin.Key, (JToken) twin.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
result.Add(twin.Key, JToken.Parse(twin.Value.ToString()));
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine(e);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static TwinCollection DictionaryToTwinCollection(Dictionary<string, JToken> x)
|
||||
{
|
||||
var result = new TwinCollection();
|
||||
|
||||
if (x == null) return result;
|
||||
|
||||
foreach (KeyValuePair<string, JToken> item in x)
|
||||
{
|
||||
try
|
||||
{
|
||||
result[item.Key] = item.Value;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine(e);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,12 +2,18 @@
|
|||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Runtime;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Models
|
||||
{
|
||||
public class Simulation
|
||||
{
|
||||
public string Etag { get; set; }
|
||||
private DateTimeOffset? startTime;
|
||||
private DateTimeOffset? endTime;
|
||||
private string iotHubConnectionString;
|
||||
|
||||
public string ETag { get; set; }
|
||||
public string Id { get; set; }
|
||||
public bool Enabled { get; set; }
|
||||
public IList<DeviceModelRef> DeviceModels { get; set; }
|
||||
|
@ -15,8 +21,34 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Models
|
|||
public DateTimeOffset Modified { get; set; }
|
||||
public long Version { get; set; }
|
||||
|
||||
public DateTimeOffset? StartTime
|
||||
{
|
||||
get => this.startTime;
|
||||
set => this.startTime = value ?? DateTimeOffset.MinValue;
|
||||
}
|
||||
|
||||
public DateTimeOffset? EndTime
|
||||
{
|
||||
get => this.endTime;
|
||||
set => this.endTime = value ?? DateTimeOffset.MaxValue;
|
||||
}
|
||||
|
||||
public string IotHubConnectionString
|
||||
{
|
||||
get => this.iotHubConnectionString;
|
||||
set => this.iotHubConnectionString = value ?? ServicesConfig.USE_DEFAULT_IOTHUB;
|
||||
}
|
||||
|
||||
public Simulation()
|
||||
{
|
||||
// When unspecified, a simulation is enabled
|
||||
this.Enabled = true;
|
||||
|
||||
this.StartTime = DateTimeOffset.MinValue;
|
||||
this.EndTime = DateTimeOffset.MaxValue;
|
||||
|
||||
// by default, use environment variable
|
||||
this.IotHubConnectionString = ServicesConfig.USE_DEFAULT_IOTHUB;
|
||||
this.DeviceModels = new List<DeviceModelRef>();
|
||||
}
|
||||
|
||||
|
@ -24,6 +56,117 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Models
|
|||
{
|
||||
public string Id { get; set; }
|
||||
public int Count { get; set; }
|
||||
public DeviceModelOverride Override { get; set; }
|
||||
}
|
||||
|
||||
public class DeviceModelOverride
|
||||
{
|
||||
// Optional field, used to customize the scripts which update the device state
|
||||
public DeviceModelSimulationOverride Simulation { get; set; }
|
||||
|
||||
// Optional field, the list can be empty
|
||||
// When non empty, the content is merged with (overwriting) the scripts in the device model definition
|
||||
// If this list is shorter than the original definition, elements in excess are removed
|
||||
// i.e. to keep all the original telemetry messages, there must be an entry for each of them
|
||||
public IList<DeviceModelTelemetryOverride> Telemetry { get; set; }
|
||||
|
||||
public DeviceModelOverride()
|
||||
{
|
||||
this.Simulation = null;
|
||||
this.Telemetry = null;
|
||||
}
|
||||
}
|
||||
|
||||
public class DeviceModelSimulationOverride
|
||||
{
|
||||
// Optional, used to customize the initial state of the device
|
||||
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
|
||||
public Dictionary<string,object> InitialState { get; set; }
|
||||
|
||||
// Optional, used to customize the device state update interval
|
||||
public TimeSpan? Interval { get; set; }
|
||||
|
||||
// Optional field, the list can be empty
|
||||
// When non empty, the content is merged with (overwriting) the scripts in the device model definition
|
||||
// If this list is shorter than the original definition, elements in excess are removed
|
||||
// i.e. to keep all the original scripts, there must be an entry for each of them
|
||||
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
|
||||
public IList<DeviceModelSimulationScriptOverride> Scripts { get; set; }
|
||||
|
||||
public DeviceModelSimulationOverride()
|
||||
{
|
||||
this.InitialState = null;
|
||||
this.Interval = null;
|
||||
this.Scripts = null;
|
||||
}
|
||||
}
|
||||
|
||||
public class DeviceModelSimulationScriptOverride
|
||||
{
|
||||
// Optional, used to change the script used
|
||||
public string Type { get; set; }
|
||||
|
||||
// Optional, used to change the script used
|
||||
public string Path { get; set; }
|
||||
|
||||
// Optional, used to provide input parameters to the script
|
||||
public object Params { get; set; }
|
||||
|
||||
public DeviceModelSimulationScriptOverride()
|
||||
{
|
||||
this.Type = null;
|
||||
this.Path = null;
|
||||
this.Params = null;
|
||||
}
|
||||
}
|
||||
|
||||
public class DeviceModelTelemetryOverride
|
||||
{
|
||||
// Optional field, used to customize the telemetry interval
|
||||
public TimeSpan? Interval { get; set; }
|
||||
|
||||
// Optional field, when null use the template set in the device model definition
|
||||
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
|
||||
public string MessageTemplate { get; set; }
|
||||
|
||||
// Optional field, when null use the schema set in the device model definition
|
||||
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
|
||||
public DeviceModelTelemetryMessageSchemaOverride MessageSchema { get; set; }
|
||||
|
||||
public DeviceModelTelemetryOverride()
|
||||
{
|
||||
this.Interval = null;
|
||||
this.MessageTemplate = null;
|
||||
this.MessageSchema = null;
|
||||
}
|
||||
}
|
||||
|
||||
public class DeviceModelTelemetryMessageSchemaOverride
|
||||
{
|
||||
// Optional, used to customize the name of the message schema
|
||||
public string Name { get; set; }
|
||||
|
||||
// Optional, used to change the message format, e.g. from JSON to base64
|
||||
public DeviceModel.DeviceModelMessageSchemaFormat? Format { get; set; }
|
||||
|
||||
// Optional, used to replace the list of fields in the schema (the content is not merged)
|
||||
public IDictionary<string, DeviceModel.DeviceModelMessageSchemaType> Fields { get; set; }
|
||||
|
||||
public DeviceModelTelemetryMessageSchemaOverride()
|
||||
{
|
||||
this.Name = null;
|
||||
this.Format = null;
|
||||
this.Fields = null;
|
||||
}
|
||||
}
|
||||
|
||||
public bool ShouldBeRunning()
|
||||
{
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
|
||||
return this.Enabled
|
||||
&& (!this.StartTime.HasValue || this.StartTime.Value.CompareTo(now) <= 0)
|
||||
&& (!this.EndTime.HasValue || this.EndTime.Value.CompareTo(now) > 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Models
|
|||
{
|
||||
public class SimulationPatch
|
||||
{
|
||||
public string Etag { get; set; }
|
||||
public string ETag { get; set; }
|
||||
public string Id { get; set; }
|
||||
public bool? Enabled { get; set; }
|
||||
}
|
||||
|
|
|
@ -0,0 +1,101 @@
|
|||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Models
|
||||
{
|
||||
public interface ISmartDictionary
|
||||
{
|
||||
IDictionary<string, object> GetAll();
|
||||
void SetAll(Dictionary<string, object> newState);
|
||||
bool Has(string key);
|
||||
object Get(string key);
|
||||
void Set(string key, object value);
|
||||
bool Changed { get; }
|
||||
void ResetChanged();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Wrapper for a dictionary that supports concurrent reads and writes
|
||||
/// and tracks if any entries have been changed.
|
||||
/// </summary>
|
||||
public class SmartDictionary : ISmartDictionary
|
||||
{
|
||||
/// <summary>
|
||||
/// A collection of items that can support concurrent reads and writes.
|
||||
/// </summary>
|
||||
private IDictionary<string, object> dictionary;
|
||||
|
||||
public bool Changed { get; private set; }
|
||||
|
||||
public SmartDictionary()
|
||||
{
|
||||
// needs to support concurrent reads and writes
|
||||
this.dictionary = new ConcurrentDictionary<string, object>();
|
||||
this.Changed = false;
|
||||
}
|
||||
|
||||
public SmartDictionary(IDictionary<string, object> dictionary)
|
||||
{
|
||||
this.dictionary = new ConcurrentDictionary<string, object>(dictionary);
|
||||
this.Changed = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when values have been synchronized. Resets 'changed' flag to false.
|
||||
/// </summary>
|
||||
public void ResetChanged()
|
||||
{
|
||||
this.Changed = false;
|
||||
}
|
||||
|
||||
/// <param name="key"></param>
|
||||
/// <exception cref="KeyNotFoundException">
|
||||
/// thrown when the key specified does not match any key in the collection.
|
||||
/// </exception>
|
||||
public object Get(string key)
|
||||
{
|
||||
if (!this.dictionary.ContainsKey(key))
|
||||
{
|
||||
throw new KeyNotFoundException();
|
||||
}
|
||||
|
||||
return this.dictionary[key];
|
||||
}
|
||||
|
||||
public IDictionary<string, object> GetAll()
|
||||
{
|
||||
return new Dictionary<string, object>(this.dictionary);
|
||||
}
|
||||
|
||||
public bool Has(string key)
|
||||
{
|
||||
return this.dictionary.ContainsKey(key);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set a property with the given key, adds new value if key does not exist.
|
||||
/// Sets the changed flag to true.
|
||||
/// </summary>
|
||||
public void Set(string key, object value)
|
||||
{
|
||||
if (this.dictionary.ContainsKey(key))
|
||||
{
|
||||
this.dictionary[key] = value;
|
||||
}
|
||||
else
|
||||
{
|
||||
this.dictionary.Add(key, value);
|
||||
}
|
||||
|
||||
this.Changed = true;
|
||||
}
|
||||
|
||||
public void SetAll(Dictionary<string, object> newState)
|
||||
{
|
||||
this.dictionary = new ConcurrentDictionary<string, object>(newState);
|
||||
this.Changed = true;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Azure.Devices;
|
||||
using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Diagnostics;
|
||||
using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Runtime;
|
||||
|
||||
namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services
|
||||
{
|
||||
public interface IPreprovisionedIotHub
|
||||
{
|
||||
// Ping the registry to see if the connection is healthy
|
||||
Task<Tuple<bool, string>> PingRegistryAsync();
|
||||
}
|
||||
|
||||
public class PreprovisionedIotHub : IPreprovisionedIotHub
|
||||
{
|
||||
private readonly ILogger log;
|
||||
private readonly string connectionString;
|
||||
|
||||
private string ioTHubHostName;
|
||||
private RegistryManager registry;
|
||||
private bool setupDone;
|
||||
|
||||
public PreprovisionedIotHub(
|
||||
IServicesConfig config,
|
||||
ILogger logger)
|
||||
{
|
||||
this.log = logger;
|
||||
this.connectionString = config.IoTHubConnString;
|
||||
this.setupDone = false;
|
||||
}
|
||||
|
||||
// Ping the registry to see if the connection is healthy
|
||||
public async Task<Tuple<bool, string>> PingRegistryAsync()
|
||||
{
|
||||
this.SetupHub();
|
||||
|
||||
try
|
||||
{
|
||||
await this.registry.GetDeviceAsync("healthcheck");
|
||||
return new Tuple<bool, string>(true, "OK");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
this.log.Error("Device registry test failed", () => new { e });
|
||||
return new Tuple<bool, string>(false, e.Message);
|
||||
}
|
||||
}
|
||||
|
||||
// This call can throw an exception, which is fine when the exception happens during a method
|
||||
// call. We cannot allow the exception to occur in the constructor though, because it
|
||||
// would break DI.
|
||||
private void SetupHub()
|
||||
{
|
||||
if (this.setupDone) return;
|
||||
|
||||
this.registry = RegistryManager.CreateFromConnectionString(this.connectionString);
|
||||
this.registry.OpenAsync();
|
||||
|
||||
this.ioTHubHostName = IotHubConnectionStringBuilder.Create(this.connectionString).HostName;
|
||||
this.log.Info("Selected active IoT Hub for preprovisioned hub status check", () => new { this.ioTHubHostName });
|
||||
|
||||
this.setupDone = true;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Runtime
|
||||
{
|
||||
public interface IDeploymentConfig
|
||||
{
|
||||
string AzureSubscriptionDomain { get; }
|
||||
string AzureSubscriptionId { get; }
|
||||
string AzureResourceGroup { get; }
|
||||
string AzureIothubName { get; }
|
||||
}
|
||||
|
||||
public class DeploymentConfig : IDeploymentConfig
|
||||
{
|
||||
public string AzureSubscriptionDomain { get; set; }
|
||||
public string AzureSubscriptionId { get; set; }
|
||||
public string AzureResourceGroup { get; set; }
|
||||
public string AzureIothubName { get; set; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
|
||||
namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Runtime
|
||||
{
|
||||
/// <summary>
|
||||
/// Provide factory pattern for dependencies that are instantiated
|
||||
/// multiple times during the application lifetime.
|
||||
/// How to use:
|
||||
/// <code>
|
||||
/// class MyClass : IMyClass {
|
||||
/// public MyClass(IFactory factory) {
|
||||
/// this.factory = factory;
|
||||
/// }
|
||||
/// public SomeMethod() {
|
||||
/// var instance1 = this.factory.Resolve<ISomething>();
|
||||
/// var instance2 = this.factory.Resolve<ISomething>();
|
||||
/// var instance3 = this.factory.Resolve<ISomething>();
|
||||
/// }
|
||||
/// }
|
||||
/// </code>
|
||||
/// </summary>
|
||||
public interface IFactory
|
||||
{
|
||||
T Resolve<T>();
|
||||
}
|
||||
|
||||
public class Factory : IFactory
|
||||
{
|
||||
private static Func<Type, object> resolver;
|
||||
|
||||
public T Resolve<T>()
|
||||
{
|
||||
return (T)resolver.Invoke(typeof(T));
|
||||
}
|
||||
|
||||
public static void RegisterResolver(Func<Type, object> func)
|
||||
{
|
||||
resolver = func;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -6,26 +6,30 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Runtime
|
|||
{
|
||||
public interface IServicesConfig
|
||||
{
|
||||
string DeviceModelsFolder { get; set; }
|
||||
string DeviceModelsScriptsFolder { get; set; }
|
||||
string IoTHubConnString { get; set; }
|
||||
string StorageAdapterApiUrl { get; set; }
|
||||
int StorageAdapterApiTimeout { get; set; }
|
||||
IRateLimitingConfiguration RateLimiting { get; set; }
|
||||
string DeviceModelsFolder { get; }
|
||||
string DeviceModelsScriptsFolder { get; }
|
||||
string IoTHubDataFolder { get; }
|
||||
string IoTHubConnString { get; }
|
||||
string StorageAdapterApiUrl { get; }
|
||||
int StorageAdapterApiTimeout { get; }
|
||||
bool TwinReadWriteEnabled { get; }
|
||||
}
|
||||
|
||||
// TODO: test Windows/Linux folder separator
|
||||
// https://github.com/Azure/device-simulation-dotnet/issues/84
|
||||
public class ServicesConfig : IServicesConfig
|
||||
{
|
||||
public const string USE_DEFAULT_IOTHUB = "default";
|
||||
|
||||
private string dtf;
|
||||
private string dtbf;
|
||||
private string ihf;
|
||||
|
||||
public ServicesConfig()
|
||||
{
|
||||
this.dtf = string.Empty;
|
||||
this.dtbf = string.Empty;
|
||||
this.RateLimiting = new RateLimitingConfiguration();
|
||||
this.ihf = string.Empty;
|
||||
}
|
||||
|
||||
public string DeviceModelsFolder
|
||||
|
@ -40,13 +44,19 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Runtime
|
|||
set { this.dtbf = this.NormalizePath(value); }
|
||||
}
|
||||
|
||||
public string IoTHubDataFolder
|
||||
{
|
||||
get { return this.ihf; }
|
||||
set { this.ihf = this.NormalizePath(value); }
|
||||
}
|
||||
|
||||
public string IoTHubConnString { get; set; }
|
||||
|
||||
public string StorageAdapterApiUrl { get; set; }
|
||||
|
||||
public int StorageAdapterApiTimeout { get; set; }
|
||||
|
||||
public IRateLimitingConfiguration RateLimiting { get; set; }
|
||||
public bool TwinReadWriteEnabled { get; set; }
|
||||
|
||||
private string NormalizePath(string path)
|
||||
{
|
||||
|
|
|
@ -7,8 +7,8 @@
|
|||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="jint" Version="2.10.4" />
|
||||
<PackageReference Include="Microsoft.Azure.Devices" Version="1.4.1" />
|
||||
<PackageReference Include="Microsoft.Azure.Devices.Client" Version="1.5.2" />
|
||||
<PackageReference Include="Microsoft.Azure.Devices" Version="1.6.0" />
|
||||
<PackageReference Include="Microsoft.Azure.Devices.Client" Version="1.7.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="2.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Ini" Version="2.0.0" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="10.0.3" />
|
||||
|
@ -23,9 +23,6 @@
|
|||
<None Update="data\devicemodels\elevator-01.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="data\devicemodels\elevator-02.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="data\devicemodels\engine-01.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
|
@ -41,80 +38,83 @@
|
|||
<None Update="data\devicemodels\scripts\EmptyTank-method.js">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="data\devicemodels\scripts\engine-01-state.js">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="data\devicemodels\scripts\engine-02-state.js">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="data\devicemodels\scripts\FillTank-method.js">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="data\devicemodels\scripts\prototype-02-state.js">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="data\devicemodels\scripts\prototype-01-state.js">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="data\devicemodels\scripts\chiller-02-state.js">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="data\devicemodels\elevator-02.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="data\devicemodels\scripts\elevator-02-state.js">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="data\devicemodels\scripts\EmergencyValveRelease-method.js">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="data\devicemodels\scripts\FirmwareUpdate-method.js">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="data\devicemodels\scripts\IncreasePressure-method.js">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="data\devicemodels\scripts\Reboot-method.js">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="data\devicemodels\scripts\RestartEngine-method.js">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="data\devicemodels\scripts\StartElevator-method.js">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="data\devicemodels\scripts\StartDevice-method.js">
|
||||
<None Update="data\devicemodels\scripts\StartMovingDevice-method.js">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="data\devicemodels\scripts\StopElevator-method.js">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="data\devicemodels\scripts\TempIncrease-method.js">
|
||||
<None Update="data\devicemodels\scripts\StopMovingDevice-method.js">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="data\devicemodels\scripts\TempDecrease-method.js">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="data\devicemodels\scripts\EmergencyValveRelease-method.js">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="data\devicemodels\scripts\IncreasePressure-method.js">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="data\devicemodels\scripts\MoveDevice-method.js">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="data\devicemodels\scripts\StopDevice-method.js">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="data\devicemodels\scripts\FirmwareUpdate-method.js">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="data\devicemodels\scripts\reboot-method.js">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="data\devicemodels\truck-01.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="data\devicemodels\truck-02.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="data\devicemodels\scripts\chiller-01-state.js">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="data\devicemodels\scripts\chiller-02-state.js">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="data\devicemodels\scripts\elevator-01-state.js">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="data\devicemodels\scripts\elevator-02-state.js">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="data\devicemodels\scripts\engine-01-state.js">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="data\devicemodels\scripts\engine-02-state.js">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="data\devicemodels\scripts\prototype-01-state.js">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="data\devicemodels\scripts\prototype-02-state.js">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="data\devicemodels\scripts\truck-01-state.js">
|
||||
<None Update="data\devicemodels\scripts\TempIncrease-method.js">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="data\devicemodels\scripts\truck-02-state.js">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="data\devicemodels\truck-02.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="data\devicemodels\truck-01.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="data\devicemodels\scripts\chiller-01-state.js">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="data\devicemodels\scripts\elevator-01-state.js">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="data\devicemodels\scripts\truck-01-state.js">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="data\iothub\README.md">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
</Project>
|
|
@ -0,0 +1,196 @@
|
|||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Diagnostics;
|
||||
using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Models;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Simulation
|
||||
{
|
||||
public interface IInternalInterpreter
|
||||
{
|
||||
// List of supported scripts, used for input validation
|
||||
HashSet<string> SupportedScripts { get; }
|
||||
|
||||
/// <summary>Invoke one of the internal scripts</summary>
|
||||
/// <param name="scriptPath">Name of the script</param>
|
||||
/// <param name="scriptParams">Script parameters, e.g. min, max, step</param>
|
||||
/// <param name="context">Context, e.g. current time, device Id, device Model</param>
|
||||
/// <param name="state">Current device sensors state</param>
|
||||
/// <param name="properties">Current device properties state</param>
|
||||
/// <remarks>Updates the internal device sensors state</remarks>
|
||||
void Invoke(
|
||||
string scriptPath, object scriptParams,
|
||||
Dictionary<string, object> context,
|
||||
ISmartDictionary state,
|
||||
ISmartDictionary properties);
|
||||
}
|
||||
|
||||
public class InternalInterpreter : IInternalInterpreter
|
||||
{
|
||||
private const string SCRIPT_RANDOM = "math.random.withinrange";
|
||||
private const string SCRIPT_INCREASING = "math.increasing";
|
||||
private const string SCRIPT_DECREASING = "math.decreasing";
|
||||
private const double EQUALITY_PRECISION = .001;
|
||||
|
||||
public HashSet<string> SupportedScripts => new HashSet<string>
|
||||
{
|
||||
SCRIPT_RANDOM,
|
||||
SCRIPT_INCREASING,
|
||||
SCRIPT_DECREASING
|
||||
};
|
||||
|
||||
private readonly Random random;
|
||||
private readonly ILogger log;
|
||||
|
||||
public InternalInterpreter(ILogger logger)
|
||||
{
|
||||
this.log = logger;
|
||||
this.random = new Random();
|
||||
}
|
||||
|
||||
public void Invoke(
|
||||
string scriptPath,
|
||||
object scriptParams,
|
||||
Dictionary<string, object> context,
|
||||
ISmartDictionary state,
|
||||
ISmartDictionary properties)
|
||||
{
|
||||
switch (scriptPath.ToLowerInvariant())
|
||||
{
|
||||
case SCRIPT_RANDOM:
|
||||
this.RunRandomNumberScript(scriptParams, state);
|
||||
break;
|
||||
case SCRIPT_INCREASING:
|
||||
this.RunIncreasingScript(scriptParams, state);
|
||||
break;
|
||||
case SCRIPT_DECREASING:
|
||||
this.RunDecreasingScript(scriptParams, state);
|
||||
break;
|
||||
default:
|
||||
throw new NotSupportedException($"Unknown script `{scriptPath}`.");
|
||||
}
|
||||
}
|
||||
|
||||
// For each sensors specified, generate a random number in the range requested
|
||||
private void RunRandomNumberScript(object scriptParams, ISmartDictionary state)
|
||||
{
|
||||
var sensors = this.JsonParamAsDictionary(scriptParams);
|
||||
foreach (var sensor in sensors)
|
||||
{
|
||||
(double min, double max) = this.GetMinMaxParameters(sensor.Value);
|
||||
var value = this.random.NextDouble() * (max - min) + min;
|
||||
state.Set(sensor.Key, value);
|
||||
}
|
||||
}
|
||||
|
||||
// For each sensors specified, increase the current state, up to a maximum, then restart from a minimum
|
||||
private void RunIncreasingScript(object scriptParams, ISmartDictionary state)
|
||||
{
|
||||
var sensors = this.JsonParamAsDictionary(scriptParams);
|
||||
foreach (var sensor in sensors)
|
||||
{
|
||||
// Extract scripts parameters from the device model script configuration
|
||||
(double min, double max, double step) = this.GetMinMaxStepParameters(sensor.Value);
|
||||
|
||||
// Add the sensor to the state if missing
|
||||
if (!state.Has(sensor.Key))
|
||||
{
|
||||
state.Set(sensor.Key, min);
|
||||
}
|
||||
|
||||
double current = Convert.ToDouble(state.Get(sensor.Key));
|
||||
double next = AreEqual(current, max) ? min : Math.Min(current + step, max);
|
||||
|
||||
state.Set(sensor.Key, next);
|
||||
}
|
||||
}
|
||||
|
||||
// For each sensors specified, decrease the current state, down to a minimum, then restart from a maximum
|
||||
private void RunDecreasingScript(object scriptParams, ISmartDictionary state)
|
||||
{
|
||||
var sensors = this.JsonParamAsDictionary(scriptParams);
|
||||
foreach (var sensor in sensors)
|
||||
{
|
||||
// Extract scripts parameters from the device model script configuration
|
||||
(double min, double max, double step) = this.GetMinMaxStepParameters(sensor.Value);
|
||||
|
||||
// Add the sensor to the state if missing
|
||||
if (!state.Has(sensor.Key))
|
||||
{
|
||||
state.Set(sensor.Key, max);
|
||||
}
|
||||
|
||||
double current = Convert.ToDouble(state.Get(sensor.Key));
|
||||
double next = AreEqual(current, min) ? max : Math.Max(current - step, min);
|
||||
|
||||
state.Set(sensor.Key, next);
|
||||
}
|
||||
}
|
||||
|
||||
private (double, double) GetMinMaxParameters(object parameters)
|
||||
{
|
||||
var dict = this.JsonParamAsDictionary(parameters);
|
||||
|
||||
if (!dict.ContainsKey("Min"))
|
||||
{
|
||||
this.log.Error("Missing 'Min' parameter", () => new { dict });
|
||||
throw new FormatException("Missing 'Min' parameter");
|
||||
}
|
||||
|
||||
if (!dict.ContainsKey("Max"))
|
||||
{
|
||||
this.log.Error("Missing 'Max' parameter", () => new { dict });
|
||||
throw new FormatException("Missing 'Max' parameter");
|
||||
}
|
||||
|
||||
return (Convert.ToDouble(dict["Min"]), Convert.ToDouble(dict["Max"]));
|
||||
}
|
||||
|
||||
private (double, double, double) GetMinMaxStepParameters(object parameters)
|
||||
{
|
||||
var dict = this.JsonParamAsDictionary(parameters);
|
||||
|
||||
if (!dict.ContainsKey("Min"))
|
||||
{
|
||||
this.log.Error("Missing 'Min' parameter", () => new { dict });
|
||||
throw new FormatException("Missing 'Min' parameter");
|
||||
}
|
||||
|
||||
if (!dict.ContainsKey("Max"))
|
||||
{
|
||||
this.log.Error("Missing 'Max' parameter", () => new { dict });
|
||||
throw new FormatException("Missing 'Max' parameter");
|
||||
}
|
||||
|
||||
if (!dict.ContainsKey("Step"))
|
||||
{
|
||||
this.log.Error("Missing 'Step' parameter", () => new { dict });
|
||||
throw new FormatException("Missing 'Step' parameter");
|
||||
}
|
||||
|
||||
return (Convert.ToDouble(dict["Min"]),
|
||||
Convert.ToDouble(dict["Max"]),
|
||||
Convert.ToDouble(dict["Step"]));
|
||||
}
|
||||
|
||||
private Dictionary<string, object> JsonParamAsDictionary(object parameters)
|
||||
{
|
||||
try
|
||||
{
|
||||
return JsonConvert.DeserializeObject<Dictionary<string, object>>(JsonConvert.SerializeObject(parameters));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
this.log.Error("Unknown script parameters format. The parameters should be passed key-value dictionary.", () => new { parameters, e });
|
||||
throw new NotSupportedException("Unknown script parameters format. The parameters should be passed key-value dictionary.");
|
||||
}
|
||||
}
|
||||
|
||||
private static bool AreEqual(double a, double b)
|
||||
{
|
||||
return Math.Abs(a - b) < EQUALITY_PRECISION;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -7,25 +7,37 @@ using System.Linq;
|
|||
using System.Threading.Tasks;
|
||||
using Jint;
|
||||
using Jint.Native;
|
||||
using Jint.Parser;
|
||||
using Jint.Parser.Ast;
|
||||
using Jint.Runtime.Descriptors;
|
||||
using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Diagnostics;
|
||||
using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Models;
|
||||
using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Runtime;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Simulation
|
||||
{
|
||||
public interface IJavascriptInterpreter
|
||||
{
|
||||
Dictionary<string, object> Invoke(
|
||||
void Invoke(
|
||||
string filename,
|
||||
Dictionary<string, object> context,
|
||||
Dictionary<string, object> state);
|
||||
ISmartDictionary state,
|
||||
ISmartDictionary properties);
|
||||
}
|
||||
|
||||
public class JavascriptInterpreter : IJavascriptInterpreter
|
||||
{
|
||||
private readonly ILogger log;
|
||||
private readonly string folder;
|
||||
private Dictionary<string, object> deviceState;
|
||||
private ISmartDictionary deviceState;
|
||||
private ISmartDictionary deviceProperties;
|
||||
|
||||
// The following are static to improve overall performance
|
||||
// TODO make the class a singleton - https://github.com/Azure/device-simulation-dotnet/issues/45
|
||||
private static readonly JavaScriptParser parser = new JavaScriptParser();
|
||||
|
||||
private static readonly Dictionary<string, Program> programs = new Dictionary<string, Program>();
|
||||
|
||||
public JavascriptInterpreter(
|
||||
IServicesConfig config,
|
||||
|
@ -38,14 +50,16 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Simulation
|
|||
/// <summary>
|
||||
/// Load a JS file and execute the main() function, passing in
|
||||
/// context information and the output from the previous execution.
|
||||
/// Returns a map of values.
|
||||
/// Modifies the internal device state with the latest values.
|
||||
/// </summary>
|
||||
public Dictionary<string, object> Invoke(
|
||||
public void Invoke(
|
||||
string filename,
|
||||
Dictionary<string, object> context,
|
||||
Dictionary<string, object> state)
|
||||
ISmartDictionary state,
|
||||
ISmartDictionary properties)
|
||||
{
|
||||
this.deviceState = state;
|
||||
this.deviceProperties = properties;
|
||||
|
||||
var engine = new Engine();
|
||||
|
||||
|
@ -53,26 +67,47 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Simulation
|
|||
// logging into the service logs
|
||||
engine.SetValue("log", new Action<object>(this.JsLog));
|
||||
|
||||
//register callback for state updates
|
||||
// register callback for state updates
|
||||
engine.SetValue("updateState", new Action<JsValue>(this.UpdateState));
|
||||
|
||||
//register sleep function for javascript use
|
||||
engine.SetValue("sleep", new Action<int>(this.Sleep));
|
||||
// register callback for property updates
|
||||
engine.SetValue("updateProperty", new Action<string, object>(this.UpdateProperty));
|
||||
|
||||
var sourceCode = this.LoadScript(filename);
|
||||
this.log.Debug("Executing JS function", () => new { filename });
|
||||
// register sleep function for javascript use
|
||||
engine.SetValue("sleep", new Action<int>(this.Sleep));
|
||||
|
||||
try
|
||||
{
|
||||
var output = engine.Execute(sourceCode).Invoke("main", context, this.deviceState);
|
||||
var result = this.JsValueToDictionary(output);
|
||||
this.log.Debug("JS function success", () => new { filename, result });
|
||||
return result;
|
||||
Program program;
|
||||
if (programs.ContainsKey(filename))
|
||||
{
|
||||
program = programs[filename];
|
||||
}
|
||||
else
|
||||
{
|
||||
var sourceCode = this.LoadScript(filename);
|
||||
|
||||
this.log.Info("Compiling script source code", () => new { filename });
|
||||
program = parser.Parse(sourceCode);
|
||||
programs.Add(filename, program);
|
||||
}
|
||||
|
||||
this.log.Debug("Executing JS function", () => new { filename });
|
||||
|
||||
JsValue output = engine.Execute(program).Invoke(
|
||||
"main",
|
||||
context,
|
||||
this.deviceState.GetAll(),
|
||||
this.deviceProperties.GetAll());
|
||||
|
||||
// update the internal device state with the new state
|
||||
this.UpdateState(output);
|
||||
|
||||
this.log.Debug("JS function success", () => new { filename, output });
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
this.log.Error("JS function failure", () => new { e.Message, e.GetType().FullName });
|
||||
return new Dictionary<string, object>();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -83,7 +118,8 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Simulation
|
|||
/// </summary>
|
||||
private Dictionary<string, object> JsValueToDictionary(JsValue data)
|
||||
{
|
||||
Dictionary<string, object> result;
|
||||
var result = new Dictionary<string, object>();
|
||||
if (data == null) return result;
|
||||
|
||||
try
|
||||
{
|
||||
|
@ -156,26 +192,28 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Simulation
|
|||
{
|
||||
string key;
|
||||
object value;
|
||||
Dictionary<string, object> stateChanges;
|
||||
Dictionary<string, object> stateChanges = this.JsValueToDictionary(data);
|
||||
|
||||
this.log.Debug("Updating state from the script", () => new { data, this.deviceState });
|
||||
|
||||
stateChanges = this.JsValueToDictionary((JsValue) data);
|
||||
|
||||
//Update device state with the script data passed
|
||||
lock (this.deviceState)
|
||||
// Update device state with the script data passed
|
||||
for (int i = 0; i < stateChanges.Count; i++)
|
||||
{
|
||||
for (int i = 0; i < stateChanges.Count; i++)
|
||||
{
|
||||
key = stateChanges.Keys.ElementAt(i);
|
||||
value = stateChanges.Values.ElementAt(i);
|
||||
if (this.deviceState.ContainsKey(key))
|
||||
{
|
||||
this.log.Debug("state change", () => new { key, value });
|
||||
this.deviceState[key] = value;
|
||||
}
|
||||
}
|
||||
key = stateChanges.Keys.ElementAt(i);
|
||||
value = stateChanges.Values.ElementAt(i);
|
||||
this.log.Debug("state change", () => new { key, value });
|
||||
this.deviceState.Set(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Move this out of the scriptinterpreter class into DeviceStateActor to keep this class stateless
|
||||
// https://github.com/Azure/device-simulation-dotnet/issues/45
|
||||
private void UpdateProperty(string key, object value)
|
||||
{
|
||||
this.log.Debug("Updating device property from the script", () => new { key, value });
|
||||
|
||||
// Update device property at key with the script value passed
|
||||
this.deviceProperties.Set(key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,41 +9,58 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Simulation
|
|||
{
|
||||
public interface IScriptInterpreter
|
||||
{
|
||||
Dictionary<string, object> Invoke(
|
||||
/// <summary>Invoke one of the device script files</summary>
|
||||
/// <param name="script">Name of the script</param>
|
||||
/// <param name="context">Context, e.g. current time, device Id, device Model</param>
|
||||
/// <param name="state">Current device state</param>
|
||||
/// <param name="properties">Current device properties</param>
|
||||
/// <remarks> Updates the internal device state and internal device properties</remarks>
|
||||
void Invoke(
|
||||
Script script,
|
||||
Dictionary<string, object> context,
|
||||
Dictionary<string, object> state);
|
||||
ISmartDictionary state,
|
||||
ISmartDictionary properties);
|
||||
}
|
||||
|
||||
public class ScriptInterpreter : IScriptInterpreter
|
||||
{
|
||||
private readonly IJavascriptInterpreter jsInterpreter;
|
||||
private readonly IInternalInterpreter intInterpreter;
|
||||
private readonly ILogger log;
|
||||
|
||||
public ScriptInterpreter(
|
||||
IJavascriptInterpreter jsInterpreter,
|
||||
IInternalInterpreter intInterpreter,
|
||||
ILogger logger)
|
||||
{
|
||||
this.jsInterpreter = jsInterpreter;
|
||||
this.intInterpreter = intInterpreter;
|
||||
this.log = logger;
|
||||
}
|
||||
|
||||
public Dictionary<string, object> Invoke(
|
||||
public void Invoke(
|
||||
Script script,
|
||||
Dictionary<string, object> context,
|
||||
Dictionary<string, object> state)
|
||||
ISmartDictionary state,
|
||||
ISmartDictionary properties)
|
||||
{
|
||||
switch (script.Type.ToLowerInvariant())
|
||||
{
|
||||
default:
|
||||
this.log.Error("Unknown script type", () => new { script.Type });
|
||||
throw new NotSupportedException($"Unknown script type `${script.Type}`.");
|
||||
throw new NotSupportedException($"Unknown script type `{script.Type}`.");
|
||||
|
||||
case "javascript":
|
||||
this.log.Debug("Invoking JS", () => new { script.Path, context, state });
|
||||
var result = this.jsInterpreter.Invoke(script.Path, context, state);
|
||||
this.log.Debug("JS result", () => new { result });
|
||||
return result;
|
||||
this.jsInterpreter.Invoke(script.Path, context, state, properties);
|
||||
this.log.Debug("JS invocation complete", () => {});
|
||||
break;
|
||||
|
||||
case "internal":
|
||||
this.log.Debug("Invoking internal script", () => new { script.Path, context, state });
|
||||
this.intInterpreter.Invoke(script.Path, script.Params, context, state, properties);
|
||||
this.log.Debug("Internal script complete", () => {});
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,9 +2,11 @@
|
|||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Diagnostics;
|
||||
using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Exceptions;
|
||||
using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.IotHub;
|
||||
using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Models;
|
||||
using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.StorageAdapter;
|
||||
using Newtonsoft.Json;
|
||||
|
@ -13,12 +15,40 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services
|
|||
{
|
||||
public interface ISimulations
|
||||
{
|
||||
/// <summary>
|
||||
/// Get list of simulations.
|
||||
/// </summary>
|
||||
Task<IList<Models.Simulation>> GetListAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Get a simulation.
|
||||
/// </summary>
|
||||
Task<Models.Simulation> GetAsync(string id);
|
||||
|
||||
/// <summary>
|
||||
/// Create a simulation.
|
||||
/// </summary>
|
||||
Task<Models.Simulation> InsertAsync(Models.Simulation simulation, string template = "");
|
||||
|
||||
/// <summary>
|
||||
/// Create or Replace a simulation.
|
||||
/// </summary>
|
||||
Task<Models.Simulation> UpsertAsync(Models.Simulation simulation);
|
||||
|
||||
/// <summary>
|
||||
/// Modify a simulation.
|
||||
/// </summary>
|
||||
Task<Models.Simulation> MergeAsync(SimulationPatch patch);
|
||||
|
||||
/// <summary>
|
||||
/// Delete a simulation and its devices.
|
||||
/// </summary>
|
||||
Task DeleteAsync(string id);
|
||||
|
||||
/// <summary>
|
||||
/// Get the ID of the devices in a simulation.
|
||||
/// </summary>
|
||||
IEnumerable<string> GetDeviceIds(Models.Simulation simulation);
|
||||
}
|
||||
|
||||
public class Simulations : ISimulations
|
||||
|
@ -29,18 +59,27 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services
|
|||
|
||||
private readonly IDeviceModels deviceModels;
|
||||
private readonly IStorageAdapterClient storage;
|
||||
private readonly IIotHubConnectionStringManager connectionStringManager;
|
||||
private readonly IDevices devices;
|
||||
private readonly ILogger log;
|
||||
|
||||
public Simulations(
|
||||
IDeviceModels deviceModels,
|
||||
IStorageAdapterClient storage,
|
||||
IIotHubConnectionStringManager connectionStringManager,
|
||||
IDevices devices,
|
||||
ILogger logger)
|
||||
{
|
||||
this.deviceModels = deviceModels;
|
||||
this.storage = storage;
|
||||
this.connectionStringManager = connectionStringManager;
|
||||
this.devices = devices;
|
||||
this.log = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get list of simulations.
|
||||
/// </summary>
|
||||
public async Task<IList<Models.Simulation>> GetListAsync()
|
||||
{
|
||||
var data = await this.storage.GetAllAsync(STORAGE_COLLECTION);
|
||||
|
@ -48,21 +87,27 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services
|
|||
foreach (var item in data.Items)
|
||||
{
|
||||
var simulation = JsonConvert.DeserializeObject<Models.Simulation>(item.Data);
|
||||
simulation.Etag = item.ETag;
|
||||
simulation.ETag = item.ETag;
|
||||
result.Add(simulation);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a simulation.
|
||||
/// </summary>
|
||||
public async Task<Models.Simulation> GetAsync(string id)
|
||||
{
|
||||
var item = await this.storage.GetAsync(STORAGE_COLLECTION, id);
|
||||
var simulation = JsonConvert.DeserializeObject<Models.Simulation>(item.Data);
|
||||
simulation.Etag = item.ETag;
|
||||
simulation.ETag = item.ETag;
|
||||
return simulation;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a simulation.
|
||||
/// </summary>
|
||||
public async Task<Models.Simulation> InsertAsync(Models.Simulation simulation, string template = "")
|
||||
{
|
||||
// TODO: complete validation
|
||||
|
@ -101,6 +146,12 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services
|
|||
}
|
||||
}
|
||||
|
||||
// TODO if write to storage adapter fails, the iothub connection string
|
||||
// will still be stored to disk. Storing the encrypted string using
|
||||
// storage adapter would address this
|
||||
// https://github.com/Azure/device-simulation-dotnet/issues/129
|
||||
simulation.IotHubConnectionString = await this.connectionStringManager.RedactAndStoreAsync(simulation.IotHubConnectionString);
|
||||
|
||||
// Note: using UpdateAsync because the service generates the ID
|
||||
var result = await this.storage.UpdateAsync(
|
||||
STORAGE_COLLECTION,
|
||||
|
@ -108,14 +159,14 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services
|
|||
JsonConvert.SerializeObject(simulation),
|
||||
"*");
|
||||
|
||||
simulation.Etag = result.ETag;
|
||||
simulation.ETag = result.ETag;
|
||||
|
||||
return simulation;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Upsert the simulation. The logic works under the assumption that
|
||||
/// there is only one simulation with id "1".
|
||||
/// Create or Replace a simulation.
|
||||
/// The logic works under the assumption that there is only one simulation with id "1".
|
||||
/// </summary>
|
||||
public async Task<Models.Simulation> UpsertAsync(Models.Simulation simulation)
|
||||
{
|
||||
|
@ -130,10 +181,16 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services
|
|||
{
|
||||
this.log.Info("Modifying simulation via PUT.", () => { });
|
||||
|
||||
if (simulation.Etag != simulations[0].Etag)
|
||||
if (simulation.ETag == "*")
|
||||
{
|
||||
this.log.Error("Invalid Etag. Running simulation Etag is:'", () => new { simulations });
|
||||
throw new InvalidInputException("Invalid Etag. Running simulation Etag is:'" + simulations[0].Etag + "'.");
|
||||
simulation.ETag = simulations[0].ETag;
|
||||
this.log.Info("The client used ETag='*' choosing to overwrite the current simulation", () => { });
|
||||
}
|
||||
|
||||
if (simulation.ETag != simulations[0].ETag)
|
||||
{
|
||||
this.log.Error("Invalid ETag. Running simulation ETag is:'", () => new { simulations });
|
||||
throw new ConflictingResourceException("Invalid ETag. Running simulation ETag is:'" + simulations[0].ETag + "'.");
|
||||
}
|
||||
|
||||
simulation.Created = simulations[0].Created;
|
||||
|
@ -151,18 +208,28 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services
|
|||
|
||||
// Note: forcing the ID because only one simulation can be created
|
||||
simulation.Id = SIMULATION_ID;
|
||||
var item = await this.storage.UpdateAsync(
|
||||
|
||||
// TODO if write to storage adapter fails, the iothub connection string
|
||||
// will still be stored to disk. Storing the encrypted string using
|
||||
// storage adapter would address this
|
||||
// https://github.com/Azure/device-simulation-dotnet/issues/129
|
||||
simulation.IotHubConnectionString = await this.connectionStringManager.RedactAndStoreAsync(simulation.IotHubConnectionString);
|
||||
|
||||
var result = await this.storage.UpdateAsync(
|
||||
STORAGE_COLLECTION,
|
||||
SIMULATION_ID,
|
||||
JsonConvert.SerializeObject(simulation),
|
||||
simulation.Etag);
|
||||
simulation.ETag);
|
||||
|
||||
// Return the new etag provided by the storage
|
||||
simulation.Etag = item.ETag;
|
||||
// Return the new ETag provided by the storage
|
||||
simulation.ETag = result.ETag;
|
||||
|
||||
return simulation;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Modify a simulation.
|
||||
/// </summary>
|
||||
public async Task<Models.Simulation> MergeAsync(SimulationPatch patch)
|
||||
{
|
||||
if (patch.Id != SIMULATION_ID)
|
||||
|
@ -173,15 +240,15 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services
|
|||
|
||||
var item = await this.storage.GetAsync(STORAGE_COLLECTION, patch.Id);
|
||||
var simulation = JsonConvert.DeserializeObject<Models.Simulation>(item.Data);
|
||||
simulation.Etag = item.ETag;
|
||||
simulation.ETag = item.ETag;
|
||||
|
||||
// Even when there's nothing to do, verify the etag mismatch
|
||||
if (patch.Etag != simulation.Etag)
|
||||
// Even when there's nothing to do, verify the ETag mismatch
|
||||
if (patch.ETag != simulation.ETag)
|
||||
{
|
||||
this.log.Warn("Etag mismatch",
|
||||
() => new { Current = simulation.Etag, Provided = patch.Etag });
|
||||
this.log.Warn("ETag mismatch",
|
||||
() => new { Current = simulation.ETag, Provided = patch.ETag });
|
||||
throw new ConflictingResourceException(
|
||||
$"The ETag provided doesn't match the current resource ETag ({simulation.Etag}).");
|
||||
$"The ETag provided doesn't match the current resource ETag ({simulation.ETag}).");
|
||||
}
|
||||
|
||||
if (!patch.Enabled.HasValue || patch.Enabled.Value == simulation.Enabled)
|
||||
|
@ -198,16 +265,44 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services
|
|||
STORAGE_COLLECTION,
|
||||
SIMULATION_ID,
|
||||
JsonConvert.SerializeObject(simulation),
|
||||
patch.Etag);
|
||||
patch.ETag);
|
||||
|
||||
simulation.Etag = item.ETag;
|
||||
simulation.ETag = item.ETag;
|
||||
|
||||
return simulation;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Delete a simulation and its devices.
|
||||
/// </summary>
|
||||
public async Task DeleteAsync(string id)
|
||||
{
|
||||
// Delete devices first
|
||||
var deviceIds = this.GetDeviceIds(await this.GetAsync(id));
|
||||
await this.devices.DeleteListAsync(deviceIds);
|
||||
|
||||
// Then delete the simulation from the storage
|
||||
await this.storage.DeleteAsync(STORAGE_COLLECTION, id);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the ID of the devices in a simulation.
|
||||
/// </summary>
|
||||
public IEnumerable<string> GetDeviceIds(Models.Simulation simulation)
|
||||
{
|
||||
var deviceIds = new List<string>();
|
||||
|
||||
// Calculate the device IDs used in the simulation
|
||||
var models = (from model in simulation.DeviceModels where model.Count > 0 select model).ToList();
|
||||
foreach (var model in models)
|
||||
{
|
||||
for (var i = 0; i < model.Count; i++)
|
||||
{
|
||||
deviceIds.Add(this.devices.GenerateId(model.Id, i));
|
||||
}
|
||||
}
|
||||
|
||||
return deviceIds;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,7 +19,7 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.StorageAdapter
|
|||
Task<ValueListApiModel> GetAllAsync(string collectionId);
|
||||
Task<ValueApiModel> GetAsync(string collectionId, string key);
|
||||
Task<ValueApiModel> CreateAsync(string collectionId, string value);
|
||||
Task<ValueApiModel> UpdateAsync(string collectionId, string key, string value, string etag);
|
||||
Task<ValueApiModel> UpdateAsync(string collectionId, string key, string value, string eTag);
|
||||
Task DeleteAsync(string collectionId, string key);
|
||||
}
|
||||
|
||||
|
@ -107,11 +107,11 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.StorageAdapter
|
|||
return JsonConvert.DeserializeObject<ValueApiModel>(response.Content);
|
||||
}
|
||||
|
||||
public async Task<ValueApiModel> UpdateAsync(string collectionId, string key, string value, string etag)
|
||||
public async Task<ValueApiModel> UpdateAsync(string collectionId, string key, string value, string eTag)
|
||||
{
|
||||
var response = await this.httpClient.PutAsync(
|
||||
this.PrepareRequest($"collections/{collectionId}/values/{key}",
|
||||
new ValueApiModel { Data = value, ETag = etag }));
|
||||
new ValueApiModel { Data = value, ETag = eTag }));
|
||||
|
||||
this.log.Debug("Storage response", () => new { response });
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
"Version": "0.0.1",
|
||||
"Name": "Chiller",
|
||||
"Description": "Chiller with external temperature, humidity and pressure sensors.",
|
||||
"Protocol": "MQTT",
|
||||
"Protocol": "AMQP",
|
||||
"Simulation": {
|
||||
"InitialState": {
|
||||
"online": true,
|
||||
|
@ -16,11 +16,13 @@
|
|||
"pressure_unit": "psig",
|
||||
"simulation_state": "normal_pressure"
|
||||
},
|
||||
"Script": {
|
||||
"Type": "javascript",
|
||||
"Path": "chiller-01-state.js",
|
||||
"Interval": "00:00:05"
|
||||
}
|
||||
"Interval": "00:00:10",
|
||||
"Scripts": [
|
||||
{
|
||||
"Type": "javascript",
|
||||
"Path": "chiller-01-state.js"
|
||||
}
|
||||
]
|
||||
},
|
||||
"Properties": {
|
||||
"Type": "Chiller",
|
||||
|
|
|
@ -16,11 +16,13 @@
|
|||
"pressure_unit": "psig",
|
||||
"simulation_state": "high_pressure"
|
||||
},
|
||||
"Script": {
|
||||
"Type": "javascript",
|
||||
"Path": "chiller-02-state.js",
|
||||
"Interval": "00:00:05"
|
||||
}
|
||||
"Interval": "00:00:10",
|
||||
"Scripts": [
|
||||
{
|
||||
"Type": "javascript",
|
||||
"Path": "chiller-02-state.js"
|
||||
}
|
||||
]
|
||||
},
|
||||
"Properties": {
|
||||
"Type": "Chiller",
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
"Version": "0.0.1",
|
||||
"Name": "Elevator",
|
||||
"Description": "Elevator with floor, vibration and temperature sensors.",
|
||||
"Protocol": "MQTT",
|
||||
"Protocol": "AMQP",
|
||||
"Simulation": {
|
||||
"InitialState": {
|
||||
"online": true,
|
||||
|
@ -14,11 +14,13 @@
|
|||
"temperature": 75.0,
|
||||
"temperature_unit": "F"
|
||||
},
|
||||
"Script": {
|
||||
"Type": "javascript",
|
||||
"Path": "elevator-01-state.js",
|
||||
"Interval": "00:00:05"
|
||||
}
|
||||
"Interval": "00:00:10",
|
||||
"Scripts": [
|
||||
{
|
||||
"Type": "javascript",
|
||||
"Path": "elevator-01-state.js"
|
||||
}
|
||||
]
|
||||
},
|
||||
"Properties": {
|
||||
"Type": "Elevator",
|
||||
|
@ -43,18 +45,18 @@
|
|||
}
|
||||
}
|
||||
],
|
||||
"CloudToDeviceMethods": {
|
||||
"FirmwareUpdate": {
|
||||
"Type": "javascript",
|
||||
"Path": "FirmwareUpdate-method.js"
|
||||
},
|
||||
"StopElevator": {
|
||||
"Type": "javascript",
|
||||
"Path": "TBD.js"
|
||||
},
|
||||
"StartElevator": {
|
||||
"Type": "javascript",
|
||||
"Path": "TBD.js"
|
||||
}
|
||||
}
|
||||
"CloudToDeviceMethods": {
|
||||
"FirmwareUpdate": {
|
||||
"Type": "javascript",
|
||||
"Path": "FirmwareUpdate-method.js"
|
||||
},
|
||||
"StopElevator": {
|
||||
"Type": "javascript",
|
||||
"Path": "StopeElevator-method.js"
|
||||
},
|
||||
"StartElevator": {
|
||||
"Type": "javascript",
|
||||
"Path": "StartElevator-method.js"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -14,11 +14,13 @@
|
|||
"temperature": 75.0,
|
||||
"temperature_unit": "F"
|
||||
},
|
||||
"Script": {
|
||||
"Type": "javascript",
|
||||
"Path": "elevator-02-state.js",
|
||||
"Interval": "00:00:05"
|
||||
}
|
||||
"Interval": "00:00:10",
|
||||
"Scripts": [
|
||||
{
|
||||
"Type": "javascript",
|
||||
"Path": "elevator-02-state.js"
|
||||
}
|
||||
]
|
||||
},
|
||||
"Properties": {
|
||||
"Type": "Elevator",
|
||||
|
@ -43,18 +45,18 @@
|
|||
}
|
||||
}
|
||||
],
|
||||
"CloudToDeviceMethods": {
|
||||
"FirmwareUpdate": {
|
||||
"Type": "javascript",
|
||||
"Path": "FirmwareUpdate-method.js"
|
||||
},
|
||||
"StopElevator": {
|
||||
"Type": "javascript",
|
||||
"Path": "TBD.js"
|
||||
},
|
||||
"StartElevator": {
|
||||
"Type": "javascript",
|
||||
"Path": "TBD.js"
|
||||
"CloudToDeviceMethods": {
|
||||
"FirmwareUpdate": {
|
||||
"Type": "javascript",
|
||||
"Path": "FirmwareUpdate-method.js"
|
||||
},
|
||||
"StopElevator": {
|
||||
"Type": "javascript",
|
||||
"Path": "StopElevator-method.js"
|
||||
},
|
||||
"StartElevator": {
|
||||
"Type": "javascript",
|
||||
"Path": "StartElevator-method.js"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -15,11 +15,14 @@
|
|||
"vibration": 10.0,
|
||||
"vibration_unit": "mm"
|
||||
},
|
||||
"Script": {
|
||||
"Type": "javascript",
|
||||
"Path": "engine-01-state.js",
|
||||
"Interval": "00:00:05"
|
||||
}
|
||||
"Interval": "00:00:10",
|
||||
"Scripts": [
|
||||
{
|
||||
"Type": "javascript",
|
||||
"Path": "engine-01-state.js"
|
||||
|
||||
}
|
||||
]
|
||||
},
|
||||
"Properties": {
|
||||
"Type": "Engine",
|
||||
|
@ -45,22 +48,22 @@
|
|||
}
|
||||
}
|
||||
],
|
||||
"CloudToDeviceMethods": {
|
||||
"FirmwareUpdate": {
|
||||
"Type": "javascript",
|
||||
"Path": "FirmwareUpdate-method.js"
|
||||
},
|
||||
"Restart": {
|
||||
"Type": "javascript",
|
||||
"Path": "TBD.js"
|
||||
},
|
||||
"EmptyTank": {
|
||||
"Type": "javascript",
|
||||
"Path": "TBD.js"
|
||||
},
|
||||
"FillTank": {
|
||||
"Type": "javascript",
|
||||
"Path": "TBD.js"
|
||||
}
|
||||
}
|
||||
"CloudToDeviceMethods": {
|
||||
"FirmwareUpdate": {
|
||||
"Type": "javascript",
|
||||
"Path": "FirmwareUpdate-method.js"
|
||||
},
|
||||
"Restart": {
|
||||
"Type": "javascript",
|
||||
"Path": "RestartEngine-method.js"
|
||||
},
|
||||
"EmptyTank": {
|
||||
"Type": "javascript",
|
||||
"Path": "EmptyTank-method.js"
|
||||
},
|
||||
"FillTank": {
|
||||
"Type": "javascript",
|
||||
"Path": "FillTank-method.js"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -15,11 +15,13 @@
|
|||
"vibration": 0.0,
|
||||
"vibration_unit": "mm"
|
||||
},
|
||||
"Script": {
|
||||
"Type": "javascript",
|
||||
"Path": "engine-02-state.js",
|
||||
"Interval": "00:00:05"
|
||||
}
|
||||
"Interval": "00:00:10",
|
||||
"Scripts": [
|
||||
{
|
||||
"Type": "javascript",
|
||||
"Path": "engine-02-state.js"
|
||||
}
|
||||
]
|
||||
},
|
||||
"Properties": {
|
||||
"Type": "Engine",
|
||||
|
@ -45,22 +47,22 @@
|
|||
}
|
||||
}
|
||||
],
|
||||
"CloudToDeviceMethods": {
|
||||
"FirmwareUpdate": {
|
||||
"Type": "javascript",
|
||||
"Path": "FirmwareUpdate-method.js"
|
||||
},
|
||||
"Restart": {
|
||||
"Type": "javascript",
|
||||
"Path": "TBD.js"
|
||||
},
|
||||
"EmptyTank": {
|
||||
"Type": "javascript",
|
||||
"Path": "TBD.js"
|
||||
},
|
||||
"FillTank": {
|
||||
"Type": "javascript",
|
||||
"Path": "TBD.js"
|
||||
}
|
||||
}
|
||||
"CloudToDeviceMethods": {
|
||||
"FirmwareUpdate": {
|
||||
"Type": "javascript",
|
||||
"Path": "FirmwareUpdate-method.js"
|
||||
},
|
||||
"Restart": {
|
||||
"Type": "javascript",
|
||||
"Path": "RestartEngine-method.js"
|
||||
},
|
||||
"EmptyTank": {
|
||||
"Type": "javascript",
|
||||
"Path": "EmptyTank-method.js"
|
||||
},
|
||||
"FillTank": {
|
||||
"Type": "javascript",
|
||||
"Path": "FillTank-method.js"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -16,11 +16,13 @@
|
|||
"longitude": -122.204184,
|
||||
"moving": false
|
||||
},
|
||||
"Script": {
|
||||
"Type": "javascript",
|
||||
"Path": "prototype-01-state.js",
|
||||
"Interval": "00:00:05"
|
||||
}
|
||||
"Interval": "00:00:10",
|
||||
"Scripts": [
|
||||
{
|
||||
"Type": "javascript",
|
||||
"Path": "prototype-01-state.js"
|
||||
}
|
||||
]
|
||||
},
|
||||
"Properties": {
|
||||
"Type": "Prototyping",
|
||||
|
@ -54,24 +56,24 @@
|
|||
"Path": "Reboot-method.js"
|
||||
},
|
||||
"FirmwareUpdate": {
|
||||
"Type": "javascript",
|
||||
"Path": "FirmwareUpdate-method.js"
|
||||
"Type": "javascript",
|
||||
"Path": "FirmwareUpdate-method.js"
|
||||
},
|
||||
"StartMoving": {
|
||||
"Type": "javascript",
|
||||
"Path": "TBD.js"
|
||||
"Path": "StartMovingDevice-method.js"
|
||||
},
|
||||
"StopMoving": {
|
||||
"Type": "javascript",
|
||||
"Path": "TBD.js"
|
||||
"Path": "StopMovingDevice-method.js"
|
||||
},
|
||||
"DecreaseTemperature": {
|
||||
"Type": "javascript",
|
||||
"Path": "TBD.js"
|
||||
"Path": "TemperatureDecrease-method.js"
|
||||
},
|
||||
"IncreaseTemperature": {
|
||||
"Type": "javascript",
|
||||
"Path": "TBD.js"
|
||||
"Path": "TemperatureIncrease-method.js"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -15,11 +15,13 @@
|
|||
"latitude": 47.620433,
|
||||
"longitude": -122.350987
|
||||
},
|
||||
"Script": {
|
||||
"Type": "javascript",
|
||||
"Path": "prototype-02-state.js",
|
||||
"Interval": "00:00:05"
|
||||
}
|
||||
"Interval": "00:00:10",
|
||||
"Scripts": [
|
||||
{
|
||||
"Type": "javascript",
|
||||
"Path": "prototype-02-state.js"
|
||||
}
|
||||
]
|
||||
},
|
||||
"Properties": {
|
||||
"Type": "Prototyping",
|
||||
|
@ -48,7 +50,7 @@
|
|||
}
|
||||
],
|
||||
"CloudToDeviceMethods": {
|
||||
"Reboot": {
|
||||
"Reboot": {
|
||||
"Type": "javascript",
|
||||
"Path": "Reboot-method.js"
|
||||
},
|
||||
|
@ -58,11 +60,11 @@
|
|||
},
|
||||
"DecreaseTemperature": {
|
||||
"Type": "javascript",
|
||||
"Path": "TBD.js"
|
||||
"Path": "TemperatureDecrease-method.js"
|
||||
},
|
||||
"IncreaseTemperature": {
|
||||
"Type": "javascript",
|
||||
"Path": "TBD.js"
|
||||
"Path": "TemperatureIncrease-method.js"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
/*global log*/
|
||||
/*global updateState*/
|
||||
/*global updateProperty*/
|
||||
/*global sleep*/
|
||||
/*jslint node: true*/
|
||||
|
||||
|
@ -12,14 +13,15 @@
|
|||
*
|
||||
* @param context The context contains current time, device model and id
|
||||
* @param previousState The device state since the last iteration
|
||||
* @param previousProperties The device properties since the last iteration
|
||||
*/
|
||||
|
||||
/*jslint unparam: true*/
|
||||
function main(context, previousState) {
|
||||
function main(context, previousState, previousProperties) {
|
||||
|
||||
log("Starting 'Emergency Valve Release' method simulation");
|
||||
|
||||
var state = {
|
||||
var state = {
|
||||
simulation_state: "normal_pressure",
|
||||
pressure: 150
|
||||
};
|
||||
|
|
|
@ -2,14 +2,23 @@
|
|||
|
||||
/*global log*/
|
||||
/*global updateState*/
|
||||
/*global updateProperty*/
|
||||
/*global sleep*/
|
||||
/*jslint node: true*/
|
||||
/*jslint todo: true*/
|
||||
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* Entry point function called by the simulation engine.
|
||||
*
|
||||
* @param context The context contains current time, device model and id
|
||||
* @param previousState The device state since the last iteration
|
||||
* @param previousProperties The device properties since the last iteration
|
||||
*/
|
||||
|
||||
/*jslint unparam: true*/
|
||||
function main(context, previousState) {
|
||||
function main(context, previousState, previousProperties) {
|
||||
|
||||
log("Executing JavaScript EmptyTank function.");
|
||||
|
||||
|
|
|
@ -2,14 +2,23 @@
|
|||
|
||||
/*global log*/
|
||||
/*global updateState*/
|
||||
/*global updateProperty*/
|
||||
/*global sleep*/
|
||||
/*jslint node: true*/
|
||||
/*jslint todo: true*/
|
||||
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* Entry point function called by the simulation engine.
|
||||
*
|
||||
* @param context The context contains current time, device model and id
|
||||
* @param previousState The device state since the last iteration
|
||||
* @param previousProperties The device properties since the last iteration
|
||||
*/
|
||||
|
||||
/*jslint unparam: true*/
|
||||
function main(context, previousState) {
|
||||
function main(context, previousState, previousProperties) {
|
||||
|
||||
log("Executing JavaScript FillTank function.");
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
/*global log*/
|
||||
/*global updateState*/
|
||||
/*global updateProperty*/
|
||||
/*global sleep*/
|
||||
/*jslint node: true*/
|
||||
|
||||
|
@ -9,54 +10,87 @@
|
|||
|
||||
// Default state
|
||||
var state = {
|
||||
online: true,
|
||||
online: true
|
||||
};
|
||||
|
||||
// Default device properties
|
||||
var properties = {
|
||||
Firmware: "1.0.0",
|
||||
DeviceMethodStatus: "Updating Firmware"
|
||||
};
|
||||
|
||||
/**
|
||||
* Restore the global state using data from the previous iteration.
|
||||
*
|
||||
* @param previousState device state from the previous iteration
|
||||
* @param previousProperties device properties from the previous iteration
|
||||
*/
|
||||
function restoreSimulation(previousState, previousProperties) {
|
||||
// If the previous state is null, force a default state
|
||||
if (previousState) {
|
||||
state = previousState;
|
||||
} else {
|
||||
log("Using default state");
|
||||
}
|
||||
|
||||
if (previousProperties) {
|
||||
properties = previousProperties;
|
||||
} else {
|
||||
log("Using default properties");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Entry point function called by the simulation engine.
|
||||
*
|
||||
* @param context The context contains current time, device model and id, not used
|
||||
* @param previousState The device state since the last iteration, not used
|
||||
* @param previousProperties The device properties since the last iteration
|
||||
*/
|
||||
|
||||
/*jslint unparam: true*/
|
||||
function main(context, previousState) {
|
||||
function main(context, previousState, previousProperties) {
|
||||
|
||||
// Restore the global device properties and the global state before
|
||||
// generating the new telemetry, so that the telemetry can apply changes
|
||||
// using the previous function state.
|
||||
restoreSimulation(previousState, previousProperties);
|
||||
|
||||
// Reboot - devices goes offline and comes online after 20 seconds
|
||||
log("Executing firmware update simulation function, firmware version passed:" + context.Firmware);
|
||||
|
||||
// update the state to offline & firmware updating
|
||||
var DeviceMethodStatusKey = "DeviceMethodStatus";
|
||||
var FirmwareKey = "Firmware";
|
||||
|
||||
// update the status to offline & firmware updating
|
||||
state.online = false;
|
||||
state.CalculateRandomizedTelemetry = false;
|
||||
var status = "Command received, updating firmware version to ";
|
||||
status = status.concat(context.Firmware);
|
||||
state.DeviceMethodStatus = status;
|
||||
updateState(state);
|
||||
updateProperty(DeviceMethodStatusKey, status);
|
||||
sleep(5000);
|
||||
|
||||
log("Image Downloading...");
|
||||
state.DeviceMethodStatus = "Image Downloading...";
|
||||
updateState(state);
|
||||
status = "Image Downloading...";
|
||||
updateProperty(DeviceMethodStatusKey, status);
|
||||
sleep(7500);
|
||||
|
||||
log("Executing firmware update simulation function, firmware version passed:" + context.Firmware);
|
||||
state.DeviceMethodStatus = "Downloaded, applying firmware...";
|
||||
updateState(state);
|
||||
status = "Downloaded, applying firmware...";
|
||||
updateProperty(DeviceMethodStatusKey, status);
|
||||
sleep(5000);
|
||||
|
||||
state.DeviceMethodStatus = "Rebooting...";
|
||||
updateState(state);
|
||||
status = "Rebooting...";
|
||||
updateProperty(DeviceMethodStatusKey, status);
|
||||
sleep(5000);
|
||||
|
||||
state.DeviceMethodStatus = "Firmware Updated.";
|
||||
state.Firmware = context.Firmware;
|
||||
updateState(state);
|
||||
status = "Firmware Updated.";
|
||||
updateProperty(DeviceMethodStatusKey, status);
|
||||
properties.Firmware = context.Firmware;
|
||||
updateProperty(FirmwareKey, context.Firmware);
|
||||
sleep(7500);
|
||||
|
||||
state.CalculateRandomizedTelemetry = true;
|
||||
state.online = true;
|
||||
state.DeviceMethodStatus = "";
|
||||
updateState(state);
|
||||
|
||||
}
|
||||
|
|
|
@ -12,10 +12,11 @@
|
|||
*
|
||||
* @param context The context contains current time, device model and id, not used
|
||||
* @param previousState The device state since the last iteration, not used
|
||||
* @param previousProperties The device properties since the last iteration
|
||||
*/
|
||||
|
||||
/*jslint unparam: true*/
|
||||
function main(context, previousState) {
|
||||
function main(context, previousState, previousProperties) {
|
||||
|
||||
log("Starting 'Increase Pressure' method simulation (5 seconds)");
|
||||
|
||||
|
|
|
@ -1,18 +0,0 @@
|
|||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
/*global log*/
|
||||
/*global updateState*/
|
||||
/*global sleep*/
|
||||
/*jslint node: true*/
|
||||
/*jslint todo: true*/
|
||||
|
||||
"use strict";
|
||||
|
||||
/*jslint unparam: true*/
|
||||
function main(context, previousState) {
|
||||
|
||||
log("Executing JavaScript MoveDevice function.");
|
||||
|
||||
// TODO: This method is not implemented
|
||||
// https://github.com/Azure/device-simulation-dotnet/issues/44
|
||||
}
|
|
@ -18,9 +18,11 @@ var state = {
|
|||
*
|
||||
* @param context The context contains current time, device model and id
|
||||
* @param previousState The device state since the last iteration
|
||||
* @param previousProperties The device properties since the last iteration
|
||||
*/
|
||||
|
||||
/*jslint unparam: true*/
|
||||
function main(context, previousState) {
|
||||
function main(context, previousState, previousProperties) {
|
||||
|
||||
// Reboot - devices goes offline and comes online after 20 seconds
|
||||
log("Executing reboot simulation function.");
|
|
@ -8,8 +8,16 @@
|
|||
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* Entry point function called by the method.
|
||||
*
|
||||
* @param context The context contains current time, device model and id
|
||||
* @param previousState The device state since the last iteration
|
||||
* @param previousProperties The device properties since the last iteration
|
||||
*/
|
||||
|
||||
/*jslint unparam: true*/
|
||||
function main(context, previousState) {
|
||||
function main(context, previousState, previousProperties) {
|
||||
|
||||
log("Executing JavaScript RestartEngine function.");
|
||||
|
||||
|
|
|
@ -1,18 +0,0 @@
|
|||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
/*global log*/
|
||||
/*global updateState*/
|
||||
/*global sleep*/
|
||||
/*jslint node: true*/
|
||||
/*jslint todo: true*/
|
||||
|
||||
"use strict";
|
||||
|
||||
/*jslint unparam: true*/
|
||||
function main(context, previousState) {
|
||||
|
||||
log("Executing JavaScript StartDevice function.");
|
||||
|
||||
// TODO: This method is not implemented
|
||||
// https://github.com/Azure/device-simulation-dotnet/issues/44
|
||||
}
|
|
@ -8,8 +8,16 @@
|
|||
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* Entry point function called by the method.
|
||||
*
|
||||
* @param context The context contains current time, device model and id
|
||||
* @param previousState The device state since the last iteration
|
||||
* @param previousProperties The device properties since the last iteration
|
||||
*/
|
||||
|
||||
/*jslint unparam: true*/
|
||||
function main(context, previousState) {
|
||||
function main(context, previousState, previousProperties) {
|
||||
|
||||
log("Executing JavaScript StartElevator function.");
|
||||
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
/*global log*/
|
||||
/*global updateState*/
|
||||
/*global sleep*/
|
||||
/*jslint node: true*/
|
||||
/*jslint todo: true*/
|
||||
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* Entry point function called by the method.
|
||||
*
|
||||
* @param context The context contains current time, device model and id
|
||||
* @param previousState The device state since the last iteration
|
||||
* @param previousProperties The device properties since the last iteration
|
||||
*/
|
||||
|
||||
/*jslint unparam: true*/
|
||||
function main(context, previousState, previousProperties) {
|
||||
|
||||
log("Executing JavaScript StartMoving function.");
|
||||
|
||||
// TODO: This method is not implemented
|
||||
// https://github.com/Azure/device-simulation-dotnet/issues/44
|
||||
}
|
|
@ -1,18 +0,0 @@
|
|||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
/*global log*/
|
||||
/*global updateState*/
|
||||
/*global sleep*/
|
||||
/*jslint node: true*/
|
||||
/*jslint todo: true*/
|
||||
|
||||
"use strict";
|
||||
|
||||
/*jslint unparam: true*/
|
||||
function main(context, previousState) {
|
||||
|
||||
log("Executing JavaScript StopDevice function.");
|
||||
|
||||
// TODO: This method is not implemented
|
||||
// https://github.com/Azure/device-simulation-dotnet/issues/44
|
||||
}
|
|
@ -8,8 +8,16 @@
|
|||
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* Entry point function called by the method.
|
||||
*
|
||||
* @param context The context contains current time, device model and id
|
||||
* @param previousState The device state since the last iteration
|
||||
* @param previousProperties The device properties since the last iteration
|
||||
*/
|
||||
|
||||
/*jslint unparam: true*/
|
||||
function main(context, previousState) {
|
||||
function main(context, previousState, previousProperties) {
|
||||
|
||||
log("Executing JavaScript StopElevator function.");
|
||||
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
/*global log*/
|
||||
/*global updateState*/
|
||||
/*global sleep*/
|
||||
/*jslint node: true*/
|
||||
/*jslint todo: true*/
|
||||
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* Entry point function called by the method.
|
||||
*
|
||||
* @param context The context contains current time, device model and id
|
||||
* @param previousState The device state since the last iteration
|
||||
* @param previousProperties The device properties since the last iteration
|
||||
*/
|
||||
|
||||
/*jslint unparam: true*/
|
||||
function main(context, previousState, previousProperties) {
|
||||
|
||||
log("Executing JavaScript StopMoving function.");
|
||||
|
||||
// TODO: This method is not implemented
|
||||
// https://github.com/Azure/device-simulation-dotnet/issues/44
|
||||
}
|
|
@ -8,8 +8,16 @@
|
|||
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* Entry point function called by the method.
|
||||
*
|
||||
* @param context The context contains current time, device model and id
|
||||
* @param previousState The device state since the last iteration
|
||||
* @param previousProperties The device properties since the last iteration
|
||||
*/
|
||||
|
||||
/*jslint unparam: true*/
|
||||
function main(context, previousState) {
|
||||
function main(context, previousState, previousProperties) {
|
||||
|
||||
log("Executing JavaScript TempDecrease function.");
|
||||
|
||||
|
|
|
@ -8,8 +8,16 @@
|
|||
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* Entry point function called by the method.
|
||||
*
|
||||
* @param context The context contains current time, device model and id
|
||||
* @param previousState The device state since the last iteration
|
||||
* @param previousProperties The device properties since the last iteration
|
||||
*/
|
||||
|
||||
/*jslint unparam: true*/
|
||||
function main(context, previousState) {
|
||||
function main(context, previousState, previousProperties) {
|
||||
|
||||
log("Executing JavaScript TempIncrease function.");
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
/*global log*/
|
||||
/*global updateState*/
|
||||
/*global updateProperty*/
|
||||
/*global sleep*/
|
||||
/*jslint node: true*/
|
||||
|
||||
|
@ -19,23 +20,35 @@ var state = {
|
|||
simulation_state: "normal_pressure"
|
||||
};
|
||||
|
||||
// Default device properties
|
||||
var properties = {};
|
||||
|
||||
/**
|
||||
* Restore the global state using data from the previous iteration.
|
||||
*
|
||||
* @param previousState The output of main() from the previous iteration
|
||||
* @param previousState device state from the previous iteration
|
||||
* @param previousProperties device properties from the previous iteration
|
||||
*/
|
||||
function restoreState(previousState) {
|
||||
function restoreSimulation(previousState, previousProperties) {
|
||||
// If the previous state is null, force a default state
|
||||
if (previousState !== undefined && previousState !== null) {
|
||||
if (previousState) {
|
||||
state = previousState;
|
||||
} else {
|
||||
log("Using default state");
|
||||
}
|
||||
|
||||
if (previousProperties) {
|
||||
properties = previousProperties;
|
||||
} else {
|
||||
log("Using default properties");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple formula generating a random value around the average
|
||||
* in between min and max
|
||||
*
|
||||
* @returns random value with given parameters
|
||||
*/
|
||||
function vary(avg, percentage, min, max) {
|
||||
var value = avg * (1 + ((percentage / 100) * (2 * Math.random() - 1)));
|
||||
|
@ -46,16 +59,20 @@ function vary(avg, percentage, min, max) {
|
|||
|
||||
/**
|
||||
* Entry point function called by the simulation engine.
|
||||
* Returns updated simulation state.
|
||||
* Device property updates must call updateProperties() to persist.
|
||||
*
|
||||
* @param context The context contains current time, device model and id
|
||||
* @param previousState The device state since the last iteration
|
||||
* @param context The context contains current time, device model and id
|
||||
* @param previousState The device state since the last iteration
|
||||
* @param previousProperties The device properties since the last iteration
|
||||
*/
|
||||
/*jslint unparam: true*/
|
||||
function main(context, previousState) {
|
||||
function main(context, previousState, previousProperties) {
|
||||
|
||||
// Restore the global state before generating the new telemetry, so that
|
||||
// the telemetry can apply changes using the previous function state.
|
||||
restoreState(previousState);
|
||||
// Restore the global device properties and the global state before
|
||||
// generating the new telemetry, so that the telemetry can apply changes
|
||||
// using the previous function state.
|
||||
restoreSimulation(previousState, previousProperties);
|
||||
|
||||
// 75F +/- 5%, Min 25F, Max 100F
|
||||
state.temperature = vary(75, 5, 25, 100);
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
/*global log*/
|
||||
/*global updateState*/
|
||||
/*global updateProperty*/
|
||||
/*global sleep*/
|
||||
/*jslint node: true*/
|
||||
|
||||
"use strict";
|
||||
|
@ -17,23 +20,35 @@ var state = {
|
|||
simulation_state: "high_pressure"
|
||||
};
|
||||
|
||||
// Default device properties
|
||||
var properties = {};
|
||||
|
||||
/**
|
||||
* Restore the global state using data from the previous iteration.
|
||||
*
|
||||
* @param previousState The output of main() from the previous iteration
|
||||
* @param previousState device state from the previous iteration
|
||||
* @param previousProperties device properties from the previous iteration
|
||||
*/
|
||||
function restoreState(previousState) {
|
||||
function restoreSimulation(previousState, previousProperties) {
|
||||
// If the previous state is null, force a default state
|
||||
if (previousState !== undefined && previousState !== null) {
|
||||
if (previousState) {
|
||||
state = previousState;
|
||||
} else {
|
||||
log("Using default state");
|
||||
}
|
||||
|
||||
if (previousProperties) {
|
||||
properties = previousProperties;
|
||||
} else {
|
||||
log("Using default properties");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple formula generating a random value around the average
|
||||
* in between min and max
|
||||
*
|
||||
* @returns random value with given parameters
|
||||
*/
|
||||
function vary(avg, percentage, min, max) {
|
||||
var value = avg * (1 + ((percentage / 100) * (2 * Math.random() - 1)));
|
||||
|
@ -44,16 +59,20 @@ function vary(avg, percentage, min, max) {
|
|||
|
||||
/**
|
||||
* Entry point function called by the simulation engine.
|
||||
* Returns updated simulation state.
|
||||
* Device property updates must call updateProperties() to persist.
|
||||
*
|
||||
* @param context The context contains current time, device model and id
|
||||
* @param previousState The device state since the last iteration
|
||||
* @param context The context contains current time, device model and id
|
||||
* @param previousState The device state since the last iteration
|
||||
* @param previousProperties The device properties since the last iteration
|
||||
*/
|
||||
/*jslint unparam: true*/
|
||||
function main(context, previousState) {
|
||||
function main(context, previousState, previousProperties) {
|
||||
|
||||
// Restore the global state before generating the new telemetry, so that
|
||||
// the telemetry can apply changes using the previous function state.
|
||||
restoreState(previousState);
|
||||
// Restore the global device properties and the global state before
|
||||
// generating the new telemetry, so that the telemetry can apply changes
|
||||
// using the previous function state.
|
||||
restoreSimulation(previousState, previousProperties);
|
||||
|
||||
// 75F +/- 5%, Min 25F, Max 100F
|
||||
state.temperature = vary(75, 5, 25, 100);
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
/*global log*/
|
||||
/*global updateState*/
|
||||
/*global updateProperty*/
|
||||
/*jslint node: true*/
|
||||
|
||||
"use strict";
|
||||
|
@ -18,18 +20,28 @@ var state = {
|
|||
moving: true
|
||||
};
|
||||
|
||||
// Default properties
|
||||
var properties = {};
|
||||
|
||||
/**
|
||||
* Restore the global state using data from the previous iteration.
|
||||
*
|
||||
* @param previousState The output of main() from the previous iteration
|
||||
* @param previousState device state from the previous iteration
|
||||
* @param previousProperties device properties from the previous iteration
|
||||
*/
|
||||
function restoreState(previousState) {
|
||||
function restoreSimulation(previousState, previousProperties) {
|
||||
// If the previous state is null, force a default state
|
||||
if (previousState !== undefined && previousState !== null) {
|
||||
if (previousState) {
|
||||
state = previousState;
|
||||
} else {
|
||||
log("Using default state");
|
||||
}
|
||||
|
||||
if (previousProperties) {
|
||||
properties = previousProperties;
|
||||
} else {
|
||||
log("Using default properties");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -58,16 +70,20 @@ function varyfloor(current, min, max) {
|
|||
|
||||
/**
|
||||
* Entry point function called by the simulation engine.
|
||||
* Returns updated simulation state.
|
||||
* Device property updates must call updateProperties() to persist.
|
||||
*
|
||||
* @param context The context contains current time, device model and id
|
||||
* @param previousState The device state since the last iteration
|
||||
* @param context The context contains current time, device model and id
|
||||
* @param previousState The device state since the last iteration
|
||||
* @param previousProperties The device properties since the last iteration
|
||||
*/
|
||||
/*jslint unparam: true*/
|
||||
function main(context, previousState) {
|
||||
function main(context, previousState, previousProperties) {
|
||||
|
||||
// Restore the global state before generating the new telemetry, so that
|
||||
// the telemetry can apply changes using the previous function state.
|
||||
restoreState(previousState);
|
||||
// Restore the global device properties and the global state before
|
||||
// generating the new telemetry, so that the telemetry can apply changes
|
||||
// using the previous function state.
|
||||
restoreSimulation(previousState, previousProperties);
|
||||
|
||||
if (state.moving) {
|
||||
state.floor = varyfloor(state.floor, 1, floors);
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
/*global log*/
|
||||
/*global updateState*/
|
||||
/*global updateProperty*/
|
||||
/*jslint node: true*/
|
||||
|
||||
"use strict";
|
||||
|
@ -18,18 +20,28 @@ var state = {
|
|||
moving: false
|
||||
};
|
||||
|
||||
// Default properties
|
||||
var properties = {};
|
||||
|
||||
/**
|
||||
* Restore the global state using data from the previous iteration.
|
||||
*
|
||||
* @param previousState The output of main() from the previous iteration
|
||||
* @param previousState device state from the previous iteration
|
||||
* @param previousProperties device properties from the previous iteration
|
||||
*/
|
||||
function restoreState(previousState) {
|
||||
function restoreSimulation(previousState, previousProperties) {
|
||||
// If the previous state is null, force a default state
|
||||
if (previousState !== undefined && previousState !== null) {
|
||||
if (previousState) {
|
||||
state = previousState;
|
||||
} else {
|
||||
log("Using default state");
|
||||
}
|
||||
|
||||
if (previousProperties) {
|
||||
properties = previousProperties;
|
||||
} else {
|
||||
log("Using default properties");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -58,16 +70,20 @@ function varyfloor(current, min, max) {
|
|||
|
||||
/**
|
||||
* Entry point function called by the simulation engine.
|
||||
* Returns updated simulation state.
|
||||
* Device property updates must call updateProperties() to persist.
|
||||
*
|
||||
* @param context The context contains current time, device model and id
|
||||
* @param previousState The device state since the last iteration
|
||||
* @param context The context contains current time, device model and id
|
||||
* @param previousState The device state since the last iteration
|
||||
* @param previousProperties The device properties since the last iteration
|
||||
*/
|
||||
/*jslint unparam: true*/
|
||||
function main(context, previousState) {
|
||||
function main(context, previousState, previousProperties) {
|
||||
|
||||
// Restore the global state before generating the new telemetry, so that
|
||||
// the telemetry can apply changes using the previous function state.
|
||||
restoreState(previousState);
|
||||
// Restore the global device properties and the global state before
|
||||
// generating the new telemetry, so that the telemetry can apply changes
|
||||
// using the previous function state.
|
||||
restoreSimulation(previousState, previousProperties);
|
||||
|
||||
if (state.moving) {
|
||||
state.floor = varyfloor(state.floor, 1, floors);
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
/*global log*/
|
||||
/*global updateState*/
|
||||
/*global updateProperty*/
|
||||
/*global sleep*/
|
||||
/*jslint node: true*/
|
||||
|
||||
"use strict";
|
||||
|
@ -16,26 +19,38 @@ var state = {
|
|||
vibration_unit: "mm"
|
||||
};
|
||||
|
||||
// Default device properties
|
||||
var properties = {};
|
||||
|
||||
/**
|
||||
* Restore the global state using data from the previous iteration.
|
||||
*
|
||||
* @param previousState The output of main() from the previous iteration
|
||||
* @param previousState device state from the previous iteration
|
||||
* @param previousProperties device properties from the previous iteration
|
||||
*/
|
||||
function restoreState(previousState) {
|
||||
function restoreSimulation(previousState, previousProperties) {
|
||||
// If the previous state is null, force a default state
|
||||
if (previousState !== undefined && previousState !== null) {
|
||||
if (previousState) {
|
||||
state = previousState;
|
||||
} else {
|
||||
log("Using default state");
|
||||
}
|
||||
|
||||
if (previousProperties) {
|
||||
properties = previousProperties;
|
||||
} else {
|
||||
log("Using default properties");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple formula generating a random value around the average
|
||||
* in between min and max
|
||||
*
|
||||
* @returns random value with given parameters
|
||||
*/
|
||||
function vary(avg, percentage, min, max) {
|
||||
var value = avg * (1 + ((percentage / 100) * (2 * Math.random() - 1)));
|
||||
var value = avg * (1 + ((percentage / 100) * (2 * Math.random() - 1)));
|
||||
value = Math.max(value, min);
|
||||
value = Math.min(value, max);
|
||||
return value;
|
||||
|
@ -43,16 +58,20 @@ function vary(avg, percentage, min, max) {
|
|||
|
||||
/**
|
||||
* Entry point function called by the simulation engine.
|
||||
* Returns updated simulation state.
|
||||
* Device property updates must call updateProperties() to persist.
|
||||
*
|
||||
* @param context The context contains current time, device model and id
|
||||
* @param previousState The device state since the last iteration
|
||||
* @param context The context contains current time, device model and id
|
||||
* @param previousState The device state since the last iteration
|
||||
* @param previousProperties The device properties since the last iteration
|
||||
*/
|
||||
/*jslint unparam: true*/
|
||||
function main(context, previousState) {
|
||||
function main(context, previousState, previousProperties) {
|
||||
|
||||
// Restore the global state before generating the new telemetry, so that
|
||||
// the telemetry can apply changes using the previous function state.
|
||||
restoreState(previousState);
|
||||
// Restore the global device properties and the global state before
|
||||
// generating the new telemetry, so that the telemetry can apply changes
|
||||
// using the previous function state.
|
||||
restoreSimulation(previousState, previousProperties);
|
||||
|
||||
// 7500 +/- 5%, Min 200, Max 10000
|
||||
state.coolant = vary(7500, 5, 200, 10000);
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
/*global log*/
|
||||
/*global updateState*/
|
||||
/*global updateProperty*/
|
||||
/*global sleep*/
|
||||
/*jslint node: true*/
|
||||
|
||||
"use strict";
|
||||
|
@ -16,23 +19,35 @@ var state = {
|
|||
vibration_unit: "mm"
|
||||
};
|
||||
|
||||
// Default device properties
|
||||
var properties = {};
|
||||
|
||||
/**
|
||||
* Restore the global state using data from the previous iteration.
|
||||
*
|
||||
* @param previousState The output of main() from the previous iteration
|
||||
* @param previousState device state from the previous iteration
|
||||
* @param previousProperties device properties from the previous iteration
|
||||
*/
|
||||
function restoreState(previousState) {
|
||||
function restoreSimulation(previousState, previousProperties) {
|
||||
// If the previous state is null, force a default state
|
||||
if (previousState !== undefined && previousState !== null) {
|
||||
if (previousState) {
|
||||
state = previousState;
|
||||
} else {
|
||||
log("Using default state");
|
||||
}
|
||||
|
||||
if (previousProperties) {
|
||||
properties = previousProperties;
|
||||
} else {
|
||||
log("Using default properties");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple formula generating a random value around the average
|
||||
* in between min and max
|
||||
*
|
||||
* @returns random value with given parameters
|
||||
*/
|
||||
function vary(avg, percentage, min, max) {
|
||||
var value = avg * (1 + ((percentage / 100) * (2 * Math.random() - 1)));
|
||||
|
@ -43,16 +58,20 @@ function vary(avg, percentage, min, max) {
|
|||
|
||||
/**
|
||||
* Entry point function called by the simulation engine.
|
||||
* Returns updated simulation state.
|
||||
* Device property updates must call updateProperties() to persist.
|
||||
*
|
||||
* @param context The context contains current time, device model and id
|
||||
* @param previousState The device state since the last iteration
|
||||
* @param context The context contains current time, device model and id
|
||||
* @param previousState The device state since the last iteration
|
||||
* @param previousProperties The device properties since the last iteration
|
||||
*/
|
||||
/*jslint unparam: true*/
|
||||
function main(context, previousState) {
|
||||
function main(context, previousState, previousProperties) {
|
||||
|
||||
// Restore the global state before generating the new telemetry, so that
|
||||
// the telemetry can apply changes using the previous function state.
|
||||
restoreState(previousState);
|
||||
// Restore the global device properties and the global state before
|
||||
// generating the new telemetry, so that the telemetry can apply changes
|
||||
// using the previous function state.
|
||||
restoreSimulation(previousState, previousProperties);
|
||||
|
||||
// 7500 +/- 5%, Min 200, Max 10000
|
||||
state.coolant = vary(7500, 5, 200, 10000);
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
/*global log*/
|
||||
/*global updateState*/
|
||||
/*global updateProperty*/
|
||||
/*global sleep*/
|
||||
/*jslint node: true*/
|
||||
|
||||
"use strict";
|
||||
|
@ -20,23 +23,35 @@ var state = {
|
|||
moving: false
|
||||
};
|
||||
|
||||
// Default device properties
|
||||
var properties = {};
|
||||
|
||||
/**
|
||||
* Restore the global state using data from the previous iteration.
|
||||
*
|
||||
* @param previousState The output of main() from the previous iteration
|
||||
* @param previousState device state from the previous iteration
|
||||
* @param previousProperties device properties from the previous iteration
|
||||
*/
|
||||
function restoreState(previousState) {
|
||||
function restoreSimulation(previousState, previousProperties) {
|
||||
// If the previous state is null, force a default state
|
||||
if (previousState !== undefined && previousState !== null) {
|
||||
if (previousState) {
|
||||
state = previousState;
|
||||
} else {
|
||||
log("Using default state");
|
||||
}
|
||||
|
||||
if (previousProperties) {
|
||||
properties = previousProperties;
|
||||
} else {
|
||||
log("Using default properties");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple formula generating a random value around the average
|
||||
* in between min and max
|
||||
*
|
||||
* @returns random value with given parameters
|
||||
*/
|
||||
function vary(avg, percentage, min, max) {
|
||||
var value = avg * (1 + ((percentage / 100) * (2 * Math.random() - 1)));
|
||||
|
@ -60,16 +75,20 @@ function varylocation(latitude, longitude, distance) {
|
|||
|
||||
/**
|
||||
* Entry point function called by the simulation engine.
|
||||
* Returns updated simulation state.
|
||||
* Device property updates must call updateProperties() to persist.
|
||||
*
|
||||
* @param context The context contains current time, device model and id
|
||||
* @param previousState The device state since the last iteration
|
||||
* @param context The context contains current time, device model and id
|
||||
* @param previousState The device state since the last iteration
|
||||
* @param previousProperties The device properties since the last iteration
|
||||
*/
|
||||
/*jslint unparam: true*/
|
||||
function main(context, previousState) {
|
||||
function main(context, previousState, previousProperties) {
|
||||
|
||||
// Restore the global state before generating the new telemetry, so that
|
||||
// the telemetry can apply changes using the previous function state.
|
||||
restoreState(previousState);
|
||||
// Restore the global device properties and the global state before
|
||||
// generating the new telemetry, so that the telemetry can apply changes
|
||||
// using the previous function state.
|
||||
restoreSimulation(previousState, previousProperties);
|
||||
|
||||
// 65 +/- 1%, Min 35, Max 100
|
||||
state.temperature = vary(65, 1, 35, 100);
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
/*global log*/
|
||||
/*global updateState*/
|
||||
/*global updateProperty*/
|
||||
/*global sleep*/
|
||||
/*jslint node: true*/
|
||||
|
||||
"use strict";
|
||||
|
@ -19,23 +22,35 @@ var state = {
|
|||
longitude: center_longitude
|
||||
};
|
||||
|
||||
// Default device properties
|
||||
var properties = {};
|
||||
|
||||
/**
|
||||
* Restore the global state using data from the previous iteration.
|
||||
*
|
||||
* @param previousState The output of main() from the previous iteration
|
||||
* @param previousState device state from the previous iteration
|
||||
* @param previousProperties device properties from the previous iteration
|
||||
*/
|
||||
function restoreState(previousState) {
|
||||
function restoreSimulation(previousState, previousProperties) {
|
||||
// If the previous state is null, force a default state
|
||||
if (previousState !== undefined && previousState !== null) {
|
||||
if (previousState) {
|
||||
state = previousState;
|
||||
} else {
|
||||
log("Using default state");
|
||||
}
|
||||
|
||||
if (previousProperties) {
|
||||
properties = previousProperties;
|
||||
} else {
|
||||
log("Using default properties");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple formula generating a random value around the average
|
||||
* in between min and max
|
||||
*
|
||||
* @returns random value with given parameters
|
||||
*/
|
||||
function vary(avg, percentage, min, max) {
|
||||
var value = avg * (1 + ((percentage / 100) * (2 * Math.random() - 1)));
|
||||
|
@ -46,16 +61,20 @@ function vary(avg, percentage, min, max) {
|
|||
|
||||
/**
|
||||
* Entry point function called by the simulation engine.
|
||||
* Returns updated simulation state.
|
||||
* Device property updates must call updateProperties() to persist.
|
||||
*
|
||||
* @param context The context contains current time, device model and id
|
||||
* @param previousState The device state since the last iteration
|
||||
* @param context The context contains current time, device model and id
|
||||
* @param previousState The device state since the last iteration
|
||||
* @param previousProperties The device properties since the last iteration
|
||||
*/
|
||||
/*jslint unparam: true*/
|
||||
function main(context, previousState) {
|
||||
function main(context, previousState, previousProperties) {
|
||||
|
||||
// Restore the global state before generating the new telemetry, so that
|
||||
// the telemetry can apply changes using the previous function state.
|
||||
restoreState(previousState);
|
||||
// Restore the global device properties and the global state before
|
||||
// generating the new telemetry, so that the telemetry can apply changes
|
||||
// using the previous function state.
|
||||
restoreSimulation(previousState, previousProperties);
|
||||
|
||||
// 85 +/- 25%, Min 35, Max 100
|
||||
state.temperature = vary(85, 25, 35, 100);
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
/*global log*/
|
||||
/*global updateState*/
|
||||
/*global updateProperty*/
|
||||
/*jslint node: true*/
|
||||
|
||||
"use strict";
|
||||
|
@ -19,18 +21,28 @@ var state = {
|
|||
cargotemperature_unit: "F"
|
||||
};
|
||||
|
||||
// Default properties
|
||||
var properties = {};
|
||||
|
||||
/**
|
||||
* Restore the global state using data from the previous iteration.
|
||||
*
|
||||
* @param previousState The output of main() from the previous iteration
|
||||
* @param previousState device state from the previous iteration
|
||||
* @param previousProperties device properties from the previous iteration
|
||||
*/
|
||||
function restoreState(previousState) {
|
||||
function restoreSimulation(previousState, previousProperties) {
|
||||
// If the previous state is null, force a default state
|
||||
if (previousState !== undefined && previousState !== null) {
|
||||
if (previousState) {
|
||||
state = previousState;
|
||||
} else {
|
||||
log("Using default state");
|
||||
}
|
||||
|
||||
if (previousProperties) {
|
||||
properties = previousProperties;
|
||||
} else {
|
||||
log("Using default properties");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -59,16 +71,20 @@ function varylocation(latitude, longitude, distance) {
|
|||
|
||||
/**
|
||||
* Entry point function called by the simulation engine.
|
||||
* Returns updated simulation state.
|
||||
* Device property updates must call updateProperties() to persist.
|
||||
*
|
||||
* @param context The context contains current time, device model and id
|
||||
* @param previousState The device state since the last iteration
|
||||
* @param context The context contains current time, device model and id
|
||||
* @param previousState The device state since the last iteration
|
||||
* @param previousProperties The device properties since the last iteration
|
||||
*/
|
||||
/*jslint unparam: true*/
|
||||
function main(context, previousState) {
|
||||
function main(context, previousState, previousProperties) {
|
||||
|
||||
// Restore the global state before generating the new telemetry, so that
|
||||
// the telemetry can apply changes using the previous function state.
|
||||
restoreState(previousState);
|
||||
// Restore the global device properties and the global state before
|
||||
// generating the new telemetry, so that the telemetry can apply changes
|
||||
// using the previous function state.
|
||||
restoreSimulation(previousState, previousProperties);
|
||||
|
||||
// 0.1 miles around some location
|
||||
var coords = varylocation(center_latitude, center_longitude, 0.1);
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
/*global log*/
|
||||
/*global updateState*/
|
||||
/*global updateProperty*/
|
||||
/*jslint node: true*/
|
||||
|
||||
"use strict";
|
||||
|
@ -19,18 +21,28 @@ var state = {
|
|||
cargotemperature_unit: "F"
|
||||
};
|
||||
|
||||
// Default properties
|
||||
var properties = {};
|
||||
|
||||
/**
|
||||
* Restore the global state using data from the previous iteration.
|
||||
*
|
||||
* @param previousState The output of main() from the previous iteration
|
||||
* @param previousState device state from the previous iteration
|
||||
* @param previousProperties device properties from the previous iteration
|
||||
*/
|
||||
function restoreState(previousState) {
|
||||
function restoreSimulation(previousState, previousProperties) {
|
||||
// If the previous state is null, force a default state
|
||||
if (previousState !== undefined && previousState !== null) {
|
||||
if (previousState) {
|
||||
state = previousState;
|
||||
} else {
|
||||
log("Using default state");
|
||||
}
|
||||
|
||||
if (previousProperties) {
|
||||
properties = previousProperties;
|
||||
} else {
|
||||
log("Using default properties");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -59,16 +71,20 @@ function varylocation(latitude, longitude, distance) {
|
|||
|
||||
/**
|
||||
* Entry point function called by the simulation engine.
|
||||
* Returns updated simulation state.
|
||||
* Device property updates must call updateProperties() to persist.
|
||||
*
|
||||
* @param context The context contains current time, device model and id
|
||||
* @param previousState The device state since the last iteration
|
||||
* @param context The context contains current time, device model and id
|
||||
* @param previousState The device state since the last iteration
|
||||
* @param previousProperties The device properties since the last iteration
|
||||
*/
|
||||
/*jslint unparam: true*/
|
||||
function main(context, previousState) {
|
||||
function main(context, previousState, previousProperties) {
|
||||
|
||||
// Restore the global state before generating the new telemetry, so that
|
||||
// the telemetry can apply changes using the previous function state.
|
||||
restoreState(previousState);
|
||||
// Restore the global device properties and the global state before
|
||||
// generating the new telemetry, so that the telemetry can apply changes
|
||||
// using the previous function state.
|
||||
restoreSimulation(previousState, previousProperties);
|
||||
|
||||
// 0.1 miles around some location
|
||||
var coords = varylocation(center_latitude, center_longitude, 0.1);
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
"Version": "0.0.1",
|
||||
"Name": "Truck",
|
||||
"Description": "Truck with GPS, speed and cargo temperature sensors",
|
||||
"Protocol": "MQTT",
|
||||
"Protocol": "AMQP",
|
||||
"Simulation": {
|
||||
"InitialState": {
|
||||
"online": true,
|
||||
|
@ -15,11 +15,13 @@
|
|||
"cargotemperature": 38.0,
|
||||
"cargotemperature_unit": "F"
|
||||
},
|
||||
"Script": {
|
||||
"Type": "javascript",
|
||||
"Path": "truck-01-state.js",
|
||||
"Interval": "00:00:05"
|
||||
}
|
||||
"Interval": "00:00:10",
|
||||
"Scripts": [
|
||||
{
|
||||
"Type": "javascript",
|
||||
"Path": "truck-01-state.js"
|
||||
}
|
||||
]
|
||||
},
|
||||
"Properties": {
|
||||
"Type": "Truck",
|
||||
|
@ -45,18 +47,18 @@
|
|||
}
|
||||
}
|
||||
],
|
||||
"CloudToDeviceMethods": {
|
||||
"FirmwareUpdate": {
|
||||
"Type": "javascript",
|
||||
"Path": "FirmwareUpdate-method.js"
|
||||
},
|
||||
"DecreaseCargoTemperature": {
|
||||
"Type": "javascript",
|
||||
"Path": "TBD.js"
|
||||
},
|
||||
"IncreaseCargoTemperature": {
|
||||
"Type": "javascript",
|
||||
"Path": "TBD.js"
|
||||
}
|
||||
}
|
||||
"CloudToDeviceMethods": {
|
||||
"FirmwareUpdate": {
|
||||
"Type": "javascript",
|
||||
"Path": "FirmwareUpdate-method.js"
|
||||
},
|
||||
"DecreaseCargoTemperature": {
|
||||
"Type": "javascript",
|
||||
"Path": "TemperatureDecrease-method.js"
|
||||
},
|
||||
"IncreaseCargoTemperature": {
|
||||
"Type": "javascript",
|
||||
"Path": "TemperatureIncrease-method.js"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -15,11 +15,13 @@
|
|||
"cargotemperature": 49.0,
|
||||
"cargotemperature_unit": "F"
|
||||
},
|
||||
"Script": {
|
||||
"Type": "javascript",
|
||||
"Path": "truck-02-state.js",
|
||||
"Interval": "00:00:05"
|
||||
}
|
||||
"Interval": "00:00:10",
|
||||
"Scripts": [
|
||||
{
|
||||
"Type": "javascript",
|
||||
"Path": "truck-02-state.js"
|
||||
}
|
||||
]
|
||||
},
|
||||
"Properties": {
|
||||
"Type": "Truck",
|
||||
|
@ -45,18 +47,18 @@
|
|||
}
|
||||
}
|
||||
],
|
||||
"CloudToDeviceMethods": {
|
||||
"FirmwareUpdate": {
|
||||
"Type": "javascript",
|
||||
"Path": "FirmwareUpdate-method.js"
|
||||
},
|
||||
"DecreaseCargoTemperature": {
|
||||
"Type": "javascript",
|
||||
"Path": "TBD.js"
|
||||
},
|
||||
"IncreaseCargoTemperature": {
|
||||
"Type": "javascript",
|
||||
"Path": "TBD.js"
|
||||
}
|
||||
}
|
||||
"CloudToDeviceMethods": {
|
||||
"FirmwareUpdate": {
|
||||
"Type": "javascript",
|
||||
"Path": "FirmwareUpdate-method.js"
|
||||
},
|
||||
"DecreaseCargoTemperature": {
|
||||
"Type": "javascript",
|
||||
"Path": "TempDecrease-method.js"
|
||||
},
|
||||
"IncreaseCargoTemperature": {
|
||||
"Type": "javascript",
|
||||
"Path": "TempIncrease-method.js"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
This folder is used at run time to store a custom data, like the custom connection string.
|
||||
|
||||
Do not delete this folder, and make sure the configuration points here.
|
|
@ -0,0 +1,156 @@
|
|||
using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services;
|
||||
using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Concurrency;
|
||||
using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Diagnostics;
|
||||
using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Models;
|
||||
using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Simulation;
|
||||
using Microsoft.Azure.IoTSolutions.DeviceSimulation.SimulationAgent.DeviceConnection;
|
||||
using Microsoft.Azure.IoTSolutions.DeviceSimulation.SimulationAgent.DeviceState;
|
||||
using Moq;
|
||||
using SimulationAgent.Test.helpers;
|
||||
using Xunit;
|
||||
|
||||
namespace SimulationAgent.Test.DeviceConnection
|
||||
{
|
||||
public class DeviceConnectionActorTest
|
||||
{
|
||||
private readonly Mock<ILogger> logger;
|
||||
private readonly Mock<IActorsLogger> actorLogger;
|
||||
private readonly Mock<IRateLimitingConfig> rateLimitingConfig;
|
||||
private readonly Mock<IScriptInterpreter> scriptInterpreter;
|
||||
private readonly Mock<IRateLimiting> rateLimiting;
|
||||
private readonly Mock<Fetch> fetchLogic;
|
||||
private readonly Mock<Register> registerLogic;
|
||||
private readonly Mock<DeviceTwinTag> deviceTwinTagLogic;
|
||||
private readonly Mock<IDevices> devices;
|
||||
private readonly Mock<Connect> connectLogic;
|
||||
private readonly Mock<IDeviceStateActor> deviceStateActor;
|
||||
private readonly Mock<ConnectionLoopSettings> loopSettings;
|
||||
private readonly DeviceConnectionActor target;
|
||||
|
||||
public DeviceConnectionActorTest()
|
||||
{
|
||||
this.logger = new Mock<ILogger>();
|
||||
this.actorLogger = new Mock<IActorsLogger>();
|
||||
this.rateLimitingConfig = new Mock<IRateLimitingConfig>();
|
||||
this.scriptInterpreter = new Mock<IScriptInterpreter>();
|
||||
this.devices = new Mock<IDevices>();
|
||||
this.rateLimiting = new Mock<IRateLimiting>();
|
||||
this.fetchLogic = new Mock<Fetch>(
|
||||
this.devices.Object,
|
||||
this.logger.Object);
|
||||
this.registerLogic = new Mock<Register>(
|
||||
this.devices.Object,
|
||||
this.logger.Object);
|
||||
this.deviceTwinTagLogic = new Mock<DeviceTwinTag>(
|
||||
this.devices.Object,
|
||||
this.logger.Object);
|
||||
this.connectLogic = new Mock<Connect>(
|
||||
this.devices.Object,
|
||||
this.scriptInterpreter.Object,
|
||||
this.logger.Object);
|
||||
this.deviceStateActor = new Mock<IDeviceStateActor>();
|
||||
this.loopSettings = new Mock<ConnectionLoopSettings>(
|
||||
this.rateLimitingConfig.Object);
|
||||
|
||||
|
||||
this.rateLimitingConfig.Setup(x => x.DeviceMessagesPerSecond).Returns(10);
|
||||
|
||||
this.target = new DeviceConnectionActor(
|
||||
this.logger.Object,
|
||||
this.actorLogger.Object,
|
||||
this.rateLimiting.Object,
|
||||
this.fetchLogic.Object,
|
||||
this.registerLogic.Object,
|
||||
this.deviceTwinTagLogic.Object,
|
||||
this.connectLogic.Object);
|
||||
}
|
||||
|
||||
[Fact, Trait(Constants.TYPE, Constants.UNIT_TEST)]
|
||||
public void TheNumberOfFailedDeviceConnectionsIsZeroAtStart()
|
||||
{
|
||||
// Arrange
|
||||
this.SetupDeviceConnectionActor();
|
||||
|
||||
// Act
|
||||
long failedDeviceConnectionCount = this.target.FailedDeviceConnectionsCount;
|
||||
|
||||
// Assert
|
||||
Assert.Equal(0, failedDeviceConnectionCount);
|
||||
}
|
||||
|
||||
[Fact, Trait(Constants.TYPE, Constants.UNIT_TEST)]
|
||||
public void ItReturnsTheNumberOfFailedDeviceConnections()
|
||||
{
|
||||
// Arrange
|
||||
const int FAILED_DEVICE_CONNECTIONS_COUNT = 3;
|
||||
this.SetupDeviceConnectionActor();
|
||||
DeviceConnectionActor.ActorEvents connectionFailed = DeviceConnectionActor.ActorEvents.ConnectionFailed;
|
||||
|
||||
// Act
|
||||
for (int i = 0; i < FAILED_DEVICE_CONNECTIONS_COUNT; i++)
|
||||
{
|
||||
this.target.HandleEvent(connectionFailed);
|
||||
}
|
||||
|
||||
long failedDeviceConnectionCount = this.target.FailedDeviceConnectionsCount;
|
||||
|
||||
// Assert
|
||||
Assert.Equal(FAILED_DEVICE_CONNECTIONS_COUNT, failedDeviceConnectionCount);
|
||||
}
|
||||
|
||||
[Fact, Trait(Constants.TYPE, Constants.UNIT_TEST)]
|
||||
public void TheNumberOfFailedTwinUpdatesIsZeroAtStart()
|
||||
{
|
||||
// Arrange
|
||||
this.SetupDeviceConnectionActor();
|
||||
|
||||
// Act
|
||||
long failedTwinUpdateCount = this.target.FailedTwinUpdatesCount;
|
||||
|
||||
// Assert
|
||||
Assert.Equal(0, failedTwinUpdateCount);
|
||||
}
|
||||
|
||||
[Fact, Trait(Constants.TYPE, Constants.UNIT_TEST)]
|
||||
public void ItReturnsTheNumberOfFailedTwinUpdates()
|
||||
{
|
||||
// Arrange
|
||||
const int FAILED_DEVICE_TWIN_UPDATES_COUNT = 5;
|
||||
this.SetupDeviceConnectionActor();
|
||||
DeviceConnectionActor.ActorEvents deviceTwinTaggingFailed = DeviceConnectionActor.ActorEvents.DeviceTwinTaggingFailed;
|
||||
|
||||
// Act
|
||||
for (int i = 0; i < FAILED_DEVICE_TWIN_UPDATES_COUNT; i++)
|
||||
{
|
||||
this.target.HandleEvent(deviceTwinTaggingFailed);
|
||||
}
|
||||
|
||||
long failedTwinUpdateCount = this.target.FailedTwinUpdatesCount;
|
||||
|
||||
// Assert
|
||||
Assert.Equal(FAILED_DEVICE_TWIN_UPDATES_COUNT, failedTwinUpdateCount);
|
||||
}
|
||||
|
||||
private void SetupDeviceConnectionActor()
|
||||
{
|
||||
string DEVICE_ID = "01";
|
||||
var deviceModel = new DeviceModel { Id = DEVICE_ID };
|
||||
var message = new DeviceModel.DeviceModelMessage();
|
||||
|
||||
this.SetupRateLimitingConfig();
|
||||
|
||||
this.target.Setup(
|
||||
DEVICE_ID,
|
||||
deviceModel,
|
||||
this.deviceStateActor.Object,
|
||||
this.loopSettings.Object);
|
||||
}
|
||||
|
||||
private void SetupRateLimitingConfig()
|
||||
{
|
||||
this.rateLimitingConfig
|
||||
.SetupGet(x => x.RegistryOperationsPerMinute)
|
||||
.Returns(1200);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,93 @@
|
|||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Concurrency;
|
||||
using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Diagnostics;
|
||||
using Microsoft.Azure.IoTSolutions.DeviceSimulation.SimulationAgent.DeviceConnection;
|
||||
using Microsoft.Azure.IoTSolutions.DeviceSimulation.SimulationAgent.DeviceProperties;
|
||||
using Microsoft.Azure.IoTSolutions.DeviceSimulation.SimulationAgent.DeviceState;
|
||||
using Microsoft.Azure.IoTSolutions.DeviceSimulation.SimulationAgent.Exceptions;
|
||||
using Moq;
|
||||
using SimulationAgent.Test.helpers;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
using static Microsoft.Azure.IoTSolutions.DeviceSimulation.SimulationAgent.DeviceProperties.DevicePropertiesActor;
|
||||
|
||||
namespace SimulationAgent.Test.DeviceProperties
|
||||
{
|
||||
public class DevicePropertiesActorTest
|
||||
{
|
||||
private readonly Mock<ILogger> logger;
|
||||
|
||||
|
||||
private readonly Mock<IActorsLogger> actorsLogger;
|
||||
private readonly Mock<IRateLimiting> rateLimiting;
|
||||
private readonly Mock<IDevicePropertiesLogic> updatePropertiesLogic;
|
||||
private readonly Mock<IDeviceConnectionActor> deviceConnectionActor;
|
||||
private readonly Mock<IDeviceStateActor> deviceStateActor;
|
||||
|
||||
private DevicePropertiesActor target;
|
||||
|
||||
public DevicePropertiesActorTest(ITestOutputHelper log)
|
||||
{
|
||||
this.logger = new Mock<ILogger>();
|
||||
this.actorsLogger = new Mock<IActorsLogger>();
|
||||
|
||||
this.rateLimiting = new Mock<IRateLimiting>();
|
||||
|
||||
this.deviceConnectionActor = new Mock<IDeviceConnectionActor>();
|
||||
this.deviceStateActor = new Mock<IDeviceStateActor>();
|
||||
this.updatePropertiesLogic = new Mock<IDevicePropertiesLogic>();
|
||||
|
||||
this.CreateNewDevicePropertiesActor();
|
||||
}
|
||||
|
||||
[Fact, Trait(Constants.TYPE, Constants.UNIT_TEST)]
|
||||
public void Setup_Called_Twice_Should_Throw_Already_Initialized_Exception()
|
||||
{
|
||||
// Arrange
|
||||
CreateNewDevicePropertiesActor();
|
||||
|
||||
// Act
|
||||
this.SetupDevicePropertiesActor();
|
||||
|
||||
// Assert
|
||||
Assert.Throws<DeviceActorAlreadyInitializedException>(
|
||||
() => this.SetupDevicePropertiesActor());
|
||||
}
|
||||
|
||||
|
||||
[Fact, Trait(Constants.TYPE, Constants.UNIT_TEST)]
|
||||
public void Handle_Event_Should_Throw_When_Out_Of_Range()
|
||||
{
|
||||
// Arrange
|
||||
const ActorEvents OUT_OF_RANGE_EVENT = (ActorEvents) 123;
|
||||
CreateNewDevicePropertiesActor();
|
||||
|
||||
// Act
|
||||
this.SetupDevicePropertiesActor();
|
||||
|
||||
// Assert
|
||||
Assert.Throws<ArgumentOutOfRangeException>(
|
||||
() => this.target.HandleEvent(OUT_OF_RANGE_EVENT));
|
||||
}
|
||||
|
||||
private void CreateNewDevicePropertiesActor()
|
||||
{
|
||||
this.target = new DevicePropertiesActor(
|
||||
this.logger.Object,
|
||||
this.actorsLogger.Object,
|
||||
this.rateLimiting.Object,
|
||||
this.updatePropertiesLogic.Object);
|
||||
}
|
||||
|
||||
private void SetupDevicePropertiesActor()
|
||||
{
|
||||
string DEVICE_ID = "01";
|
||||
|
||||
this.target.Setup(DEVICE_ID,
|
||||
this.deviceStateActor.Object,
|
||||
this.deviceConnectionActor.Object);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,153 @@
|
|||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services;
|
||||
using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Diagnostics;
|
||||
using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Models;
|
||||
using Microsoft.Azure.IoTSolutions.DeviceSimulation.SimulationAgent.DeviceConnection;
|
||||
using Microsoft.Azure.IoTSolutions.DeviceSimulation.SimulationAgent.DeviceProperties;
|
||||
using Microsoft.Azure.IoTSolutions.DeviceSimulation.SimulationAgent.DeviceState;
|
||||
using Moq;
|
||||
using SimulationAgent.Test.helpers;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
using static Microsoft.Azure.IoTSolutions.DeviceSimulation.SimulationAgent.DeviceProperties.DevicePropertiesActor;
|
||||
|
||||
namespace SimulationAgent.Test.DeviceProperties
|
||||
{
|
||||
public class UpdateReportedPropertiesTest
|
||||
{
|
||||
private const string DEVICE_ID = "01";
|
||||
private readonly Mock<ILogger> logger;
|
||||
private readonly Mock<IDevicePropertiesActor> devicePropertiesActor;
|
||||
private readonly Mock<IDeviceStateActor> deviceStateActor;
|
||||
private readonly Mock<IDeviceConnectionActor> deviceConnectionActor;
|
||||
private readonly Mock<ISmartDictionary> properties;
|
||||
private readonly Mock<IDeviceClient> client;
|
||||
|
||||
private UpdateReportedProperties target;
|
||||
|
||||
public UpdateReportedPropertiesTest(ITestOutputHelper log)
|
||||
{
|
||||
this.logger = new Mock<ILogger>();
|
||||
this.devicePropertiesActor = new Mock<IDevicePropertiesActor>();
|
||||
this.deviceStateActor = new Mock<IDeviceStateActor>();
|
||||
this.deviceConnectionActor = new Mock<IDeviceConnectionActor>();
|
||||
this.properties = new Mock<ISmartDictionary>();
|
||||
this.client = new Mock<IDeviceClient>();
|
||||
|
||||
this.target = new UpdateReportedProperties(logger.Object);
|
||||
}
|
||||
|
||||
[Fact, Trait(Constants.TYPE, Constants.UNIT_TEST)]
|
||||
public void Device_Offline_Should_Call_Handle_Event_Update_Failed()
|
||||
{
|
||||
// Arrange
|
||||
this.SetupPropertiesActorProperties();
|
||||
this.SetupPropertiesActorStateOffline();
|
||||
this.SetupPropertiesChangedToTrue();
|
||||
this.target.Setup(this.devicePropertiesActor.Object, DEVICE_ID);
|
||||
|
||||
// Act
|
||||
this.target.Run();
|
||||
|
||||
// Assert
|
||||
this.devicePropertiesActor.Verify(x => x.HandleEvent(ActorEvents.PropertiesUpdateFailed));
|
||||
}
|
||||
|
||||
[Fact, Trait(Constants.TYPE, Constants.UNIT_TEST)]
|
||||
public void Device_Online_Should_Call_Update_Properties_Async()
|
||||
{
|
||||
// Arrange
|
||||
this.SetupPropertiesActor();
|
||||
this.SetupPropertiesActorProperties();
|
||||
this.SetupPropertiesActorStateOnline();
|
||||
this.SetupPropertiesChangedToTrue();
|
||||
this.SetupClient();
|
||||
this.target.Setup(this.devicePropertiesActor.Object, DEVICE_ID);
|
||||
|
||||
// Act
|
||||
this.target.Run();
|
||||
|
||||
// Assert
|
||||
this.devicePropertiesActor.Verify(x => x.Client.UpdatePropertiesAsync(It.IsAny<ISmartDictionary>()));
|
||||
}
|
||||
|
||||
private void SetupPropertiesActor()
|
||||
{
|
||||
this.devicePropertiesActor.Object.Setup(
|
||||
DEVICE_ID,
|
||||
this.deviceStateActor.Object,
|
||||
this.deviceConnectionActor.Object);
|
||||
}
|
||||
|
||||
private void SetupPropertiesActorProperties()
|
||||
{
|
||||
var properties = new Dictionary<string, object>
|
||||
{
|
||||
{ "testKey1", "testValue1" },
|
||||
{ "testKey2", "testValue2" }
|
||||
};
|
||||
|
||||
this.devicePropertiesActor
|
||||
.Setup(x => x.DeviceProperties.GetAll())
|
||||
.Returns(properties);
|
||||
|
||||
var smartDictionary = new SmartDictionary(properties);
|
||||
|
||||
this.devicePropertiesActor
|
||||
.Setup(x => x.DeviceProperties)
|
||||
.Returns(smartDictionary);
|
||||
}
|
||||
|
||||
private void SetupPropertiesChangedToTrue()
|
||||
{
|
||||
this.devicePropertiesActor
|
||||
.Setup(x => x.DeviceProperties.Changed)
|
||||
.Returns(true);
|
||||
}
|
||||
|
||||
private void SetupPropertiesChangedToFalse()
|
||||
{
|
||||
this.devicePropertiesActor
|
||||
.Setup(x => x.DeviceProperties.Changed)
|
||||
.Returns(false);
|
||||
}
|
||||
|
||||
private void SetupPropertiesActorStateOnline()
|
||||
{
|
||||
var state = new Dictionary<string, object>
|
||||
{
|
||||
{ "online", true }
|
||||
};
|
||||
|
||||
this.devicePropertiesActor
|
||||
.Setup(x => x.DeviceState.GetAll())
|
||||
.Returns(state);
|
||||
}
|
||||
|
||||
private void SetupPropertiesActorStateOffline()
|
||||
{
|
||||
var state = new Dictionary<string, object>
|
||||
{
|
||||
{ "online", false }
|
||||
};
|
||||
|
||||
this.devicePropertiesActor
|
||||
.Setup(x => x.DeviceState.GetAll())
|
||||
.Returns(state);
|
||||
}
|
||||
|
||||
private void SetupClient()
|
||||
{
|
||||
this.devicePropertiesActor
|
||||
.Setup(x => x.Client)
|
||||
.Returns(this.client.Object);
|
||||
|
||||
this.devicePropertiesActor
|
||||
.Setup(x => x.Client.UpdatePropertiesAsync(It.IsAny<ISmartDictionary>()))
|
||||
.Returns(Task.CompletedTask);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,71 @@
|
|||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Diagnostics;
|
||||
using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Models;
|
||||
using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Simulation;
|
||||
using Microsoft.Azure.IoTSolutions.DeviceSimulation.SimulationAgent.DeviceState;
|
||||
using Moq;
|
||||
using SimulationAgent.Test.helpers;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace SimulationAgent.Test.DeviceState
|
||||
{
|
||||
public class DeviceStateActorTest
|
||||
{
|
||||
private readonly Mock<ILogger> logger;
|
||||
private readonly Mock<IScriptInterpreter> scriptInterpreter;
|
||||
private readonly Mock<UpdateDeviceState> updateDeviceStateLogic;
|
||||
private readonly DeviceStateActor target;
|
||||
|
||||
public DeviceStateActorTest(ITestOutputHelper log)
|
||||
{
|
||||
this.logger = new Mock<ILogger>();
|
||||
this.scriptInterpreter = new Mock<IScriptInterpreter>();
|
||||
this.updateDeviceStateLogic = new Mock<UpdateDeviceState>(
|
||||
this.scriptInterpreter.Object,
|
||||
this.logger.Object);
|
||||
|
||||
this.target = new DeviceStateActor(
|
||||
this.logger.Object,
|
||||
this.updateDeviceStateLogic.Object);
|
||||
}
|
||||
|
||||
[Fact, Trait(Constants.TYPE, Constants.UNIT_TEST)]
|
||||
public void ReportInactiveStatusBeforeRun()
|
||||
{
|
||||
// Arrange
|
||||
SetupDeviceStateActor();
|
||||
|
||||
// Act
|
||||
var result = this.target.IsDeviceActive;
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact, Trait(Constants.TYPE, Constants.UNIT_TEST)]
|
||||
public void ReportActiveStatusAfterRun()
|
||||
{
|
||||
// Arrange
|
||||
SetupDeviceStateActor();
|
||||
|
||||
// Act
|
||||
this.target.Run();
|
||||
var result = this.target.IsDeviceActive;
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
private void SetupDeviceStateActor()
|
||||
{
|
||||
string DEVICE_ID = "01";
|
||||
int postion = 1;
|
||||
int total = 10;
|
||||
var deviceModel = new DeviceModel { Id = DEVICE_ID };
|
||||
|
||||
this.target.Setup(DEVICE_ID, deviceModel, postion, total);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,134 @@
|
|||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services;
|
||||
using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Concurrency;
|
||||
using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Diagnostics;
|
||||
using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Models;
|
||||
using Microsoft.Azure.IoTSolutions.DeviceSimulation.SimulationAgent.DeviceConnection;
|
||||
using Microsoft.Azure.IoTSolutions.DeviceSimulation.SimulationAgent.DeviceState;
|
||||
using Microsoft.Azure.IoTSolutions.DeviceSimulation.SimulationAgent.DeviceTelemetry;
|
||||
using Moq;
|
||||
using SimulationAgent.Test.helpers;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace SimulationAgent.Test.DeviceTelemetry
|
||||
{
|
||||
public class DeviceTelemetryActorTest
|
||||
{
|
||||
private readonly Mock<ILogger> logger;
|
||||
private readonly Mock<IActorsLogger> actorLogger;
|
||||
private readonly Mock<IRateLimitingConfig> rateLimitingConfig;
|
||||
private readonly Mock<IRateLimiting> rateLimiting;
|
||||
private readonly Mock<SendTelemetry> sendTelemetryLogic;
|
||||
private readonly Mock<IDeviceStateActor> deviceStateActor;
|
||||
private readonly Mock<IDeviceConnectionActor> deviceConnectionActor;
|
||||
private readonly Mock<IDevices> devices;
|
||||
private readonly DeviceTelemetryActor target;
|
||||
|
||||
public DeviceTelemetryActorTest(ITestOutputHelper log)
|
||||
{
|
||||
this.logger = new Mock<ILogger>();
|
||||
this.actorLogger = new Mock<IActorsLogger>();
|
||||
this.rateLimitingConfig = new Mock<IRateLimitingConfig>();
|
||||
this.rateLimiting = new Mock<IRateLimiting>();
|
||||
this.devices = new Mock<IDevices>();
|
||||
this.sendTelemetryLogic = new Mock<SendTelemetry>(this.logger.Object);
|
||||
|
||||
|
||||
this.deviceStateActor = new Mock<IDeviceStateActor>();
|
||||
this.deviceConnectionActor = new Mock<IDeviceConnectionActor>();
|
||||
|
||||
this.rateLimitingConfig.Setup(x => x.DeviceMessagesPerSecond).Returns(10);
|
||||
|
||||
this.target = new DeviceTelemetryActor(
|
||||
this.logger.Object,
|
||||
this.actorLogger.Object,
|
||||
this.rateLimiting.Object,
|
||||
this.sendTelemetryLogic.Object);
|
||||
}
|
||||
|
||||
[Fact, Trait(Constants.TYPE, Constants.UNIT_TEST)]
|
||||
public void TheNumberOfFailedMessagesIsZeroAtStart()
|
||||
{
|
||||
// Arrange
|
||||
SetupDeviceTelemetryActor();
|
||||
|
||||
// Act
|
||||
long failedMessagesCount = this.target.FailedMessagesCount;
|
||||
|
||||
// Assert
|
||||
Assert.Equal(0, failedMessagesCount);
|
||||
}
|
||||
|
||||
[Fact, Trait(Constants.TYPE, Constants.UNIT_TEST)]
|
||||
public void ItReturnsTheNumberOfFailedMessages()
|
||||
{
|
||||
// Arrange
|
||||
const int FAILED_MESSAGES_COUNT = 5;
|
||||
SetupDeviceTelemetryActor();
|
||||
DeviceTelemetryActor.ActorEvents messageFailed = DeviceTelemetryActor.ActorEvents.TelemetrySendFailure;
|
||||
|
||||
// Act
|
||||
for(int i = 0; i < FAILED_MESSAGES_COUNT; i++)
|
||||
{
|
||||
this.target.HandleEvent(messageFailed);
|
||||
}
|
||||
|
||||
// Get results
|
||||
long failedMessagesCount = this.target.FailedMessagesCount;
|
||||
|
||||
// Assert
|
||||
Assert.Equal(FAILED_MESSAGES_COUNT, failedMessagesCount);
|
||||
}
|
||||
|
||||
[Fact, Trait(Constants.TYPE, Constants.UNIT_TEST)]
|
||||
public void TheNumberOfTotalMessagesIsZeroAtStart()
|
||||
{
|
||||
// Arrange
|
||||
SetupDeviceTelemetryActor();
|
||||
|
||||
// Act
|
||||
long totalMessagesCount = this.target.TotalMessagesCount;
|
||||
|
||||
// Assert
|
||||
Assert.Equal(0, totalMessagesCount);
|
||||
}
|
||||
|
||||
[Fact, Trait(Constants.TYPE, Constants.UNIT_TEST)]
|
||||
public void ItReturnsTheNumberOfTotalMessages()
|
||||
{
|
||||
// Arrange
|
||||
const int MESSAGES_SENDING_COUNT = 5;
|
||||
SetupDeviceTelemetryActor();
|
||||
DeviceTelemetryActor.ActorEvents sendingMessage = DeviceTelemetryActor.ActorEvents.SendingTelemetry;
|
||||
|
||||
// Act
|
||||
for (int i = 0; i < MESSAGES_SENDING_COUNT; i++)
|
||||
{
|
||||
this.target.HandleEvent(sendingMessage);
|
||||
}
|
||||
|
||||
// Get results
|
||||
long totalMessagesCount = this.target.TotalMessagesCount;
|
||||
|
||||
// Assert
|
||||
Assert.Equal(MESSAGES_SENDING_COUNT, totalMessagesCount);
|
||||
}
|
||||
|
||||
private void SetupDeviceTelemetryActor()
|
||||
{
|
||||
string DEVICE_ID = "01";
|
||||
var deviceModel = new DeviceModel { Id = DEVICE_ID };
|
||||
var message = new DeviceModel.DeviceModelMessage();
|
||||
this.deviceConnectionActor.SetupGet(x => x.Connected).Returns(true);
|
||||
|
||||
this.target.Setup(
|
||||
DEVICE_ID,
|
||||
deviceModel,
|
||||
message,
|
||||
this.deviceStateActor.Object,
|
||||
this.deviceConnectionActor.Object);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,13 +2,18 @@
|
|||
<PropertyGroup>
|
||||
<TargetFramework>netcoreapp2.0</TargetFramework>
|
||||
<LangVersion>7</LangVersion>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.5.0" />
|
||||
<PackageReference Include="Moq" Version="4.7.145" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.6.2" />
|
||||
<PackageReference Include="Moq" Version="4.8.2" />
|
||||
<PackageReference Include="xunit" Version="2.3.1" />
|
||||
<PackageReference Include="xunit.assert" Version="2.3.1" />
|
||||
<PackageReference Include="xunit.runner.console" Version="2.3.1" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.3.1" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Services\Services.csproj" />
|
||||
<ProjectReference Include="..\SimulationAgent\SimulationAgent.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
|
@ -0,0 +1,386 @@
|
|||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services;
|
||||
using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Concurrency;
|
||||
using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Diagnostics;
|
||||
using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Models;
|
||||
using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Runtime;
|
||||
using Microsoft.Azure.IoTSolutions.DeviceSimulation.SimulationAgent.DeviceConnection;
|
||||
using Microsoft.Azure.IoTSolutions.DeviceSimulation.SimulationAgent.DeviceProperties;
|
||||
using Microsoft.Azure.IoTSolutions.DeviceSimulation.SimulationAgent.DeviceState;
|
||||
using Microsoft.Azure.IoTSolutions.DeviceSimulation.SimulationAgent.DeviceTelemetry;
|
||||
using Moq;
|
||||
using SimulationAgent.Test.helpers;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
using static Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Models.DeviceModel;
|
||||
using static Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Models.Simulation;
|
||||
using SimulationModel = Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Models.Simulation;
|
||||
using SimulationRunner = Microsoft.Azure.IoTSolutions.DeviceSimulation.SimulationAgent.SimulationRunner;
|
||||
|
||||
namespace SimulationAgent.Test
|
||||
{
|
||||
public class SimulationRunnerTest
|
||||
{
|
||||
private readonly Mock<IRateLimitingConfig> ratingConfig;
|
||||
private readonly Mock<ILogger> logger;
|
||||
private readonly Mock<IDeviceModels> deviceModels;
|
||||
private readonly Mock<IDeviceModelsGeneration> deviceModelsOverriding;
|
||||
private readonly Mock<IDevices> devices;
|
||||
private readonly Mock<ISimulations> simulations;
|
||||
private readonly Mock<IFactory> factory;
|
||||
private readonly Mock<IDictionary<string, IDeviceStateActor>> deviceStateActors;
|
||||
private readonly Mock<IDeviceStateActor> deviceStateActor;
|
||||
private readonly Mock<IDeviceConnectionActor> deviceConnectionActor;
|
||||
private readonly Mock<IDeviceTelemetryActor> deviceTelemetryActor;
|
||||
private readonly Mock<IDevicePropertiesActor> devicePropertiesActor;
|
||||
private readonly Mock<IRateLimiting> rateLimiting;
|
||||
private readonly Mock<UpdateDeviceState> updateDeviceStateLogic;
|
||||
private readonly SimulationRunner target;
|
||||
|
||||
public SimulationRunnerTest(ITestOutputHelper log)
|
||||
{
|
||||
this.ratingConfig = new Mock<IRateLimitingConfig>();
|
||||
this.logger = new Mock<ILogger>();
|
||||
this.deviceModels = new Mock<IDeviceModels>();
|
||||
this.deviceModelsOverriding = new Mock<IDeviceModelsGeneration>();
|
||||
this.devices = new Mock<IDevices>();
|
||||
this.simulations = new Mock<ISimulations>();
|
||||
this.factory = new Mock<IFactory>();
|
||||
this.deviceStateActors = new Mock<IDictionary<string, IDeviceStateActor>>();
|
||||
this.deviceStateActor = new Mock<IDeviceStateActor>();
|
||||
this.deviceConnectionActor = new Mock<IDeviceConnectionActor>();
|
||||
this.deviceTelemetryActor = new Mock<IDeviceTelemetryActor>();
|
||||
this.devicePropertiesActor = new Mock<IDevicePropertiesActor>();
|
||||
this.updateDeviceStateLogic = new Mock<UpdateDeviceState>();
|
||||
this.rateLimiting = new Mock<IRateLimiting>();
|
||||
this.ratingConfig.Setup(x => x.DeviceMessagesPerSecond).Returns(10);
|
||||
|
||||
this.target = new SimulationRunner(
|
||||
this.ratingConfig.Object,
|
||||
this.rateLimiting.Object,
|
||||
this.logger.Object,
|
||||
this.deviceModels.Object,
|
||||
this.deviceModelsOverriding.Object,
|
||||
this.devices.Object,
|
||||
this.simulations.Object,
|
||||
this.factory.Object);
|
||||
}
|
||||
|
||||
[Fact, Trait(Constants.TYPE, Constants.UNIT_TEST)]
|
||||
public void TheActiveDevicesCountIsZeroAtStart()
|
||||
{
|
||||
// Act
|
||||
var result = this.target.ActiveDevicesCount;
|
||||
|
||||
// Assert
|
||||
Assert.Equal(0, result);
|
||||
}
|
||||
|
||||
[Fact, Trait(Constants.TYPE, Constants.UNIT_TEST)]
|
||||
public void ItReturnsTheNumberOfActiveDevices()
|
||||
{
|
||||
// Arrange
|
||||
const int ACTIVE_DEVICES_COUNT = 7;
|
||||
|
||||
var simulation = this.GenerateSimulationModel(ACTIVE_DEVICES_COUNT);
|
||||
|
||||
this.SetupSimulationReadyToStart();
|
||||
|
||||
// Act
|
||||
this.target.Start(simulation);
|
||||
var result = this.target.ActiveDevicesCount;
|
||||
|
||||
// Assert
|
||||
Assert.Equal(ACTIVE_DEVICES_COUNT, result);
|
||||
}
|
||||
|
||||
[Fact, Trait(Constants.TYPE, Constants.UNIT_TEST)]
|
||||
public void TheNumberOfTotalMessagesCountIsZeroAtStart()
|
||||
{
|
||||
// Act
|
||||
var result = this.target.TotalMessagesCount;
|
||||
|
||||
// Assert
|
||||
Assert.Equal(0, result);
|
||||
}
|
||||
|
||||
[Fact, Trait(Constants.TYPE, Constants.UNIT_TEST)]
|
||||
public void ItReturnsTheNumberOfTotalMessagesCount()
|
||||
{
|
||||
// Arrange
|
||||
const int TOTAL_MESSAGES_PER_DEVICE_COUNT = 10;
|
||||
const int ACTIVE_DEVICES_COUNT = 7;
|
||||
|
||||
var simulation = this.GenerateSimulationModel(ACTIVE_DEVICES_COUNT);
|
||||
|
||||
this.SetupSimulationReadyToStart();
|
||||
|
||||
this.deviceTelemetryActor
|
||||
.Setup(x => x.TotalMessagesCount)
|
||||
.Returns(TOTAL_MESSAGES_PER_DEVICE_COUNT);
|
||||
|
||||
// Act
|
||||
this.target.Start(simulation);
|
||||
var result = this.target.TotalMessagesCount;
|
||||
|
||||
// Assert
|
||||
Assert.Equal(TOTAL_MESSAGES_PER_DEVICE_COUNT * ACTIVE_DEVICES_COUNT, result);
|
||||
}
|
||||
|
||||
[Fact, Trait(Constants.TYPE, Constants.UNIT_TEST)]
|
||||
public void TheNumberOfFailedMessagesCountIsZeroAtStart()
|
||||
{
|
||||
// Act
|
||||
var result = this.target.FailedMessagesCount;
|
||||
|
||||
// Assert
|
||||
Assert.Equal(0, result);
|
||||
}
|
||||
|
||||
[Fact, Trait(Constants.TYPE, Constants.UNIT_TEST)]
|
||||
public void ItReturnsTheNumberOfFailedMessagesCount()
|
||||
{
|
||||
// Arrange
|
||||
const int FAILED_MESSAGES_PER_DEVICE_COUNT = 1;
|
||||
const int ACTIVE_DEVICES_COUNT = 7;
|
||||
|
||||
var simulation = this.GenerateSimulationModel(ACTIVE_DEVICES_COUNT);
|
||||
|
||||
this.SetupSimulationReadyToStart();
|
||||
|
||||
this.deviceTelemetryActor
|
||||
.Setup(x => x.FailedMessagesCount)
|
||||
.Returns(FAILED_MESSAGES_PER_DEVICE_COUNT);
|
||||
|
||||
// Act
|
||||
this.target.Start(simulation);
|
||||
var result = this.target.FailedMessagesCount;
|
||||
|
||||
// Assert
|
||||
Assert.Equal(FAILED_MESSAGES_PER_DEVICE_COUNT * ACTIVE_DEVICES_COUNT, result);
|
||||
}
|
||||
|
||||
[Fact, Trait(Constants.TYPE, Constants.UNIT_TEST)]
|
||||
public void TheNumberOfFailedDeviceConnectionsCountIsZeroAtStart()
|
||||
{
|
||||
// Act
|
||||
var result = this.target.FailedDeviceConnectionsCount;
|
||||
|
||||
// Assert
|
||||
Assert.Equal(0, result);
|
||||
}
|
||||
|
||||
[Fact, Trait(Constants.TYPE, Constants.UNIT_TEST)]
|
||||
public void ItReturnsTheNumberOfFailedDeviceConnectionsCount()
|
||||
{
|
||||
// Arrange
|
||||
const int FAILED_DEVICE_CONNECTIONS_COUNT = 1;
|
||||
const int ACTIVE_DEVICES_COUNT = 7;
|
||||
|
||||
var simulation = this.GenerateSimulationModel(ACTIVE_DEVICES_COUNT);
|
||||
|
||||
this.SetupSimulationReadyToStart();
|
||||
|
||||
this.deviceConnectionActor
|
||||
.Setup(x => x.FailedDeviceConnectionsCount)
|
||||
.Returns(FAILED_DEVICE_CONNECTIONS_COUNT);
|
||||
|
||||
// Act
|
||||
this.target.Start(simulation);
|
||||
var result = this.target.FailedDeviceConnectionsCount;
|
||||
|
||||
// Assert
|
||||
Assert.Equal(FAILED_DEVICE_CONNECTIONS_COUNT * ACTIVE_DEVICES_COUNT, result);
|
||||
}
|
||||
|
||||
[Fact, Trait(Constants.TYPE, Constants.UNIT_TEST)]
|
||||
public void TheNumberOfFailedDeviceTwinUpdatesCountIsZeroAtStart()
|
||||
{
|
||||
// Act
|
||||
var result = this.target.FailedDeviceTwinUpdatesCount;
|
||||
|
||||
// Assert
|
||||
Assert.Equal(0, result);
|
||||
}
|
||||
|
||||
[Fact, Trait(Constants.TYPE, Constants.UNIT_TEST)]
|
||||
public void ItReturnsTheNumberOfFailedDeviceTwinUpdatesCount()
|
||||
{
|
||||
// Arrange
|
||||
const int FAILED_DEVICE_TWIN_UPDATES_COUNT = 1;
|
||||
const int ACTIVE_DEVICES_COUNT = 7;
|
||||
|
||||
var simulation = this.GenerateSimulationModel(ACTIVE_DEVICES_COUNT);
|
||||
|
||||
this.SetupSimulationReadyToStart();
|
||||
|
||||
this.deviceConnectionActor
|
||||
.Setup(x => x.FailedTwinUpdatesCount)
|
||||
.Returns(FAILED_DEVICE_TWIN_UPDATES_COUNT);
|
||||
|
||||
// Act
|
||||
this.target.Start(simulation);
|
||||
var result = this.target.FailedDeviceTwinUpdatesCount;
|
||||
|
||||
// Assert
|
||||
Assert.Equal(FAILED_DEVICE_TWIN_UPDATES_COUNT * ACTIVE_DEVICES_COUNT, result);
|
||||
}
|
||||
|
||||
[Fact, Trait(Constants.TYPE, Constants.UNIT_TEST)]
|
||||
public void TheNumberOfSimulationErrorsCountIsZeroAtStart()
|
||||
{
|
||||
// Act
|
||||
var result = this.target.SimulationErrorsCount;
|
||||
|
||||
// Assert
|
||||
Assert.Equal(0, result);
|
||||
}
|
||||
|
||||
[Fact, Trait(Constants.TYPE, Constants.UNIT_TEST)]
|
||||
public void ItReturnsTheNumberOfSimulationErrorsCount()
|
||||
{
|
||||
// Arrange
|
||||
const int FAILED_DEVICE_STATE_COUNT = 1;
|
||||
const int FAILED_DEVICE_CONNECTIONS_COUNT = 2;
|
||||
const int FAILED_MESSAGES_PER_DEVICE_COUNT = 3;
|
||||
const int ACTIVE_DEVICES_COUNT = 7;
|
||||
|
||||
var simulation = this.GenerateSimulationModel(ACTIVE_DEVICES_COUNT);
|
||||
|
||||
this.SetupSimulationReadyToStart();
|
||||
|
||||
this.deviceConnectionActor
|
||||
.Setup(x => x.SimulationErrorsCount)
|
||||
.Returns(FAILED_DEVICE_CONNECTIONS_COUNT);
|
||||
|
||||
this.deviceStateActor
|
||||
.Setup(x => x.SimulationErrorsCount)
|
||||
.Returns(FAILED_DEVICE_STATE_COUNT);
|
||||
|
||||
this.deviceTelemetryActor
|
||||
.Setup(x => x.FailedMessagesCount)
|
||||
.Returns(FAILED_MESSAGES_PER_DEVICE_COUNT);
|
||||
|
||||
// Act
|
||||
this.target.Start(simulation);
|
||||
var result = this.target.SimulationErrorsCount;
|
||||
|
||||
// Assert
|
||||
var EXPECT_RESULT = (FAILED_DEVICE_STATE_COUNT +
|
||||
FAILED_DEVICE_CONNECTIONS_COUNT +
|
||||
FAILED_MESSAGES_PER_DEVICE_COUNT) * ACTIVE_DEVICES_COUNT;
|
||||
Assert.Equal(EXPECT_RESULT, result);
|
||||
}
|
||||
|
||||
private SimulationModel GenerateSimulationModel(int ACTIVE_DEVICES_COUNT)
|
||||
{
|
||||
var models = new List<DeviceModelRef>
|
||||
{
|
||||
new DeviceModelRef { Id = "01", Count = ACTIVE_DEVICES_COUNT }
|
||||
};
|
||||
|
||||
return new SimulationModel
|
||||
{
|
||||
Id = "1",
|
||||
Created = DateTimeOffset.UtcNow.Subtract(TimeSpan.FromDays(10)),
|
||||
Modified = DateTimeOffset.UtcNow.Subtract(TimeSpan.FromDays(10)),
|
||||
ETag = "ETag0",
|
||||
Enabled = false,
|
||||
Version = 1,
|
||||
DeviceModels = models
|
||||
};
|
||||
}
|
||||
|
||||
private void SetupSimulationReadyToStart()
|
||||
{
|
||||
this.SetupSimulations();
|
||||
|
||||
this.SetUpDeviceModelsOverriding();
|
||||
|
||||
this.SetupDevices();
|
||||
|
||||
this.SetupDeviceStateActor();
|
||||
|
||||
this.SetupDeviceConnectionActor();
|
||||
|
||||
this.SetupDeviceTelemetryActor();
|
||||
|
||||
this.SetupDevicePropertiesActor();
|
||||
|
||||
this.SetupActiveDeviceStatus();
|
||||
}
|
||||
|
||||
private void SetupActiveDeviceStatus()
|
||||
{
|
||||
this.deviceStateActor
|
||||
.Setup(x => x.IsDeviceActive)
|
||||
.Returns(true);
|
||||
}
|
||||
|
||||
private void SetupDeviceTelemetryActor()
|
||||
{
|
||||
this.factory
|
||||
.Setup(x => x.Resolve<IDeviceTelemetryActor>())
|
||||
.Returns(this.deviceTelemetryActor.Object);
|
||||
}
|
||||
|
||||
private void SetupDevicePropertiesActor()
|
||||
{
|
||||
this.factory
|
||||
.Setup(x => x.Resolve<IDevicePropertiesActor>())
|
||||
.Returns(this.devicePropertiesActor.Object);
|
||||
}
|
||||
|
||||
private void SetupDeviceConnectionActor()
|
||||
{
|
||||
this.factory
|
||||
.Setup(x => x.Resolve<IDeviceConnectionActor>())
|
||||
.Returns(this.deviceConnectionActor.Object);
|
||||
}
|
||||
|
||||
private void SetupDeviceStateActor()
|
||||
{
|
||||
this.factory
|
||||
.Setup(x => x.Resolve<IDeviceStateActor>())
|
||||
.Returns(this.deviceStateActor.Object);
|
||||
}
|
||||
|
||||
private void SetupDevices()
|
||||
{
|
||||
this.devices
|
||||
.Setup(x => x.GenerateId(
|
||||
It.IsAny<string>(),
|
||||
It.IsAny<int>()))
|
||||
.Returns("Simulate-01");
|
||||
}
|
||||
|
||||
private void SetUpDeviceModelsOverriding()
|
||||
{
|
||||
var telemetry = new List<DeviceModelMessage>();
|
||||
var message = new DeviceModelMessage
|
||||
{
|
||||
Interval = TimeSpan.FromSeconds(10)
|
||||
};
|
||||
telemetry.Add(message);
|
||||
var deviceModel = new DeviceModel { Id = "01", Telemetry = telemetry };
|
||||
|
||||
this.deviceModelsOverriding
|
||||
.Setup(x => x.Generate(
|
||||
It.IsAny<DeviceModel>(),
|
||||
It.IsAny<DeviceModelOverride>()))
|
||||
.Returns(deviceModel);
|
||||
}
|
||||
|
||||
private void SetupSimulations()
|
||||
{
|
||||
var deviceIds = new List<string> { "01", "02" };
|
||||
|
||||
this.simulations
|
||||
.Setup(x => x.GetDeviceIds(It.IsAny<Simulation>()))
|
||||
.Returns(deviceIds);
|
||||
}
|
||||
}
|
||||
}
|
Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше
Загрузка…
Ссылка в новой задаче