Add Device Properties logic to internal simulation (#173)

* uncomment UpdateTwinAsync

* add folder for DeviceTwinActor

* Add support classes for DeviceTwinActor to update twin -- uses empty methods

* remove Properties from DeviceStateActor class

* Add InternalDeviceState Class and ability to update twin via callback from javascript

* add placeholders for twin update logic for future pr

* remove eslint jsdoc updates and update javascriptinterpreter tests

* Add new thread for device properties updates

* return null for not implemented methods

* Add Unit Tests for Internal Device State

* Update existing tests with new InternalDeviceState class

* Update scripts with instructions on device property updates

* Fix errors with JS device scripts and readonly dictionary

* revert changes to DeviceTwin class

* Update log messages for readability with timestamp

* Rename logging methods for consistency

* Separate properties and state and remove unused lines

* update comments for twin update branch

* Revert UpdateTwinAsync signature in DeviceClient

* revert UpdateState and JSValueToDictionary to private

* update comment on restoreState in chiller js script

* Update InternalDevicePropertiesTest names

* Consolidated to SmartDictionary class

* remove DevicePropertiesActor, revert DeviceTwin, change terminology from sensors to state

* revert actors logger

* revert simulation runner

* revert simulation runner test

* revert deletion of UpdateReportedProperties.cs

* fix spacing in SimulationRunner

* fix spacing in device twin

* consolidate restore state javascript methods

* add properties to internal script method call

* variable/method naming, whitespace cleanup, add missing method in elevator state script
This commit is contained in:
Jill Bender 2018-03-19 15:57:38 -07:00 коммит произвёл Parvez
Родитель c6ef837442
Коммит 37a720d044
16 изменённых файлов: 630 добавлений и 176 удалений

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

@ -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>
{
@ -57,22 +62,26 @@ namespace Services.Test.Simulation
["lights_on"] = false
};
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"));
}
[Fact, Trait(Constants.TYPE, Constants.UNIT_TEST)]
public void TestJavascriptFiles()
{
// Arrange
SmartDictionary deviceState = new SmartDictionary();
var files = new List<string>
{
"chiller-01-state.js",
@ -89,8 +98,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());
}
}

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

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

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

@ -77,7 +77,7 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Diagnostics
this.telemetryLogFile = this.path + Path.DirectorySeparatorChar + "telemetry.log";
this.enabled = this.enabledInConfig && !string.IsNullOrEmpty(this.path);
if (!this.enabled) return;
try
@ -363,4 +363,4 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Diagnostics
this.log.LogToFile(filename, text);
}
}
}
}

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

@ -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 = newState;
this.Changed = true;
}
}
}

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

@ -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 Newtonsoft.Json;
namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Simulation
@ -17,11 +18,13 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Simulation
/// <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>
/// <returns>New device sensors state</returns>
Dictionary<string, object> Invoke(
/// <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,
Dictionary<string, object> state);
ISmartDictionary state,
ISmartDictionary properties);
}
public class InternalInterpreter : IInternalInterpreter
@ -47,43 +50,43 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Simulation
this.random = new Random();
}
public Dictionary<string, object> Invoke(
public void Invoke(
string scriptPath,
object scriptParams,
Dictionary<string, object> context,
Dictionary<string, object> state)
ISmartDictionary state,
ISmartDictionary properties)
{
switch (scriptPath.ToLowerInvariant())
{
case SCRIPT_RANDOM:
return this.RunRandomNumberScript(scriptParams, state);
this.RunRandomNumberScript(scriptParams, state);
break;
case SCRIPT_INCREASING:
return this.RunIncreasingScript(scriptParams, state);
this.RunIncreasingScript(scriptParams, state);
break;
case SCRIPT_DECREASING:
return this.RunDecreasingScript(scriptParams, state);
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 Dictionary<string, object> RunRandomNumberScript(object scriptParams, Dictionary<string, object> state)
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);
state[sensor.Key] = this.random.NextDouble() * (max - min) + min;
var value = this.random.NextDouble() * (max - min) + min;
state.Set(sensor.Key, value);
}
return state;
}
// For each sensors specified, increase the current state, up to a maximum, then restart from a minimum
private Dictionary<string, object> RunIncreasingScript(object scriptParams, Dictionary<string, object> state)
private void RunIncreasingScript(object scriptParams, ISmartDictionary state)
{
var sensors = this.JsonParamAsDictionary(scriptParams);
foreach (var sensor in sensors)
@ -92,22 +95,20 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Simulation
(double min, double max, double step) = this.GetMinMaxStepParameters(sensor.Value);
// Add the sensor to the state if missing
if (!state.ContainsKey(sensor.Key))
if (!state.Has(sensor.Key))
{
state[sensor.Key] = min;
state.Set(sensor.Key, min);
}
double current = Convert.ToDouble(state[sensor.Key]);
double current = Convert.ToDouble(state.Get(sensor.Key));
double next = AreEqual(current, max) ? min : Math.Min(current + step, max);
state[sensor.Key] = next;
state.Set(sensor.Key, next);
}
return state;
}
// For each sensors specified, decrease the current state, down to a minimum, then restart from a maximum
private Dictionary<string, object> RunDecreasingScript(object scriptParams, Dictionary<string, object> state)
private void RunDecreasingScript(object scriptParams, ISmartDictionary state)
{
var sensors = this.JsonParamAsDictionary(scriptParams);
foreach (var sensor in sensors)
@ -116,18 +117,16 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Simulation
(double min, double max, double step) = this.GetMinMaxStepParameters(sensor.Value);
// Add the sensor to the state if missing
if (!state.ContainsKey(sensor.Key))
if (!state.Has(sensor.Key))
{
state[sensor.Key] = max;
state.Set(sensor.Key, max);
}
double current = Convert.ToDouble(state[sensor.Key]);
double current = Convert.ToDouble(state.Get(sensor.Key));
double next = AreEqual(current, min) ? max : Math.Max(current - step, min);
state[sensor.Key] = next;
state.Set(sensor.Key, next);
}
return state;
}
private (double, double) GetMinMaxParameters(object parameters)

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

@ -11,23 +11,27 @@ 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
@ -46,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();
@ -61,10 +67,13 @@ 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
// register callback for property updates
engine.SetValue("updateProperty", new Action<string, object>(this.UpdateProperty));
// register sleep function for javascript use
engine.SetValue("sleep", new Action<int>(this.Sleep));
try
@ -84,16 +93,21 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Simulation
}
this.log.Debug("Executing JS function", () => new { filename });
JsValue output = engine.Execute(program).Invoke("main", context, this.deviceState);
var result = this.JsValueToDictionary(output);
this.log.Debug("JS function success", () => new { filename, result });
return result;
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>();
}
}
@ -178,26 +192,34 @@ 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
// Update device state with the script data passed
lock (this.deviceState)
{
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;
}
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
lock (this.deviceProperties)
{
this.deviceProperties.Set(key, value);
}
}
}
}

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

@ -9,10 +9,17 @@ 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
@ -31,10 +38,11 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Simulation
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())
{
@ -44,15 +52,15 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Simulation
case "javascript":
this.log.Debug("Invoking JS", () => new { script.Path, context, state });
var jsResult = this.jsInterpreter.Invoke(script.Path, context, state);
this.log.Debug("JS result", () => new { result = jsResult });
return jsResult;
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 });
var intResult = this.intInterpreter.Invoke(script.Path, script.Params, context, state);
this.log.Debug("Internal script result", () => new { intresult = intResult });
return intResult;
this.intInterpreter.Invoke(script.Path, script.Params, context, state, properties);
this.log.Debug("Internal script complete", () => {});
break;
}
}
}

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

@ -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,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";
@ -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);

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

@ -6,8 +6,6 @@ using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Simulation;
using Microsoft.Azure.IoTSolutions.DeviceSimulation.SimulationAgent.DeviceState;
using Moq;
using SimulationAgent.Test.helpers;
using System;
using System.Collections.Generic;
using Xunit;
using Xunit.Abstractions;
@ -66,18 +64,7 @@ namespace SimulationAgent.Test.DeviceState
int postion = 1;
int total = 10;
var deviceModel = new DeviceModel { Id = DEVICE_ID };
var deviceState = new Dictionary<string, object>
{
{ DEVICE_ID, new Object { } }
};
this.scriptInterpreter
.Setup(x => x.Invoke(
It.IsAny<Script>(),
It.IsAny<Dictionary<string, object>>(),
It.IsAny<Dictionary<string, object>>()))
.Returns(deviceState);
this.target.Setup(DEVICE_ID, deviceModel, postion, total);
}
}

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

@ -240,4 +240,4 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.SimulationAgent.Simulati
}
}
}
*/
*/

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

@ -6,12 +6,14 @@ using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Diagnostics;
using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Models;
using Microsoft.Azure.IoTSolutions.DeviceSimulation.SimulationAgent.Exceptions;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.SimulationAgent.DeviceState
{
public interface IDeviceStateActor
{
Dictionary<string, object> DeviceState { get; }
ISmartDictionary DeviceState { get; }
ISmartDictionary DeviceProperties { get; }
bool IsDeviceActive { get; }
void Setup(string deviceId, DeviceModel deviceModel, int position, int totalDevices);
void Run();
@ -25,13 +27,10 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.SimulationAgent.DeviceSt
Updating
}
public const string CALC_TELEMETRY = "CalculateRandomizedTelemetry";
public ISmartDictionary DeviceState { get; set; }
public ISmartDictionary DeviceProperties { get; set; }
/// <summary>
/// The virtual state of the simulated device. The state is
/// periodically updated using an external script.
/// </summary>
public Dictionary<string, object> DeviceState { get; set; }
public const string CALC_TELEMETRY = "CalculateRandomizedTelemetry";
/// <summary>
/// The device is considered active when the state is being updated.
@ -70,7 +69,7 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.SimulationAgent.DeviceSt
/// Invoke this method before calling Start(), to initialize the actor
/// with details like the device model and message type to simulate.
/// If this method is not called before Start(), the application will
/// thrown an exception.
/// throw an exception.
/// Setup() should be called only once, typically after the constructor.
/// </summary>
public void Setup(string deviceId, DeviceModel deviceModel, int position, int totalDevices)
@ -99,8 +98,9 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.SimulationAgent.DeviceSt
// Prepare the dependencies
case ActorStatus.None:
this.updateDeviceStateLogic.Setup(this, this.deviceId, this.deviceModel);
this.DeviceState = this.SetupTelemetryAndProperties(this.deviceModel);
this.log.Debug("Initial device state", () => new { this.deviceId, this.DeviceState });
this.DeviceState = this.GetInitialState(this.deviceModel);
this.DeviceProperties = this.GetInitialProperties(this.deviceModel);
this.log.Debug("Initial device state", () => new { this.deviceId, this.DeviceState, this.DeviceProperties });
this.MoveForward();
return;
@ -145,27 +145,41 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.SimulationAgent.DeviceSt
throw new Exception("Application error, MoveForward() should not be invoked when status = " + this.status);
}
private Dictionary<string, object> SetupTelemetryAndProperties(DeviceModel deviceModel)
/// <summary>
/// Initializes device properties from the device model.
/// </summary>
private ISmartDictionary GetInitialProperties(DeviceModel model)
{
// put telemetry properties in state
Dictionary<string, object> state = CloneObject(deviceModel.Simulation.InitialState);
var properties = new SmartDictionary();
// Ensure the state contains the "online" key
if (!state.ContainsKey("online"))
foreach (var property in model.Properties)
{
state["online"] = true;
properties.Set(property.Key, JToken.FromObject(property.Value));
}
// TODO: think about whether these should be pulled from the hub instead of disk
// (the device model); i.e. what if someone has modified the hub twin directly
// put reported properties from device model into state
foreach (var property in deviceModel.Properties)
state.Add(property.Key, property.Value);
return properties;
}
// TODO:This is used to control whether telemetry is calculated in UpdateDeviceState.
// methods can turn telemetry off/on; e.g. setting temp high- turnoff, set low, turn on
// it would be better to do this at the telemetry item level - we should add this in the future
state.Add(CALC_TELEMETRY, true);
/// <summary>
/// Initializes device state from the device model.
/// </summary>
private ISmartDictionary GetInitialState(DeviceModel model)
{
var initialState = CloneObject(model.Simulation.InitialState);
var state = new SmartDictionary(initialState);
// Ensure the state contains the "online" key
if (!state.Has("online"))
{
state.Set("online", true);
}
// TODO: This is used to control whether telemetry is calculated in UpdateDeviceState.
// methods can turn telemetry off/on; e.g. setting temp high- turnoff, set low, turn on
// it would be better to do this at the telemetry item level - we should add this in the future
// https://github.com/Azure/device-simulation-dotnet/issues/174
state.Set(CALC_TELEMETRY, true);
return state;
}

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

@ -48,7 +48,7 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.SimulationAgent.DeviceSt
public void Run()
{
if ((bool) this.context.DeviceState[DeviceStateActor.CALC_TELEMETRY])
if ((bool) this.context.DeviceState.Get(DeviceStateActor.CALC_TELEMETRY))
{
this.log.Debug("Updating device telemetry data", () => new { this.deviceId });
@ -63,10 +63,12 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.SimulationAgent.DeviceSt
{
foreach (var script in this.deviceModel.Simulation.Scripts)
{
this.context.DeviceState = this.scriptInterpreter.Invoke(
// call Invoke() which to update the internal device state
this.scriptInterpreter.Invoke(
script,
scriptContext,
this.context.DeviceState);
this.context.DeviceState,
this.context.DeviceProperties);
}
}

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

@ -1,20 +1,19 @@
// 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.SimulationAgent.Exceptions;
using Microsoft.Azure.IoTSolutions.DeviceSimulation.SimulationAgent.DeviceConnection;
using Microsoft.Azure.IoTSolutions.DeviceSimulation.SimulationAgent.DeviceState;
using Microsoft.Azure.IoTSolutions.DeviceSimulation.SimulationAgent.Exceptions;
namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.SimulationAgent.DeviceTelemetry
{
public interface IDeviceTelemetryActor
{
Dictionary<string, object> DeviceState { get; }
ISmartDictionary DeviceState { get; }
IDeviceClient Client { get; }
DeviceModel.DeviceModelMessage Message { get; }
@ -80,7 +79,7 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.SimulationAgent.DeviceTe
/// <summary>
/// State maintained by the state actor
/// </summary>
public Dictionary<string, object> DeviceState => this.deviceStateActor.DeviceState;
public ISmartDictionary DeviceState => this.deviceStateActor.DeviceState;
/// <summary>
/// Azure IoT Hub client created by the connection actor

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

@ -37,39 +37,39 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.SimulationAgent.DeviceTe
try
{
var state = this.context.DeviceState;
this.log.Debug("Checking to see if device is online", () => new { this.deviceId });
if ((bool) state["online"])
{
// device could be rebooting, updating firmware, etc.
this.log.Debug("The device state says the device is online", () => new { this.deviceId });
// Inject the device state into the message template
this.log.Debug("Preparing the message content using the device state", () => new { this.deviceId });
var msg = this.message.MessageTemplate;
foreach (var value in state)
var state = this.context.DeviceState.GetAll();
this.log.Debug("Checking to see if device is online", () => new { this.deviceId });
if ((bool) state["online"])
{
msg = msg.Replace("${" + value.Key + "}", value.Value.ToString());
}
// device could be rebooting, updating firmware, etc.
this.log.Debug("The device state says the device is online", () => new { this.deviceId });
this.log.Debug("Calling SendMessageAsync...",
() => new { this.deviceId, MessageSchema = this.message.MessageSchema.Name, msg });
var now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
this.context.Client.SendMessageAsync(msg, this.message.MessageSchema)
.ContinueWith(t =>
// Inject the device state into the message template
this.log.Debug("Preparing the message content using the device state", () => new { this.deviceId });
var msg = this.message.MessageTemplate;
foreach (var value in state)
{
var timeSpent = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() - now;
this.log.Debug("Telemetry delivered", () => new { this.deviceId, timeSpent, MessageSchema = this.message.MessageSchema.Name });
this.context.HandleEvent(DeviceTelemetryActor.ActorEvents.TelemetryDelivered);
});
}
else
{
// device could be rebooting, updating firmware, etc.
this.log.Debug("No telemetry will be sent as the device is offline...", () => new { this.deviceId });
this.context.HandleEvent(DeviceTelemetryActor.ActorEvents.TelemetryDelivered);
}
msg = msg.Replace("${" + value.Key + "}", value.Value.ToString());
}
this.log.Debug("Calling SendMessageAsync...",
() => new { this.deviceId, MessageSchema = this.message.MessageSchema.Name, msg });
var now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
this.context.Client.SendMessageAsync(msg, this.message.MessageSchema)
.ContinueWith(t =>
{
var timeSpent = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() - now;
this.log.Debug("Telemetry delivered", () => new { this.deviceId, timeSpent, MessageSchema = this.message.MessageSchema.Name });
this.context.HandleEvent(DeviceTelemetryActor.ActorEvents.TelemetryDelivered);
});
}
else
{
// device could be rebooting, updating firmware, etc.
this.log.Debug("No telemetry will be sent as the device is offline...", () => new { this.deviceId });
this.context.HandleEvent(DeviceTelemetryActor.ActorEvents.TelemetryDelivered);
}
}
catch (Exception e)
{