예제 #1
0
        public void SmartDictionary_Should_HaveNewItem_When_NewDesiredPropertyAdded()
        {
            // Arrange
            const string NEW_KEY   = "new key";
            const string NEW_VALUE = "new value";

            ISmartDictionary reportedProps = this.GetTestProperties();

            this.target.RegisterChangeUpdateAsync(this.sdkClient.Object, DEVICE_ID, reportedProps).CompleteOrTimeout();

            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);
        }
        public void Invoke(
            Script script,
            Dictionary <string, object> context,
            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}`.");

            case JAVASCRIPT_SCRIPT:
                this.log.Debug("Invoking JS", () => new { script.Path, context, state });
                this.jsInterpreter.Invoke(script, context, state, properties);
                this.log.Debug("JS invocation complete");
                break;

            case INTERNAL_SCRIPT:
                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;
            }
        }
 /// <summary>
 /// Load a JS file and execute the main() function, passing in
 /// context information and the output from the previous execution.
 /// Modifies the internal device state with the latest values.
 /// </summary>
 public void Invoke(
     string filename,
     Dictionary <string, object> context,
     ISmartDictionary state,
     ISmartDictionary properties)
 {
     this.Invoke(filename, null, context, state, properties);
 }
예제 #4
0
        public void ItCanBeInitializedWithNullValue()
        {
            // Act
            this.target = new SmartDictionary(null);

            // Assert
            Assert.True(this.target.Changed);
        }
예제 #5
0
        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));
        }
        /// <summary>
        /// Load a JS file and execute the main() function, passing in
        /// context information and the output from the previous execution.
        /// Modifies the internal device state with the latest values.
        /// </summary>
        public void Invoke(
            string filename,
            object scriptParams,
            Dictionary <string, object> context,
            ISmartDictionary state,
            ISmartDictionary properties)
        {
            this.deviceState      = state;
            this.deviceProperties = properties;

            var engine = new Engine();

            // Inject the logger in the JS context, to allow the JS function
            // logging into the service logs
            engine.SetValue("log", new Action <object>(this.JsLog));

            // register callback for state updates
            engine.SetValue("updateState", new Action <JsValue>(this.UpdateState));

            // 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
            {
                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 });

                engine.Execute(program).Invoke(
                    "main",
                    context,
                    this.deviceState.GetAll(),
                    this.deviceProperties.GetAll(),
                    scriptParams);

                this.log.Debug("JS function success", () => new { filename, this.deviceState });
            }
            catch (Exception e)
            {
                this.log.Error("JS function failure", () => new { e.Message, e.GetType().FullName });
            }
        }
예제 #7
0
        public async Task RegisterMethodsForDeviceAsync(
            IDictionary <string, Script> methods,
            ISmartDictionary deviceState,
            ISmartDictionary deviceProperties)
        {
            this.log.Debug("Attempting to register device methods",
                           () => new { this.deviceId });

            await this.deviceMethods.RegisterMethodsAsync(this.deviceId, methods, deviceState, deviceProperties);
        }
        // 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);
            }
        }
예제 #9
0
        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);
        }
예제 #10
0
        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);
        }
예제 #11
0
        public async Task RegisterMethodsAsync(
            IDeviceClientWrapper client,
            string deviceId,
            IDictionary <string, Script> methods,
            ISmartDictionary deviceState,
            ISmartDictionary deviceProperties,
            IScriptInterpreter scriptInterpreter)
        {
            if (methods == null)
            {
                return;
            }

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

            if (!this.methodsEnabled)
            {
                this.isRegistered = true;
                this.log.Debug("Skipping methods registration, methods are disabled in the global configuration.");
                return;
            }

            this.deviceId             = deviceId;
            this.cloudToDeviceMethods = methods;
            this.deviceState          = deviceState;
            this.deviceProperties     = deviceProperties;

            this.log.Debug("Setting up methods for device.", () => new
            {
                this.deviceId,
                methodsCount = this.cloudToDeviceMethods.Count
            });

            // walk this list and add a method handler for each method specified
            foreach (var item in this.cloudToDeviceMethods)
            {
                this.log.Debug("Setting up method for device.", () => new { item.Key, this.deviceId });

                await client.SetMethodHandlerAsync(item.Key, this.ExecuteMethodAsync, scriptInterpreter);

                this.log.Debug("Method for device setup successfully", () => new
                {
                    this.deviceId,
                    methodName = item.Key
                });
            }

            this.isRegistered = true;
        }
예제 #12
0
        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);
        }
예제 #13
0
        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);
        }
예제 #14
0
        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);
        }
예제 #15
0
        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);
        }
예제 #16
0
        public void ItCanDetectUnchangedValues()
        {
            // Arrange
            this.target = this.GetTestProperties();
            var key   = Guid.NewGuid().ToString();
            var value = Guid.NewGuid().ToString();

            this.target.Set(key, value, false);
            this.target.ResetChanged();

            // Act - Assert
            this.target.Set(key, value, true);
            Assert.False(this.target.Changed);

            // Assert
            this.target.Set(key, value, false);
            Assert.True(this.target.Changed);
        }
        /// <summary>
        /// Updates the reported properties in the device twin on the IoT Hub
        /// </summary>
        public async Task UpdatePropertiesAsync(ISmartDictionary properties)
        {
            try
            {
                var reportedProperties = this.SmartDictionaryToTwinCollection(properties);
                await this.client.UpdateReportedPropertiesAsync(reportedProperties);

                this.log.Debug("Update reported properties for device", () => new
                {
                    this.deviceId,
                    reportedProperties
                });
            }
            catch (KeyNotFoundException e)
            {
                // This exception sometimes occurs when calling UpdateReportedPropertiesAsync.
                // Still unknown why, apparently an issue with the internal AMQP library
                // used by IoT SDK. We need to collect extra information to report the issue.
                this.log.Error("Unexpected error, failed to update reported properties",
                               () => new
                {
                    Protocol  = this.protocol.ToString(),
                    Exception = e.GetType().FullName,
                    e.Message,
                    e.StackTrace,
                    e.Data,
                    e.Source,
                    e.TargetSite,
                    e.InnerException     // This appears to always be null in this scenario
                });
            }
            catch (Exception e)
            {
                this.log.Error("Failed to update reported properties",
                               () => new
                {
                    Protocol         = this.protocol.ToString(),
                    ExceptionMessage = e.Message,
                    Exception        = e.GetType().FullName,
                    e.InnerException
                });
            }
        }
예제 #18
0
        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"]);
        }
예제 #19
0
        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"]);
        }
        public async Task RegisterChangeUpdateAsync(string deviceId, ISmartDictionary deviceProperties)
        {
            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);
            }

            this.deviceId         = deviceId;
            this.deviceProperties = deviceProperties ?? new SmartDictionary();

            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(this.OnChangeCallback, null);

            this.log.Debug("Callback for desired properties updates setup successfully", () => new { this.deviceId });

            this.isRegistered = true;
        }
        // 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);
            }
        }
        public async Task RegisterMethodsAsync(
            string deviceId,
            IDictionary <string, Script> methods,
            ISmartDictionary deviceState,
            ISmartDictionary deviceProperties)
        {
            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);
            }

            this.deviceId             = deviceId;
            this.cloudToDeviceMethods = methods;
            this.deviceState          = deviceState;
            this.deviceProperties     = deviceProperties;

            this.log.Debug("Setting up methods for device.", () => new
            {
                this.deviceId,
                methodsCount = this.cloudToDeviceMethods.Count
            });

            // walk this list and add a method handler for each method specified
            foreach (var item in this.cloudToDeviceMethods)
            {
                this.log.Debug("Setting up method for device.", () => new { item.Key, this.deviceId });

                await this.client.SetMethodHandlerAsync(item.Key, this.ExecuteMethodAsync, null);

                this.log.Debug("Method for device setup successfully", () => new
                {
                    this.deviceId,
                    methodName = item.Key
                });
            }

            this.isRegistered = true;
        }
        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);
        }
        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);
        }
예제 #25
0
        public async Task RegisterChangeUpdateAsync(IDeviceClientWrapper client, string deviceId, ISmartDictionary deviceProperties)
        {
            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);
            }

            if (!this.deviceTwinEnabled)
            {
                this.isRegistered = true;
                this.log.Debug("Skipping twin notification registration, twin operations are disabled in the global configuration.");
                return;
            }

            this.deviceId         = deviceId;
            this.deviceProperties = deviceProperties ?? new SmartDictionary();

            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 client.SetDesiredPropertyUpdateCallbackAsync(this.OnChangeCallback, null);

            this.log.Debug("Callback for desired properties updates setup successfully", () => new { this.deviceId });

            this.isRegistered = true;
        }
예제 #26
0
 public SmartDictionaryTest(ITestOutputHelper log)
 {
     // Initialize device properties
     this.target = this.GetTestProperties();
 }
예제 #27
0
        /// <summary>
        /// Load a JS file and execute the main() function, passing in
        /// context information and the output from the previous execution.
        /// Modifies the internal device state with the latest values.
        /// </summary>
        public void Invoke(
            Script script,
            Dictionary <string, object> context,
            ISmartDictionary state,
            ISmartDictionary properties)
        {
            this.deviceState      = state;
            this.deviceProperties = properties;

            var engine = new Engine();

            // Inject the logger in the JS context, to allow the JS function
            // logging into the service logs
            engine.SetValue("log", new Action <object>(this.JsLog));

            // register callback for state updates
            engine.SetValue("updateState", new Action <JsValue>(this.UpdateState));

            // 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
            {
                Program program;
                bool    isInStorage = string.Equals(script.Path.Trim(),
                                                    DeviceModelScript.DeviceModelScriptPath.Storage.ToString(),
                                                    StringComparison.OrdinalIgnoreCase);
                string filename = isInStorage ? script.Id : script.Path;

                if (programs.ContainsKey(filename))
                {
                    program = programs[filename];
                }
                else
                {
                    // TODO: refactor the code to avoid blocking
                    //       https://github.com/Azure/device-simulation-dotnet/issues/240
                    var task = this.LoadScriptAsync(filename, isInStorage);
                    task.Wait(TimeSpan.FromSeconds(30));
                    var sourceCode = task.Result;

                    this.log.Debug("Compiling script source code", () => new { filename });
                    program = parser.Parse(sourceCode);
                    programs.Add(filename, program);
                }

                this.log.Debug("Executing JS function", () => new { filename });

                engine.Execute(program).Invoke(
                    "main",
                    context,
                    this.deviceState.GetAll(),
                    this.deviceProperties.GetAll(),
                    script.Params);

                this.log.Debug("JS function success", () => new { filename, this.deviceState });
            }
            catch (Exception e)
            {
                this.log.Error("JS function failure", e);
            }
        }
        /// <summary>
        /// Updates the reported properties in the device twin on the IoT Hub
        /// </summary>
        public async Task UpdatePropertiesAsync(ISmartDictionary properties)
        {
            var start = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();

            long GetTimeSpentMsecs() => DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() - start;

            try
            {
                var reportedProperties = this.SmartDictionaryToTwinCollection(properties);
                await this.client.UpdateReportedPropertiesAsync(reportedProperties);

                var timeSpentMsecs = GetTimeSpentMsecs();
                this.log.Debug("Update reported properties for device",
                               () => new { this.deviceId, timeSpentMsecs, reportedProperties });
            }
            catch (NullReferenceException)
            {
                // In case of multi-threaded access to the client, nothing to do
                if (this.client == null)
                {
                    return;
                }

                throw;
            }
            catch (KeyNotFoundException e)
            {
                // This exception sometimes occurs when calling UpdateReportedPropertiesAsync.
                // Still unknown why, apparently an issue with the internal AMQP library
                // used by IoT SDK. We need to collect extra information to report the issue.
                // The exception is logged differently than usual to capture info that might help fixing the SDK.
                var timeSpentMsecs = GetTimeSpentMsecs();
                this.log.Error("Unexpected error, failed to update reported properties",
                               () => new
                {
                    timeSpentMsecs,
                    this.deviceId,
                    Protocol  = this.protocol.ToString(),
                    Exception = e.GetType().FullName,
                    e.Message,
                    e.StackTrace,
                    e.Data,
                    e.Source,
                    e.TargetSite,
                    e.InnerException     // This appears to always be null in this scenario
                });
                throw new BrokenDeviceClientException("Unexpected error, failed to update reported properties", e);
            }
            catch (TimeoutException e)
            {
                // Note: this exception can occur in case of throttling, and
                // the caller should not recreate the client

                var timeSpentMsecs = GetTimeSpentMsecs();
                this.log.Error("Reported properties update timed out",
                               () => new { timeSpentMsecs, this.deviceId, Protocol = this.protocol.ToString(), e });

                throw new PropertySendException("Reported properties update timed out", e);
            }
            catch (IotHubCommunicationException e)
            {
                // Note: this exception can occur in case of throttling, and
                // the caller should not recreate the client

                var timeSpentMsecs = GetTimeSpentMsecs();
                this.log.Error("Failed to update reported properties",
                               () => new { timeSpentMsecs, this.deviceId, Protocol = this.protocol.ToString(), e });

                throw new PropertySendException("Failed to update reported properties", e);
            }
            catch (Exception e)
            {
                var timeSpentMsecs = GetTimeSpentMsecs();
                this.log.Error("Failed to update reported properties",
                               () => new { timeSpentMsecs, this.deviceId, Protocol = this.protocol.ToString(), e });

                throw new BrokenDeviceClientException("Failed to update reported properties", e);
            }
        }