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:
Jill Bender 2018-04-12 17:42:46 -07:00 коммит произвёл Devis Lucato
Родитель 1e7089446a
Коммит b211cb7805
214 изменённых файлов: 10994 добавлений и 3331 удалений

2
.gitattributes поставляемый
Просмотреть файл

@ -1,3 +1,5 @@
# Copyright (c) Microsoft. All rights reserved.
# Auto-detect text files, ensure they use LF.
* text=auto eol=lf

2
.gitignore поставляемый
Просмотреть файл

@ -1,3 +1,5 @@
# Copyright (c) Microsoft. All rights reserved.
### Custom
_dev/

Просмотреть файл

@ -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);
}
}
}

Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше