├── .github ├── ISSUE_TEMPLATE.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── device-connectivity ├── .vscode │ ├── launch.json │ ├── settings.json │ └── tasks.json ├── LICENSE.md ├── README.md ├── THIRD-PARTY-NOTICES.txt ├── appsettings.json ├── deviceConnectivity.csproj ├── models │ └── CustomTelemetryMessage.cs └── program.cs ├── digital-twins-samples.code-workspace └── occupancy-quickstart ├── .vscode ├── launch.json ├── settings.json └── tasks.json ├── LICENSE.md ├── README.md ├── docs └── faq.md ├── src ├── actions │ ├── actionNames.cs │ ├── createEndpoints.cs │ ├── createEndpoints.yaml │ ├── createRoleAssignments.cs │ ├── createRoleAssignments.yaml │ ├── descriptions.cs │ ├── descriptionsExtensions.cs │ ├── getAvailableAndFreshSpaces.cs │ ├── getCreationSummary.cs │ ├── getSpaces.cs │ ├── provisionResults.cs │ ├── provisionSample.cs │ ├── provisionSample.yaml │ └── userDefinedFunctions │ │ ├── availability.js │ │ ├── availabilityForTutorial.js │ │ └── multiplemotionsensors.js ├── api │ ├── README.md │ ├── create.cs │ ├── find.cs │ ├── get.cs │ ├── isResourceProvisioning.cs │ └── update.cs ├── appSettings.cs ├── appSettings.json ├── auth.cs ├── loggers.cs ├── loggingHttpHandler.cs ├── models │ ├── README.md │ ├── device.cs │ ├── deviceCreate.cs │ ├── endpointsCreate.cs │ ├── historicalValues.cs │ ├── keyStoreCreate.cs │ ├── matcher.cs │ ├── matcherCreate.cs │ ├── ontology.cs │ ├── property.cs │ ├── propertyCreate.cs │ ├── propertyKeyCreate.cs │ ├── propertyKeys.cs │ ├── resource.cs │ ├── resourceCreate.cs │ ├── roleAssignmentCreate.cs │ ├── sensor.cs │ ├── sensorCreate.cs │ ├── sensorValues.cs │ ├── space.cs │ ├── spaceCreate.cs │ ├── userDefinedFunction.cs │ ├── userDefinedFunctionCreate.cs │ └── userDefinedFunctionUpdate.cs ├── occupancyQuickstart.csproj └── program.cs └── tests ├── createEndpointsTests.cs ├── createRoleAssignmentsTests.cs ├── fakeDigitalTwinsHttpClient.cs ├── fakeHttpHandler.cs ├── loggers.cs ├── occupancyQuickstart.tests.csproj ├── provisionSampleDevicesTests.cs ├── provisionSampleMatchersTests.cs ├── provisionSampleResourcesTests.cs ├── provisionSampleRoleAssignmentsTests.cs ├── provisionSampleSensorsTests.cs ├── provisionSampleSpacesTests.cs ├── provisionSampleUserDefinedFunctionsTests.cs ├── responses.cs └── userDefinedFunctions ├── function1.js └── function2.js /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Description 2 | 3 | Please provide a succinct description of your issue. Please include which sample (ie top level folder) this is referring to. 4 | 5 | ### Repro steps 6 | 7 | Please provide the steps required to reproduce the problem. 8 | 9 | 1. Step A 10 | 11 | 2. Step B 12 | 13 | ### Expected behavior 14 | 15 | Please provide a description of the behavior you expect. 16 | 17 | ### Actual behavior 18 | 19 | Please provide a description of the actual behavior you observe. 20 | 21 | ### Logs 22 | 23 | If applicable, please provide any logs or output from the sample. 24 | 25 | ### Known workarounds 26 | 27 | Please provide a description of any known workarounds. 28 | 29 | ### Related information 30 | 31 | * Operating system 32 | * CoreCLR version (run `dotnet --version`) 33 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Purpose 2 | 3 | Please describe the intention of the changes being proposed. What problem does it solve or functionality does it add? 4 | 5 | ## Pull Request Type 6 | 7 | What kind of change does this Pull Request introduce? 8 | 9 | 10 | ``` 11 | [ ] Bugfix 12 | [ ] New sample or new feature within a sample 13 | [ ] Code style update (formatting, naming) 14 | [ ] Refactoring (no functional changes) 15 | [ ] Documentation content changes 16 | [ ] Other... Please describe: 17 | ``` 18 | 19 | ## Test By 20 | 21 | 28 | 29 | Please describe how you tested 30 | 31 | ## Other Information 32 | 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.suo 8 | *.user 9 | *.userosscache 10 | *.sln.docstates 11 | 12 | # Build results 13 | [Dd]ebug/ 14 | [Dd]ebugPublic/ 15 | [Rr]elease/ 16 | [Rr]eleases/ 17 | x64/ 18 | x86/ 19 | bld/ 20 | [Bb]in/ 21 | [Oo]bj/ 22 | [Ll]og/ 23 | 24 | # Visual Studio 2015/2017 cache/options directory 25 | .vs/ 26 | 27 | # Visual Studio 2017 auto generated files 28 | Generated\ Files/ 29 | 30 | # .NET Core 31 | project.lock.json 32 | project.fragment.lock.json 33 | artifacts/ 34 | **/Properties/launchSettings.json 35 | 36 | # NuGet Packages 37 | *.nupkg 38 | # The packages folder can be ignored because of Package Restore 39 | **/[Pp]ackages/* 40 | # except build/, which is used as an MSBuild target. 41 | !**/[Pp]ackages/build/ 42 | # Uncomment if necessary however generally it will be regenerated when needed 43 | #!**/[Pp]ackages/repositories.config 44 | # NuGet v3's project.json files produces more ignorable files 45 | *.nuget.props 46 | *.nuget.targets 47 | 48 | # Visual Studio cache files 49 | # files ending in .cache can be ignored 50 | *.[Cc]ache 51 | # but keep track of directories ending in .cache 52 | !*.[Cc]ache/ 53 | 54 | # OSX 55 | .DS_Store 56 | 57 | # Others 58 | ClientBin/ 59 | ~$* 60 | *~ 61 | *.dbmdl 62 | *.dbproj.schemaview 63 | *.jfm 64 | *.pfx 65 | *.publishsettings 66 | orleans.codegen.cs 67 | appsettings.dev.json 68 | .accesstoken 69 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | This project welcomes contributions and suggestions. Most contributions require you to agree to a 4 | Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us 5 | the rights to use your contribution. For details, visit https://cla.microsoft.com. 6 | 7 | When you submit a pull request, a CLA-bot will automatically determine whether you need to provide 8 | a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the instructions 9 | provided by the bot. You will only need to do this once across all repos using our CLA. 10 | 11 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 12 | For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or 13 | contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 14 | 15 | - [Ask a Question](#askaquestion) 16 | - [File a Bug](#bug) 17 | - [Request a Feature](#feature) 18 | - [Contribute Changes](#contribute) 19 | 20 | ## Ask a question 21 | We monitor the Github issues section specifically for bugs found with our samples, however we will reply to questions asked using Github issues too. 22 | 23 | The Azure Digital Twins team monitors Stack Overflow via the [azure-digital-twins](http://stackoverflow.com/questions/tagged/azure-digital-twins) tag. It is a great place to ask questions about [Azure Digital Twins](https://azure.microsoft.com/en-us/services/digital-twins/). 24 | 25 | ## File a bug (code or documentation) 26 | 27 | Bugs are definitely something we want to hear about! Please [open an issue](https://github.com/Azure-Samples/digital-twins-samples-csharp/issues/new) on github and we'll try address it as fast as possible. When creating the issue, please help us by filling out as much of the requested info as possible. 28 | 29 | Our samples are entirely open-source and we do accept pull-requests if you feel like taking a stab at fixing the bug. Please be sure to file an issues first and mention the relevant issue number in your pull request description. 30 | 31 | ## Request a Feature 32 | You can *request* a new feature by [submitting an issue](https://github.com/Azure-Samples/digital-twins-samples-csharp/issues/new) to the GitHub Repository. If you would like to *implement* a new feature, please submit an issue with 33 | a proposal for your work first, to be sure that we can use it. 34 | 35 | ## Contribute Changes 36 | 37 | We try to maintain a high bar for code quality and maintainability, we request that all new code samples are accompanied by documentation, example scenarios, and tests (for non trivial samples). 38 | 39 | If you feel like your contribution is going to be a major effort, before you spend the time, just check with us to make sure your plans and ours are in sync. Just [open an issue](https://github.com/Azure-Samples/digital-twins-samples-csharp/issues/new) on github and tag it "enhancement" or "feature request" 40 | 41 | Thank you for your contributions! 42 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Microsoft Corporation. All rights reserved. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | --- 2 | page_type: sample 3 | languages: 4 | - csharp 5 | products: 6 | - azure 7 | - azure-digital-twins 8 | name: Digital Twins Samples (ARCHIVED) 9 | description: "For the older version of Azure Digital Twins: This repo contains .NET Core samples that demonstrate how to use the Azure Digital Twins platform. Each folder contains a separate .NET Core app." 10 | --- 11 | 12 | # Digital Twins Samples (ARCHIVED) 13 | 14 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 15 | 16 | ************************************************** 17 | **NOTE (July 2020): A new version of the Azure Digital Twins service has been released, with new features and new implementation details. This sample repository corresponds to the previous set of documentation, which has now been [archived](https://docs.microsoft.com/previous-versions/azure/digital-twins/about-digital-twins). It no longer reflects the current Azure Digital Twins best practices and is no longer being maintained.** 18 | 19 | **To view the latest information for the new service, visit the active [Azure Digital Twins Documentation](https://docs.microsoft.com/azure/digital-twins/) and its [samples](https://docs.microsoft.com/samples/azure-samples/digital-twins-samples/digital-twins-samples/).** 20 | ************************************************** 21 | 22 | This repo contains .NET Core samples that demonstrate how to use the Azure Digital Twins platform. Each folder contains a separate .NET Core app. 23 | 24 | See the `README.md` in each sub-folder for specific details about each app: 25 | 26 | * [Occupancy sample](https://github.com/Azure-Samples/digital-twins-samples-csharp/tree/master/occupancy-quickstart/README.md) 27 | * [Device Connectivity sample](https://github.com/Azure-Samples/digital-twins-samples-csharp/tree/master/device-connectivity/README.md) 28 | 29 | ## Get Started 30 | 31 | 1. [Install dotnet core](https://www.microsoft.com/net/download). 32 | 1. Clone the repo: 33 | 34 | ```shell 35 | git clone https://github.com/Azure-Samples/digital-twins-samples-csharp.git 36 | cd digital-twins-samples-csharp 37 | ``` 38 | 39 | The repo contains several standalone projects: 40 | 41 | * The [Occupancy](https://github.com/Azure-Samples/digital-twins-samples-csharp/tree/master/occupancy-quickstart/README.md) sample is suggested as a first example to gain familiarity with Digital Twins. 42 | * The [Device Connectivity](https://github.com/Azure-Samples/digital-twins-samples-csharp/tree/master/device-connectivity/README.md) sample demonstrates how to connect a device to Digital Twins and submit sensory data. 43 | 44 | For corresponding documentation, please see the project `README's` above. 45 | 46 | ## Visual Studio Code 47 | 48 | A [workspace file](https://github.com/Azure-Samples/digital-twins-samples-csharp/blob/master/digital-twins-samples.code-workspace) containing all the apps is included for [Visual Studio Code](https://code.visualstudio.com/) users. 49 | 50 | Alternatively, each app can be opened individually. 51 | 52 | ## Licensing and Use 53 | 54 | Azure Digital Twins Samples are [MIT Licensed](https://github.com/Azure-Samples/digital-twins-samples-csharp/blob/master/LICENSE.md). 55 | 56 | ## See also 57 | 58 | * Azure Digital Twins [product documentation](https://docs.microsoft.com/azure/digital-twins/) 59 | 60 | * Azure Digital Twins [User-defined functions client library reference](https://docs.microsoft.com/azure/digital-twins/reference-user-defined-functions-client-library) 61 | -------------------------------------------------------------------------------- /device-connectivity/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to find out which attributes exist for C# debugging 3 | // Use hover for the description of the existing attributes 4 | // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": ".NET Core Launch (console)", 9 | "type": "coreclr", 10 | "request": "launch", 11 | "preLaunchTask": "build", 12 | // If you have changed target frameworks, make sure to update the program path. 13 | "program": "${workspaceFolder}/bin/Debug/netcoreapp2.1/deviceConnectivity.dll", 14 | "args": ["GetSpaces"], 15 | "cwd": "${workspaceFolder}", 16 | // For more information about the 'console' field, see https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md#console-terminal-window 17 | "console": "internalConsole", 18 | "stopAtEntry": false, 19 | "internalConsoleOptions": "openOnSessionStart" 20 | }, 21 | { 22 | "name": ".NET Core Attach", 23 | "type": "coreclr", 24 | "request": "attach", 25 | "processId": "${command:pickProcess}" 26 | } 27 | ,] 28 | } -------------------------------------------------------------------------------- /device-connectivity/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.exclude": { 3 | "**/bin/": true, 4 | "**/obj/": true 5 | } 6 | } -------------------------------------------------------------------------------- /device-connectivity/.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "build", 6 | "command": "dotnet", 7 | "type": "process", 8 | "args": [ 9 | "build", 10 | "${workspaceFolder}/" 11 | ], 12 | "problemMatcher": "$msCompile" 13 | } 14 | ] 15 | } -------------------------------------------------------------------------------- /device-connectivity/LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Microsoft Corporation. All rights reserved. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE -------------------------------------------------------------------------------- /device-connectivity/README.md: -------------------------------------------------------------------------------- 1 | # Digital Twins Device Connectivity Sample 2 | 3 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![Contribute](https://img.shields.io/badge/PR%27s-welcome-brightgreen.svg)](../CONTRIBUTING.md) 4 | 5 | This introductory sample demonstrates how to connect a device to Digital Twins and submit sample sensory data. 6 | 7 | ## Configure the app 8 | 9 | Edit your settings file to fill in the following values: 10 | 11 | * `DeviceConnectionString` 12 | * `Sensors` 13 | 14 | ### DeviceConnectionString 15 | 16 | You can get the connection string by calling Management API's Devices controller. E.g. for a device with ID `22215ed9-91e8-40af-9d1b-a22727849393`: 17 | 18 | ```plaintext 19 | GET https://{{your-instance-name}}.westcentralus.azuresmartspaces.net/management/api/v1.0/devices/22215ed9-91e8-40af-9d1b-a22727849393?includes=ConnectionString 20 | ``` 21 | 22 | ```JSON 23 | { 24 | "name": "My Sample Device", 25 | "typeId": 2, 26 | "subtypeId": 1, 27 | "hardwareId": "00AABBCCDDEE", 28 | "spaceId": "ef46f69f-0b95-45ee-a41f-718b3ee4c355", 29 | "status": "Active", 30 | "id": "22215ed9-91e8-40af-9d1b-a22727849393", 31 | "connectionString": "Hostname=..." 32 | } 33 | ``` 34 | 35 | Copy the `connectionString` over to the [appsettings.json](./appsettings.json) file: 36 | 37 | ```plaintext 38 | { 39 | "Settings": { 40 | "DeviceConnectionString": "Hostname=...", 41 | ... 42 | } 43 | } 44 | ``` 45 | 46 | ### Sensors 47 | 48 | An array of one or more Sensors you wish to send data for using this sample. You can get the `dataType` and `hardwareId` of each Sensor by calling Management API's Sensors controller. E.g. for a sensor with ID `c4cf2f41-edd6-4fc3-a47a-6bedab6470db`: 49 | 50 | ```plaintext 51 | GET https://{{your-instance-name}}.westcentralus.azuresmartspaces.net/management/api/v1.0/sensors/c4cf2f41-edd6-4fc3-a47a-6bedab6470db?includes=Types 52 | ``` 53 | 54 | ```JSON 55 | { 56 | "dataType":"Motion", 57 | "dataTypeId":27, 58 | "deviceId":"f51e5295-ca81-4517-8f5d-0b940f678ef2", 59 | "id": "c4cf2f41-edd6-4fc3-a47a-6bedab6470db", 60 | "hardwareId":"1234ABC", 61 | "name": "My Sample Sensor", 62 | "spaceId": "ef46f69f-0b95-45ee-a41f-718b3ee4c355" 63 | } 64 | ``` 65 | 66 | Copy the `hardwareId` and `dataType` over to be one entry in the `Sensors` array in the [appsettings.json](./appsettings.json) file: 67 | 68 | ```JSON 69 | { 70 | "Settings": { 71 | "Sensors": [{ 72 | "DataType": "Motion", 73 | "HardwareId": "1234ABC" 74 | },{ 75 | "DataType": "CarbonDioxide", 76 | "HardwareId": "SOMEOTHERID" 77 | }] 78 | } 79 | } 80 | ``` 81 | 82 | ## Build and Run the app 83 | 84 | 1. [Install .NET Core SDK](https://www.microsoft.com/net/core) on your execution platform. 85 | 1. Run the following commands to build the app: 86 | 87 | ```shell 88 | dotnet restore 89 | dotnet build 90 | ``` 91 | 1. Run the app: 92 | 93 | ```shell 94 | dotnet run 95 | ``` 96 | 97 | ## Platform-specific instructions 98 | 99 | Instructions specific to your execution platform. 100 | 101 | ### Build and run the app on a Raspberry Pi 102 | 103 | Choose `win-arm` or `linux-arm` for your platform: 104 | 105 | * Run the following: 106 | 107 | ```shell 108 | dotnet publish -r 109 | ``` 110 | 111 | Under either: 112 | 113 | * `./bin/Debug/netcoreapp2.0/{{your-runtime-identifier}}/publish` 114 | * `.\bin\Debug\netcoreapp2.0\{{your-runtime-identifier}}\publish` 115 | 116 | You will see the whole self contained app that you need to copy to your Raspberry Pi. 117 | 118 | ### Windows 10 IoT 119 | 120 | * Run on device by invoking the executable. 121 | 122 | ### Linux 123 | 124 | 1. Install [Linux](https://www.raspberrypi.org/downloads/) on your Pi. 125 | 1. Install the [platform dependencies](https://github.com/dotnet/core/blob/master/Documentation/prereqs.md) for .NET Core through your distribution's package manager. 126 | 1. Run: 127 | 128 | ```bash 129 | chmod 755 ./device-connectivity 130 | ``` 131 | 132 | ## Customizing the app to your needs 133 | 134 | This app compiles a sample telemetry message with some randomly generated data. You can customize the message format as well as the payload by modifying [models/CustomTelemetryMessage.cs](./models/CustomTelemetryMessage.cs). The data contract of the model can be changed to any serializable format or you can choose to compile your own payload by generating a byte array or stream that can be passed to [Message(byte[] byteArray)](https://docs.microsoft.com/en-us/dotnet/api/microsoft.azure.devices.client.message.-ctor?view=azure-dotnet#Microsoft_Azure_Devices_Client_Message__ctor_System_Byte___), as found in the `SendEvent` method. You will have to maintain a set of properties to ensure your message keeps getting routed appropriately, as listed below. 135 | 136 | ### Telemetry Properties 137 | 138 | While the payload contents of a `Message` can be arbitrary data, up to 256kb in size, there are a few requirements on expected [Message.Properties](https://docs.microsoft.com/dotnet/api/microsoft.azure.devices.client.message.properties?view=azure-dotnet). The following is the list of required and optional properties supported by the system: 139 | 140 | | Property Name | Value | Required | Description | 141 | |---------------|-------|----------|-------------| 142 | | DigitalTwins-Telemetry | 1.0 | yes | A constant value that identifies a message to the system | 143 | | DigitalTwins-SensorHardwareId | `string(72)` | yes | A unique identifier pointing to a Sensor in the topology for which the `Message` is meant for. This value must match an object's `HardwareId` property for the system to process it. E.g.: `00FF0643BE88-PIR` | 144 | | CreationTimeUtc | `string` | no | An [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) formatted date string identifying the sampling time of the payload. E.g.: `2018-09-20T07:35:00.8587882-07:00` | 145 | | CorrelationId | `string` | no | A `uuid` formatted as a string that can be used to trace events across the system. E.g.: `cec16751-ab27-405d-8fe6-c68e1412ce1f`| 146 | 147 | ## Licensing and Use 148 | 149 | Azure Digital Twins Samples are [MIT Licensed](./LICENSE.md). 150 | -------------------------------------------------------------------------------- /device-connectivity/THIRD-PARTY-NOTICES.txt: -------------------------------------------------------------------------------- 1 | THIRD-PARTY SOFTWARE NOTICES AND INFORMATION 2 | Do Not Translate or Localize 3 | 4 | This project incorporates components from the projects listed below. The original copyright notices 5 | and licenses under which Microsoft received such components are set forth below. Microsoft reserves all rights not 6 | expressly granted herein, whether by implication, estoppel or otherwise. 7 | 8 | 1. Newtonsoft.Json (https://github.com/JamesNK/Newtonsoft.Json) 9 | 2. YamlDotNet (https://github.com/aaubry/YamlDotNet) 10 | 11 | 12 | newtonsoft.json NOTICES AND INFORMATION BEGIN HERE 13 | ========================================= 14 | The MIT License (MIT) 15 | 16 | Copyright (c) 2007 James Newton-King 17 | 18 | Permission is hereby granted, free of charge, to any person obtaining a copy of 19 | this software and associated documentation files (the "Software"), to deal in 20 | the Software without restriction, including without limitation the rights to 21 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 22 | the Software, and to permit persons to whom the Software is furnished to do so, 23 | subject to the following conditions: 24 | 25 | The above copyright notice and this permission notice shall be included in all 26 | copies or substantial portions of the Software. 27 | 28 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 29 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 30 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 31 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 32 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 33 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 34 | ========================================= 35 | END OF newtonsoft.json NOTICES AND INFORMATION 36 | 37 | 38 | YamlDotNet NOTICES AND INFORMATION BEGIN HERE 39 | ========================================= 40 | Copyright (c) 2008, 2009, 2010, 2011, 2012, 2013, 2014 Antoine Aubry and contributors 41 | 42 | Permission is hereby granted, free of charge, to any person obtaining a copy of 43 | this software and associated documentation files (the "Software"), to deal in 44 | the Software without restriction, including without limitation the rights to 45 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 46 | of the Software, and to permit persons to whom the Software is furnished to do 47 | so, subject to the following conditions: 48 | 49 | The above copyright notice and this permission notice shall be included in all 50 | copies or substantial portions of the Software. 51 | 52 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 53 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 54 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 55 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 56 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 57 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 58 | SOFTWARE. 59 | ========================================= 60 | END OF YamlDotNet NOTICES AND INFORMATION 61 | -------------------------------------------------------------------------------- /device-connectivity/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Settings": { 3 | "DeviceConnectionString": "", 4 | "MessageIntervalInSeconds": 5, 5 | "Sensors": [{ 6 | "DataType": "Motion", 7 | "HardwareId": "SAMPLE_SENSOR_MOTION" 8 | },{ 9 | "DataType": "CarbonDioxide", 10 | "HardwareId": "SAMPLE_SENSOR_CARBONDIOXIDE" 11 | }] 12 | } 13 | } -------------------------------------------------------------------------------- /device-connectivity/deviceConnectivity.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | netcoreapp2.1 6 | latest 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | PreserveNewest 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /device-connectivity/models/CustomTelemetryMessage.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System; 5 | using System.Runtime.Serialization; 6 | 7 | namespace Microsoft.Azure.DigitalTwins.Samples.Models 8 | { 9 | [DataContract(Name="CustomTelemetryMessage")] 10 | public class CustomTelemetryMessage 11 | { 12 | [DataMember(Name="SensorValue")] 13 | public string SensorValue { get; set; } 14 | } 15 | } -------------------------------------------------------------------------------- /device-connectivity/program.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Globalization; 7 | using System.IO; 8 | using System.Linq; 9 | using System.Net.NetworkInformation; 10 | using System.Runtime.Serialization.Json; 11 | using System.Text; 12 | using System.Threading; 13 | using System.Threading.Tasks; 14 | using Microsoft.Azure.Devices.Client; 15 | using Microsoft.Azure.DigitalTwins.Samples.Models; 16 | using Microsoft.Extensions.Configuration; 17 | using Microsoft.Extensions.Configuration.Binder; 18 | 19 | namespace Microsoft.Azure.DigitalTwins.Samples 20 | { 21 | class Program 22 | { 23 | private static Random rnd = new Random(); 24 | private static IConfigurationSection settings; 25 | static void Main(string[] args) 26 | { 27 | if (args.Length != 0) 28 | { 29 | Console.WriteLine("Usage: dotnet run\nNo arguments are supported"); 30 | return; 31 | } 32 | 33 | settings = new ConfigurationBuilder() 34 | .SetBasePath(Directory.GetCurrentDirectory()) 35 | .AddJsonFile("appsettings.json") 36 | .Build() 37 | .GetSection("Settings"); 38 | 39 | try 40 | { 41 | DeviceClient deviceClient = DeviceClient.CreateFromConnectionString(settings["DeviceConnectionString"]); 42 | 43 | if (deviceClient == null) 44 | { 45 | Console.WriteLine("ERROR: Failed to create DeviceClient!"); 46 | return; 47 | } 48 | 49 | SendEvent(deviceClient).Wait(); 50 | } 51 | catch (Exception ex) 52 | { 53 | Console.WriteLine("EXIT: Unexpected error: {0}", ex.Message); 54 | } 55 | } 56 | 57 | static Func CreateGetRandomSensorReading(string sensorDataType, int iteration) 58 | { 59 | switch (sensorDataType) 60 | { 61 | default: 62 | throw new Exception($"Unsupported configuration: SensorDataType, '{sensorDataType}'. Please check your appsettings.json."); 63 | case "Motion": 64 | if (iteration % 6 < 3) 65 | return () => "false"; 66 | else 67 | return () => "true"; 68 | 69 | case "Temperature": 70 | return () => rnd.Next(70, 100).ToString(CultureInfo.InvariantCulture); 71 | case "CarbonDioxide": 72 | if (iteration % 6 < 3) 73 | return () => rnd.Next(800, 1000).ToString(CultureInfo.InvariantCulture); 74 | else 75 | return () => rnd.Next(1000, 1100).ToString(CultureInfo.InvariantCulture); 76 | } 77 | } 78 | 79 | static async Task SendEvent(DeviceClient deviceClient) 80 | { 81 | var serializer = new DataContractJsonSerializer(typeof(CustomTelemetryMessage)); 82 | 83 | var sensors = settings.GetSection("Sensors").Get(); 84 | 85 | var delayPerMessageSend = int.Parse(settings["MessageIntervalInSeconds"]); 86 | var countOfSendsPerIteration = sensors.Length; 87 | var maxSecondsToRun = 15 * 60; 88 | var maxIterations = maxSecondsToRun / countOfSendsPerIteration / delayPerMessageSend; 89 | var curIteration = 0; 90 | 91 | do { 92 | foreach (var sensor in sensors) 93 | { 94 | var getRandomSensorReading = CreateGetRandomSensorReading(sensor.DataType, curIteration); 95 | var telemetryMessage = new CustomTelemetryMessage() 96 | { 97 | SensorValue = getRandomSensorReading(), 98 | }; 99 | 100 | using (var stream = new MemoryStream()) 101 | { 102 | serializer.WriteObject(stream, telemetryMessage); 103 | var byteArray = stream.ToArray(); 104 | Message eventMessage = new Message(byteArray); 105 | eventMessage.Properties.Add("DigitalTwins-Telemetry", "1.0"); 106 | eventMessage.Properties.Add("DigitalTwins-SensorHardwareId", $"{sensor.HardwareId}"); 107 | eventMessage.Properties.Add("CreationTimeUtc", DateTime.UtcNow.ToString("o")); 108 | eventMessage.Properties.Add("x-ms-client-request-id", Guid.NewGuid().ToString()); 109 | 110 | Console.WriteLine($"\t{DateTime.UtcNow.ToLocalTime()}> Sending message: {Encoding.UTF8.GetString(eventMessage.GetBytes())} Properties: {{ {eventMessage.Properties.Aggregate(new StringBuilder(), (sb, x) => sb.Append($"'{x.Key}': '{x.Value}',"), sb => sb.ToString())} }}"); 111 | 112 | await deviceClient.SendEventAsync(eventMessage); 113 | } 114 | } 115 | 116 | await Task.Delay(TimeSpan.FromSeconds(delayPerMessageSend)); 117 | 118 | } while (++curIteration < maxIterations); 119 | 120 | Console.WriteLine($"Finished sending {curIteration} events (per sensor type)"); 121 | } 122 | } 123 | 124 | public class Sensor 125 | { 126 | public string DataType { get; set; } 127 | public string HardwareId { get; set; } 128 | } 129 | } -------------------------------------------------------------------------------- /digital-twins-samples.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "path": "occupancy-quickstart" 5 | }, 6 | { 7 | "path": "device-connectivity" 8 | } 9 | ], 10 | "settings": {} 11 | } -------------------------------------------------------------------------------- /occupancy-quickstart/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to find out which attributes exist for C# debugging 3 | // Use hover for the description of the existing attributes 4 | // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": ".NET Core Launch (console)", 9 | "type": "coreclr", 10 | "request": "launch", 11 | "preLaunchTask": "build", 12 | // If you have changed target frameworks, make sure to update the program path. 13 | "program": "${workspaceFolder}/src/bin/Debug/netcoreapp2.1/occupancyQuickstart.dll", 14 | "args": ["GetSpaces"], 15 | "cwd": "${workspaceFolder}/src", 16 | // For more information about the 'console' field, see https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md#console-terminal-window 17 | "console": "internalConsole", 18 | "stopAtEntry": false, 19 | "internalConsoleOptions": "openOnSessionStart" 20 | }, 21 | { 22 | "name": ".NET Core Attach", 23 | "type": "coreclr", 24 | "request": "attach", 25 | "processId": "${command:pickProcess}" 26 | } 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /occupancy-quickstart/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.exclude": { 3 | "**/bin/": true, 4 | "**/obj/": true 5 | } 6 | } -------------------------------------------------------------------------------- /occupancy-quickstart/.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "build", 6 | "command": "dotnet", 7 | "type": "process", 8 | "args": [ 9 | "build", 10 | "${workspaceFolder}/src" 11 | ], 12 | "problemMatcher": "$msCompile" 13 | }, 14 | { 15 | "label": "test", 16 | "command": "dotnet", 17 | "type": "process", 18 | "args": [ 19 | "test", 20 | "${workspaceFolder}/tests" 21 | ], 22 | "problemMatcher": "$msCompile" 23 | } 24 | ] 25 | } -------------------------------------------------------------------------------- /occupancy-quickstart/LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Microsoft Corporation. All rights reserved. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE -------------------------------------------------------------------------------- /occupancy-quickstart/README.md: -------------------------------------------------------------------------------- 1 | # Occupancy Sample 2 | 3 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![Contribute](https://img.shields.io/badge/PR%27s-welcome-brightgreen.svg)](../CONTRIBUTING.md) 4 | 5 | Sample code to provision and read resources in a Digital Twins topology via management APIs. It also creates an example function that runs within the Digital Twins instance that computes motion from a sensor in a room in order to determine the occupancy. 6 | 7 | ## Build and Run the Sample 8 | 9 | Below are some details on how to get up and running. For a more detailed walkthrough or more details on how to get the values described below, please see this [quickstart doc](https://docs.microsoft.com/azure/digital-twins/quickstart-view-occupancy-dotnet). 10 | 11 | ### Update appSettings.json 12 | 13 | [appSettings.json](./src/appSettings.json) is used to specify info on which Digital Twins instance to connect to. The three fields you will need to fill in are: 14 | 15 | - `ClientId`: The **application ID** of a native Azure Active Directory app that has permissions to call the Azure Digital Twins service. 16 | - `Tenant`: The **directory ID** of a your Azure Active Directory. 17 | - `BaseUrl`: The management api url to your Digital Twins instance (see `appSetting.json` for what this should look like). 18 | - `AadRedirectUri`: A valid **Redirect URI** configured for your Azure Active Directory app. We recommend using the default **Redirect URI** `http://www.localhost:8080`. However, you may also choose another port or domain as required. 19 | 20 | ### Use a shell 21 | 22 | 1. Run the app: 23 | 24 | ```shell 25 | cd src 26 | dotnet restore 27 | dotnet run 28 | ``` 29 | This will show usage. For a walkthrough of what you can do see [quickstart doc](https://docs.microsoft.com/azure/digital-twins/quickstart-view-occupancy-dotnet). 30 | 31 | 1. Run tests: 32 | 33 | ```shell 34 | dotnet test ../tests 35 | ``` 36 | 37 | ### Use Visual Studio Code 38 | 39 | 1. Open the 'occupancy-quickstart' folder in Visual Studio Code. 40 | 1. Run the app by using the `F5` key. You can change the command-line parameters in `launch.json`. 41 | 1. To build and run tests use the 'Run Task' command in Visual Studio Code and choose `test`. 42 | 43 | ## Notes 44 | 45 | ### Authentication 46 | 47 | To learn more about configuration, security, and API authentication: 48 | 49 | 1. See the [role-based access control doc](https://docs.microsoft.com/azure/digital-twins/security-role-based-access-control). 50 | 1. See the [Management API doc](https://docs.microsoft.com/azure/digital-twins/security-authenticating-apis). 51 | 52 | ## Problems 53 | 54 | If you run into problems or have questions checkout our [FAQ](./docs/faq.md). If that doesn't help search open and closed [issues](https://github.com/Azure-Samples/digital-twins-samples-csharp/issues) and open a new one if you can't find an answer. 55 | 56 | ## Licensing and Use 57 | 58 | Azure Digital Twins Samples are [MIT Licensed](./LICENSE.md). 59 | -------------------------------------------------------------------------------- /occupancy-quickstart/docs/faq.md: -------------------------------------------------------------------------------- 1 | ## Occupany Sample FAQ 2 | 3 | #### Exception: YamlDotNet.Core.SyntaxErrorException 4 | 5 | ``` 6 | Exception: YamlDotNet.Core.SyntaxErrorException: (Line: X, Col: Y, Idx: ZZ) - (Line: X, Col: Y, Idx: ZZ): While scanning for the next token, find character that cannot start any token. 7 | ``` 8 | 9 | Yaml files don't support tab characters. NOTE: that visual studio 2017 doesn't know (without customizations) to insert spaces instead of tabs into yaml files. More details [here](https://developercommunity.visualstudio.com/content/problem/71238/editor-inserts-tabs-instead-of-spaces-for-yaml-fil.html) 10 | 11 | 12 | #### Response Status: 404, NotFound 13 | 14 | ``` 15 | trce: DigitalTwinsQuickstart[0] 16 | Request: GET https://yourResource.yourLocation.azuresmartspaces.net/management/somethingWrong 17 | trce: DigitalTwinsQuickstart[0] 18 | Response Status: 404, NotFound, 19 | ``` 20 | 21 | A 404 usually indicates the `BaseUrl` in appSettings.json (or appSettings.dev.json) is misconfigured. 22 | 23 | 24 | #### Response Status: 401, Unauthorized 25 | 26 | There are several conditions that can cause a 401. Below are different errors that can be included as part of a 401 and possible corrections. 27 | 28 | ##### The request body must contain the following parameter: 'client_secret or client_assertion 29 | ``` 30 | trce: DigitalTwinsQuickstart[0] 31 | Response Status: 401, Unauthorized, {"message":"Authorization has been denied for this request."} 32 | To sign in, use a web browser to open the page https://microsoft.com/devicelogin and enter the code ZZZZZZZ to authenticate. 33 | Exception: Microsoft.IdentityModel.Clients.ActiveDirectory.AdalServiceException: AADSTS70002: The request body must contain the following parameter: 'client_secret or client_assertion'. 34 | ``` 35 | 36 | This usually indicates the aad app you are using (as represented by `ClientId` in appSettings.json or appSettings.dev.json) 37 | is an aad *web* app instead of an aad *native* app). 38 | 39 | 40 | #### Response Status: 403, Forbidden 41 | 42 | This usually is because the user account used for login (when prompted by this app) is not authenticated in the Digital Twins' Management Api. See [role-based access control](https://docs.microsoft.com/en-us/azure/digital-twins/security-role-based-access-control) for more info. -------------------------------------------------------------------------------- /occupancy-quickstart/src/actions/actionNames.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Microsoft.Azure.DigitalTwins.Samples 5 | { 6 | enum ActionName 7 | { 8 | CreateEndpoints, 9 | CreateRoleAssignments, 10 | GetAvailableAndFreshSpaces, 11 | GetOntologies, 12 | GetSpaces, 13 | ProvisionSample, 14 | } 15 | } -------------------------------------------------------------------------------- /occupancy-quickstart/src/actions/createEndpoints.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.IO; 7 | using System.Linq; 8 | using System.Net.Http; 9 | using System.Text; 10 | using System.Threading.Tasks; 11 | using Microsoft.Extensions.Logging; 12 | using Newtonsoft.Json; 13 | using YamlDotNet.Serialization; 14 | 15 | namespace Microsoft.Azure.DigitalTwins.Samples 16 | { 17 | public static partial class Actions 18 | { 19 | public static async Task> CreateEndpoints(HttpClient httpClient, ILogger logger) 20 | { 21 | IEnumerable endpointDescriptions; 22 | using (var r = new StreamReader("actions/createEndpoints.yaml")) 23 | { 24 | endpointDescriptions = await GetCreateEndpointsDescriptions(r); 25 | } 26 | 27 | var createdIds = (await CreateEndpoints(httpClient, logger, endpointDescriptions)).ToList(); 28 | 29 | Console.WriteLine($"CreateEndpoints completed. {GetCreationSummary("endpoint", "endpoints", createdIds)}"); 30 | 31 | return createdIds; 32 | } 33 | 34 | public static async Task> GetCreateEndpointsDescriptions(TextReader textReader) 35 | => new Deserializer().Deserialize>(await textReader.ReadToEndAsync()); 36 | 37 | public static async Task> CreateEndpoints( 38 | HttpClient httpClient, 39 | ILogger logger, 40 | IEnumerable descriptions) 41 | { 42 | var endpointIds = new List(); 43 | foreach (var description in descriptions) 44 | { 45 | endpointIds.Add(await Api.CreateEndpoints(httpClient, logger, description.ToEndpointCreate())); 46 | } 47 | return endpointIds.Where(id => id != Guid.Empty); 48 | } 49 | } 50 | } -------------------------------------------------------------------------------- /occupancy-quickstart/src/actions/createEndpoints.yaml: -------------------------------------------------------------------------------- 1 | - type: EventGrid 2 | eventTypes: 3 | - SensorChange 4 | - SpaceChange 5 | - TopologyOperation 6 | - UdfCustom 7 | connectionString: 8 | secondaryConnectionString: 9 | path: 10 | -------------------------------------------------------------------------------- /occupancy-quickstart/src/actions/createRoleAssignments.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.IO; 7 | using System.Linq; 8 | using System.Net.Http; 9 | using System.Text; 10 | using System.Threading.Tasks; 11 | using Microsoft.Extensions.Logging; 12 | using Newtonsoft.Json; 13 | using YamlDotNet.Serialization; 14 | 15 | namespace Microsoft.Azure.DigitalTwins.Samples 16 | { 17 | public static partial class Actions 18 | { 19 | public static async Task> CreateRoleAssignments(HttpClient httpClient, ILogger logger) 20 | { 21 | IEnumerable roleAssignmentsDescriptions; 22 | using (var r = new StreamReader("actions/createRoleAssignments.yaml")) 23 | { 24 | roleAssignmentsDescriptions = await GetCreateRoleAssignmentsDescriptions(r); 25 | } 26 | 27 | var createdIds = (await CreateRoleAssignments(httpClient, logger, roleAssignmentsDescriptions)).ToList(); 28 | 29 | Console.WriteLine($"CreateRoleAssignments completed: {GetCreationSummary("role assignment", "role assignments", createdIds)}"); 30 | 31 | return createdIds; 32 | } 33 | 34 | public static async Task> GetCreateRoleAssignmentsDescriptions(TextReader textReader) 35 | => new Deserializer().Deserialize>(await textReader.ReadToEndAsync()); 36 | 37 | public static async Task> CreateRoleAssignments( 38 | HttpClient httpClient, 39 | ILogger logger, 40 | IEnumerable descriptions) 41 | { 42 | var roleAssignmentIds = new List(); 43 | foreach (var description in descriptions) 44 | { 45 | roleAssignmentIds.Add(await Api.CreateRoleAssignment(httpClient, logger, description.ToRoleAssignmentCreate())); 46 | } 47 | 48 | return roleAssignmentIds.Where(id => id != Guid.Empty); 49 | } 50 | } 51 | } -------------------------------------------------------------------------------- /occupancy-quickstart/src/actions/createRoleAssignments.yaml: -------------------------------------------------------------------------------- 1 | # Examples of how to add a user as a space admin to the root. 2 | # See docs for more details or how to add other types to Digital Twins role-based access controls 3 | # To use: Substitute and with appropriate values from your AAD directory and run CreateRoleAssignments 4 | - objectId: # ObjectId (guid) of user to add 5 | objectIdType: UserId 6 | path: / 7 | roleId: 98e44ad7-28d4-4007-853b-b9968ad132d1 # System Role: SpaceAdministrator 8 | tenantId: # aka the AAD DirectoryId -------------------------------------------------------------------------------- /occupancy-quickstart/src/actions/descriptions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System.Collections.Generic; 5 | using Newtonsoft.Json; 6 | 7 | // Descriptions are a representation of a space topology that are specific to this app 8 | // They are an in memory version of the yaml file in this directory 9 | 10 | namespace Microsoft.Azure.DigitalTwins.Samples 11 | { 12 | public class DeviceDescription 13 | { 14 | public string hardwareId { get; set; } 15 | public string name { get; set; } 16 | 17 | [JsonIgnore] 18 | public IEnumerable sensors { get; set; } 19 | } 20 | 21 | public class EndpointDescription 22 | { 23 | public string type { get; set; } 24 | public string[] eventTypes { get; set; } 25 | public string connectionString { get; set; } 26 | public string secondaryConnectionString { get; set; } 27 | public string path { get; set; } 28 | } 29 | 30 | public class KeyStoreDescription 31 | { 32 | public string name { get; set; } 33 | } 34 | 35 | public class MatcherDescription 36 | { 37 | public string name { get; set; } 38 | public string dataTypeValue { get; set; } 39 | } 40 | 41 | public class ResourceDescription 42 | { 43 | public string type { get; set; } 44 | } 45 | 46 | public class RoleAssignmentDescription 47 | { 48 | public string objectId { get; set; } 49 | public string objectIdType { get; set; } 50 | public string objectName { get; set; } 51 | public string path { get; set; } 52 | public string roleId { get; set; } 53 | public string tenantId { get; set; } 54 | } 55 | 56 | public class SensorDescription 57 | { 58 | public string dataType { get; set; } 59 | public string hardwareId { get; set; } 60 | } 61 | 62 | public class UserDefinedFunctionDescription 63 | { 64 | public string name { get; set; } 65 | public IEnumerable matcherNames { get; set; } 66 | public string script { get; set; } 67 | } 68 | 69 | public class SpaceDescription 70 | { 71 | public string name { get; set; } 72 | public string type { get; set; } 73 | public string subType { get; set; } 74 | 75 | [JsonIgnore] 76 | public IEnumerable devices { get; set; } 77 | 78 | [JsonIgnore] 79 | public IEnumerable keystores { get; set; } 80 | 81 | [JsonIgnore] 82 | public IEnumerable matchers { get; set; } 83 | 84 | [JsonIgnore] 85 | public IEnumerable roleassignments { get; set; } 86 | 87 | [JsonIgnore] 88 | public IEnumerable resources { get; set; } 89 | 90 | [JsonIgnore] 91 | public IEnumerable spaces { get; set; } 92 | 93 | [JsonIgnore] 94 | public IEnumerable userdefinedfunctions { get; set; } 95 | } 96 | } -------------------------------------------------------------------------------- /occupancy-quickstart/src/actions/descriptionsExtensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | 8 | // These extensions translate the descriptions (which are in memory representations 9 | // of the sample yaml file) to Digital Twins models. 10 | 11 | namespace Microsoft.Azure.DigitalTwins.Samples 12 | { 13 | public static class DescriptionExtensions 14 | { 15 | public static Models.DeviceCreate ToDeviceCreate(this DeviceDescription description, Guid spaceId) 16 | => new Models.DeviceCreate() 17 | { 18 | HardwareId = description.hardwareId, 19 | Name = description.name, 20 | SpaceId = spaceId.ToString(), 21 | }; 22 | 23 | public static Models.EndpointsCreate ToEndpointCreate(this EndpointDescription description) 24 | => new Models.EndpointsCreate() 25 | { 26 | ConnectionString = description.connectionString, 27 | EventTypes = description.eventTypes, 28 | Path = description.path, 29 | SecondaryConnectionString = description.secondaryConnectionString, 30 | Type = description.type, 31 | }; 32 | 33 | public static Models.MatcherCreate ToMatcherCreate(this MatcherDescription description, Guid spaceId) 34 | => new Models.MatcherCreate() 35 | { 36 | Name = description.name, 37 | SpaceId = spaceId.ToString(), 38 | Conditions = new [] { 39 | new Models.ConditionCreate() 40 | { 41 | Target = "Sensor", 42 | Path = "$.dataType", 43 | Value = $"\"{description.dataTypeValue}\"", 44 | Comparison = "Equals", 45 | } 46 | } 47 | }; 48 | 49 | public static Models.KeyStoreCreate ToKeyStoreCreate(this KeyStoreDescription description, Guid spaceId) 50 | => new Models.KeyStoreCreate() 51 | { 52 | Name = description.name, 53 | SpaceId = spaceId.ToString(), 54 | }; 55 | 56 | public static Models.RoleAssignmentCreate ToRoleAssignmentCreate(this RoleAssignmentDescription description, string objectId = null, string path = null) 57 | => new Models.RoleAssignmentCreate() 58 | { 59 | ObjectId = objectId ?? description.objectId, 60 | ObjectIdType = description.objectIdType, 61 | Path = path ?? description.path, 62 | RoleId = description.roleId, 63 | TenantId = description.tenantId, 64 | }; 65 | 66 | public static Models.ResourceCreate ToResourceCreate(this ResourceDescription description, Guid spaceId) 67 | => new Models.ResourceCreate() 68 | { 69 | SpaceId = spaceId.ToString(), 70 | Type = description.type, 71 | }; 72 | 73 | public static Models.SensorCreate ToSensorCreate(this SensorDescription description, Guid deviceId) 74 | => new Models.SensorCreate() 75 | { 76 | DataType = description.dataType, 77 | DeviceId = deviceId.ToString(), 78 | HardwareId = description.hardwareId, 79 | }; 80 | 81 | public static Models.SpaceCreate ToSpaceCreate(this SpaceDescription description, Guid parentId) 82 | => new Models.SpaceCreate() 83 | { 84 | Name = description.name, 85 | ParentSpaceId = parentId != Guid.Empty ? parentId.ToString() : "", 86 | }; 87 | 88 | public static Models.UserDefinedFunction ToUserDefinedFunction(this UserDefinedFunctionDescription description, string Id, Guid spaceId, IEnumerable matchers) 89 | => new Models.UserDefinedFunction() 90 | { 91 | Id = Id, 92 | Name = description.name, 93 | SpaceId = spaceId.ToString(), 94 | Matchers = matchers, 95 | }; 96 | 97 | public static Models.UserDefinedFunctionCreate ToUserDefinedFunctionCreate(this UserDefinedFunctionDescription description, Guid spaceId, IEnumerable matcherIds) 98 | => new Models.UserDefinedFunctionCreate() 99 | { 100 | Name = description.name, 101 | SpaceId = spaceId.ToString(), 102 | Matchers = matcherIds, 103 | }; 104 | 105 | public static Models.UserDefinedFunctionUpdate ToUserDefinedFunctionUpdate(this UserDefinedFunctionDescription description, string id, Guid spaceId, IEnumerable matcherIds) 106 | => new Models.UserDefinedFunctionUpdate() 107 | { 108 | Id = id, 109 | Name = description.name, 110 | SpaceId = spaceId.ToString(), 111 | Matchers = matcherIds, 112 | }; 113 | } 114 | } -------------------------------------------------------------------------------- /occupancy-quickstart/src/actions/getAvailableAndFreshSpaces.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Net.Http; 8 | using System.Threading.Tasks; 9 | using Microsoft.Extensions.Logging; 10 | using Newtonsoft.Json; 11 | 12 | namespace Microsoft.Azure.DigitalTwins.Samples 13 | { 14 | public static partial class Actions 15 | { 16 | // Prints out and returns spaces with the occupany property key set 17 | public static async Task GetAvailableAndFreshSpaces(HttpClient httpClient) 18 | { 19 | Console.WriteLine("Polling spaces with 'AvailableAndFresh' value type"); 20 | 21 | var maxGets = 30; 22 | for (var curGets = 0; curGets < maxGets; ++curGets) 23 | { 24 | var (spaces, response) = await GetManagementItemsAsync(httpClient, "spaces", "includes=values"); 25 | if (spaces == null) 26 | { 27 | var content = await response.Content?.ReadAsStringAsync(); 28 | Console.WriteLine($"ERROR: GET spaces?includes=values failed with: {(int)response.StatusCode}, {response.StatusCode} {content}"); 29 | break; 30 | } 31 | 32 | var availableAndFreshSpaces = spaces.Where(s => s.Values != null && s.Values.Any(v => v.Type == "AvailableAndFresh")); 33 | if (availableAndFreshSpaces.Any()) 34 | { 35 | var availableAndFreshDisplay = availableAndFreshSpaces 36 | .Select(s => GetDisplayValues(s)) 37 | .Aggregate((acc, cur) => acc + "\n" + cur); 38 | Console.WriteLine($"{availableAndFreshDisplay}"); 39 | } 40 | else 41 | { 42 | Console.WriteLine("Unable to find a space with value type 'AvailableAndFresh'"); 43 | } 44 | 45 | await Task.Delay(TimeSpan.FromSeconds(4)); 46 | } 47 | } 48 | 49 | private static async Task<(IEnumerable, HttpResponseMessage)> GetManagementItemsAsync( 50 | HttpClient httpClient, 51 | string queryItem, 52 | string queryParams) 53 | { 54 | var response = await httpClient.GetAsync($"{queryItem}?{queryParams}"); 55 | if (response.IsSuccessStatusCode) 56 | { 57 | var content = await response.Content.ReadAsStringAsync(); 58 | var objects = JsonConvert.DeserializeObject>(content); 59 | return (objects, response); 60 | } 61 | 62 | return (null, response); 63 | } 64 | 65 | private static string GetDisplayValues(Models.Space space) 66 | { 67 | var spaceValue = space.Values.First(v => v.Type == "AvailableAndFresh"); 68 | return $"Name: {space.Name}\nId: {space.Id}\nTimestamp: {spaceValue.Timestamp}\nValue: {spaceValue.Value}\n"; 69 | } 70 | } 71 | } -------------------------------------------------------------------------------- /occupancy-quickstart/src/actions/getCreationSummary.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace Microsoft.Azure.DigitalTwins.Samples 6 | { 7 | public static partial class Actions 8 | { 9 | private static string GetCreationSummary(string itemTypeSingular, string itemTypePlural, List createdIds) 10 | => createdIds.Count == 0 11 | ? $"Created 0 {itemTypePlural}." 12 | : createdIds.Count == 1 13 | ? $"Created 1 {itemTypeSingular}: {AggregateIdsIntoString(createdIds)}" 14 | : $"Created {createdIds.Count} {itemTypePlural}: {AggregateIdsIntoString(createdIds)}"; 15 | 16 | private static string AggregateIdsIntoString(IEnumerable ids) 17 | => ids 18 | .Select(id => id.ToString()) 19 | .Aggregate((acc, cur) => acc + ", " + cur); 20 | } 21 | } -------------------------------------------------------------------------------- /occupancy-quickstart/src/actions/getSpaces.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Net.Http; 8 | using System.Threading.Tasks; 9 | using Microsoft.Extensions.Logging; 10 | using Newtonsoft.Json; 11 | 12 | namespace Microsoft.Azure.DigitalTwins.Samples 13 | { 14 | public static partial class Actions 15 | { 16 | public static async Task GetSpaces(HttpClient httpClient, ILogger logger) 17 | { 18 | var spaces = await Api.GetSpaces( 19 | httpClient, logger, 20 | maxNumberToGet: 100, includes: "types,values,properties"); 21 | 22 | Console.WriteLine($"GetSpaces: {JsonConvert.SerializeObject(spaces, Formatting.Indented)}"); 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /occupancy-quickstart/src/actions/provisionResults.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | 9 | namespace Microsoft.Azure.DigitalTwins.Samples.ProvisionResults 10 | { 11 | public struct Space 12 | { 13 | public Guid Id; 14 | public IEnumerable Devices; 15 | public IEnumerable Sensors; 16 | public IEnumerable Spaces; 17 | } 18 | 19 | public struct Device 20 | { 21 | public string ConnectionString; 22 | public string HardwareId; 23 | } 24 | 25 | public struct Sensor 26 | { 27 | public string DataType; 28 | public string HardwareId; 29 | } 30 | } -------------------------------------------------------------------------------- /occupancy-quickstart/src/actions/provisionSample.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.IO; 7 | using System.Linq; 8 | using System.Net.Http; 9 | using System.Text; 10 | using System.Threading.Tasks; 11 | using Microsoft.Extensions.Logging; 12 | using Newtonsoft.Json; 13 | using YamlDotNet.Serialization; 14 | 15 | namespace Microsoft.Azure.DigitalTwins.Samples 16 | { 17 | public static partial class Actions 18 | { 19 | public static async Task> ProvisionSample(HttpClient httpClient, ILogger logger) 20 | { 21 | IEnumerable spaceCreateDescriptions; 22 | using (var r = new StreamReader("actions/provisionSample.yaml")) 23 | { 24 | spaceCreateDescriptions = await GetProvisionSampleTopology(r); 25 | } 26 | 27 | var results = await CreateSpaces(httpClient, logger, spaceCreateDescriptions, Guid.Empty); 28 | 29 | Console.WriteLine($"Completed Provisioning: {JsonConvert.SerializeObject(results, Formatting.Indented, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore } )}"); 30 | 31 | return results; 32 | } 33 | 34 | public static async Task> GetProvisionSampleTopology(TextReader textReader) 35 | => new Deserializer().Deserialize>(await textReader.ReadToEndAsync()); 36 | 37 | public static async Task> CreateSpaces( 38 | HttpClient httpClient, 39 | ILogger logger, 40 | IEnumerable descriptions, 41 | Guid parentId) 42 | { 43 | var spaceResults = new List(); 44 | foreach (var description in descriptions) 45 | { 46 | var spaceId = await GetExistingSpaceOrCreate(httpClient, logger, parentId, description); 47 | 48 | if (spaceId != Guid.Empty) 49 | { 50 | // This must happen before devices (or anyhting that could have devices like other spaces) 51 | // or the device create will fail because a resource is required on an ancestor space 52 | if (description.resources != null) 53 | await CreateResources(httpClient, logger, description.resources, spaceId); 54 | 55 | var devices = description.devices != null 56 | ? await CreateDevices(httpClient, logger, description.devices, spaceId) 57 | : Array.Empty(); 58 | 59 | if (description.matchers != null) 60 | await CreateMatchers(httpClient, logger, description.matchers, spaceId); 61 | 62 | if (description.userdefinedfunctions != null) 63 | await CreateUserDefinedFunctions(httpClient, logger, description.userdefinedfunctions, spaceId); 64 | 65 | if (description.roleassignments != null) 66 | await CreateRoleAssignments(httpClient, logger, description.roleassignments, spaceId); 67 | 68 | var childSpacesResults = description.spaces != null 69 | ? await CreateSpaces(httpClient, logger, description.spaces, spaceId) 70 | : Array.Empty(); 71 | 72 | var sensors = await Api.GetSensorsOfSpace(httpClient, logger, spaceId); 73 | 74 | spaceResults.Add(new ProvisionResults.Space() 75 | { 76 | Id = spaceId, 77 | Devices = devices.Select(device => new ProvisionResults.Device() 78 | { 79 | ConnectionString = device.ConnectionString, 80 | HardwareId = device.HardwareId, 81 | }), 82 | Sensors = sensors.Select(sensor => new ProvisionResults.Sensor() 83 | { 84 | DataType = sensor.DataType, 85 | HardwareId = sensor.HardwareId, 86 | }), 87 | Spaces = childSpacesResults, 88 | }); 89 | } 90 | } 91 | 92 | return spaceResults; 93 | } 94 | 95 | private static async Task> CreateDevices( 96 | HttpClient httpClient, 97 | ILogger logger, 98 | IEnumerable descriptions, 99 | Guid spaceId) 100 | { 101 | if (spaceId == Guid.Empty) 102 | throw new ArgumentException("Devices must have a spaceId"); 103 | 104 | var devices = new List(); 105 | 106 | foreach (var description in descriptions) 107 | { 108 | var device = await GetExistingDeviceOrCreate(httpClient, logger, spaceId, description); 109 | 110 | if (device != null) 111 | { 112 | devices.Add(device); 113 | 114 | if (description.sensors != null) 115 | await CreateSensors(httpClient, logger, description.sensors, Guid.Parse(device.Id)); 116 | } 117 | } 118 | 119 | return devices; 120 | } 121 | 122 | private static async Task CreateMatchers( 123 | HttpClient httpClient, 124 | ILogger logger, 125 | IEnumerable descriptions, 126 | Guid spaceId) 127 | { 128 | if (spaceId == Guid.Empty) 129 | throw new ArgumentException("Matchers must have a spaceId"); 130 | 131 | foreach (var description in descriptions) 132 | { 133 | await Api.CreateMatcher(httpClient, logger, description.ToMatcherCreate(spaceId)); 134 | } 135 | } 136 | 137 | private static async Task CreateResources( 138 | HttpClient httpClient, 139 | ILogger logger, 140 | IEnumerable descriptions, 141 | Guid spaceId) 142 | { 143 | if (spaceId == Guid.Empty) 144 | throw new ArgumentException("Resources must have a spaceId"); 145 | 146 | foreach (var description in descriptions) 147 | { 148 | var createdId = await Api.CreateResource(httpClient, logger, description.ToResourceCreate(spaceId)); 149 | if (createdId != Guid.Empty) 150 | { 151 | // After creation resources might take time to be ready to use so we need 152 | // to poll until it is done since downstream operations (like device creation) 153 | // may depend on it 154 | logger.LogInformation("Polling until resource is no longer in 'Provisioning' state..."); 155 | while (await Api.IsResourceProvisioning(httpClient, logger, createdId)) 156 | { 157 | await Task.Delay(5000); 158 | } 159 | } 160 | } 161 | } 162 | 163 | private static async Task CreateRoleAssignments( 164 | HttpClient httpClient, 165 | ILogger logger, 166 | IEnumerable descriptions, 167 | Guid spaceId) 168 | { 169 | if (spaceId == Guid.Empty) 170 | throw new ArgumentException("RoleAssignments must have a spaceId"); 171 | 172 | var space = await Api.GetSpace(httpClient, logger, spaceId, includes: "fullpath"); 173 | 174 | // A SpacePath is the list of spaces formatted like so: "space1/space2" - where space2 has space1 as a parent 175 | // When getting SpacePaths of a space itself there is always exactly one path - the path from the root to itself 176 | // This is not true when getting space paths of other topology items (ie non spaces) 177 | var path = space.SpacePaths.Single(); 178 | 179 | foreach (var description in descriptions) 180 | { 181 | string objectId; 182 | switch (description.objectIdType) 183 | { 184 | case "UserDefinedFunctionId": 185 | objectId = (await Api.FindUserDefinedFunction(httpClient, logger, description.objectName, spaceId))?.Id; 186 | break; 187 | default: 188 | objectId = null; 189 | logger.LogError($"roleAssignment with objectName must have known objectIdType but instead has '{description.objectIdType}'"); 190 | break; 191 | } 192 | 193 | if (objectId != null) 194 | { 195 | await Api.CreateRoleAssignment(httpClient, logger, description.ToRoleAssignmentCreate(objectId, path)); 196 | } 197 | } 198 | } 199 | 200 | private static async Task CreateSensors(HttpClient httpClient, ILogger logger, IEnumerable descriptions, Guid deviceId) 201 | { 202 | if (deviceId == Guid.Empty) 203 | throw new ArgumentException("Sensors must have a deviceId"); 204 | 205 | foreach (var description in descriptions) 206 | { 207 | await Api.CreateSensor(httpClient, logger, description.ToSensorCreate(deviceId)); 208 | } 209 | } 210 | 211 | private static async Task CreateUserDefinedFunctions( 212 | HttpClient httpClient, 213 | ILogger logger, 214 | IEnumerable descriptions, 215 | Guid spaceId) 216 | { 217 | if (spaceId == Guid.Empty) 218 | throw new ArgumentException("UserDefinedFunctions must have a spaceId"); 219 | 220 | foreach (var description in descriptions) 221 | { 222 | var matchers = await Api.FindMatchers(httpClient, logger, description.matcherNames, spaceId); 223 | 224 | using (var r = new StreamReader(description.script)) 225 | { 226 | var js = await r.ReadToEndAsync(); 227 | if (String.IsNullOrWhiteSpace(js)) 228 | { 229 | logger.LogError($"Error creating user defined function: Couldn't read from {description.script}"); 230 | } 231 | else 232 | { 233 | await CreateOrPatchUserDefinedFunction( 234 | httpClient, 235 | logger, 236 | description, 237 | js, 238 | spaceId, 239 | matchers); 240 | } 241 | } 242 | } 243 | } 244 | 245 | private static async Task GetExistingDeviceOrCreate(HttpClient httpClient, ILogger logger, Guid spaceId, DeviceDescription description) 246 | { 247 | // NOTE: The API doesn't support getting connection strings on bulk get devices calls so we 248 | // even in the case where we are reusing a preexisting device we need to make the GetDevice 249 | // call below to get the connection string 250 | var existingDeviceId = (await Api.FindDevice(httpClient, logger, description.hardwareId, spaceId))?.Id; 251 | var deviceId = existingDeviceId != null 252 | ? Guid.Parse(existingDeviceId) 253 | : await Api.CreateDevice(httpClient, logger, description.ToDeviceCreate(spaceId)); 254 | return await Api.GetDevice(httpClient, logger, deviceId, includes: "ConnectionString"); 255 | } 256 | 257 | private static async Task GetExistingSpaceOrCreate(HttpClient httpClient, ILogger logger, Guid parentId, SpaceDescription description) 258 | { 259 | var existingSpace = await Api.FindSpace(httpClient, logger, description.name, parentId); 260 | return existingSpace?.Id != null 261 | ? Guid.Parse(existingSpace.Id) 262 | : await Api.CreateSpace(httpClient, logger, description.ToSpaceCreate(parentId)); 263 | } 264 | 265 | private static async Task CreateOrPatchUserDefinedFunction( 266 | HttpClient httpClient, 267 | ILogger logger, 268 | UserDefinedFunctionDescription description, 269 | string js, 270 | Guid spaceId, 271 | IEnumerable matchers) 272 | { 273 | var userDefinedFunction = await Api.FindUserDefinedFunction(httpClient, logger, description.name, spaceId); 274 | 275 | if (userDefinedFunction != null) 276 | { 277 | await Api.UpdateUserDefinedFunction( 278 | httpClient, 279 | logger, 280 | description.ToUserDefinedFunctionUpdate(userDefinedFunction.Id, spaceId, matchers.Select(m => m.Id)), 281 | js); 282 | } 283 | else 284 | { 285 | await Api.CreateUserDefinedFunction( 286 | httpClient, 287 | logger, 288 | description.ToUserDefinedFunctionCreate(spaceId, matchers.Select(m => m.Id)), 289 | js); 290 | } 291 | } 292 | } 293 | } -------------------------------------------------------------------------------- /occupancy-quickstart/src/actions/provisionSample.yaml: -------------------------------------------------------------------------------- 1 | - name: Quickstart Building 2 | type: Venue 3 | resources: 4 | - type: IoTHub 5 | spaces: 6 | - name: Floor 1 7 | type: Floor 8 | spaces: 9 | - name: Area A 10 | type: Area 11 | - name: Conference Room 11 12 | type: Room 13 | subType: ConferenceRoom 14 | - name: Focus Room A1 15 | type: Room 16 | subType: FocusRoom 17 | devices: 18 | - name: Raspberry Pi 3 A1 19 | hardwareId: 1234567890AB 20 | sensors: 21 | - dataType: Motion 22 | hardwareId: SAMPLE_SENSOR_MOTION 23 | - dataType: CarbonDioxide 24 | hardwareId: SAMPLE_SENSOR_CARBONDIOXIDE 25 | # - dataType: Temperature 26 | # hardwareId: SAMPLE_SENSOR_TEMPERATURE 27 | matchers: 28 | - name: Matcher Motion A1 29 | dataTypeValue: Motion 30 | - name: Matcher CarbonDioxide A1 31 | dataTypeValue: CarbonDioxide 32 | # - name: Matcher Temperature 33 | # dataTypeValue: Temperature 34 | userdefinedfunctions: 35 | - name: Motion Processor 36 | matcherNames: 37 | - Matcher Motion A1 38 | - Matcher CarbonDioxide A1 39 | # - Matcher Temperature 40 | script: actions/userDefinedFunctions/availability.js 41 | roleassignments: 42 | - roleId: 98e44ad7-28d4-4007-853b-b9968ad132d1 # System Role: SpaceAdministrator 43 | objectName: Motion Processor 44 | objectIdType: UserDefinedFunctionId 45 | -------------------------------------------------------------------------------- /occupancy-quickstart/src/actions/userDefinedFunctions/availability.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | var carbonDioxideType = "CarbonDioxide"; 5 | var motionType = "Motion"; 6 | var spaceAvailFresh = "AvailableAndFresh"; 7 | var carbonDioxideThreshold = 1000.0; 8 | // Add your sensor type here 9 | 10 | function process(telemetry, executionContext) { 11 | 12 | try { 13 | // Log SensorId and Message 14 | log(`Sensor ID: ${telemetry.SensorId}. `); 15 | log(`Sensor value: ${JSON.stringify(telemetry.Message)}.`); 16 | 17 | // Get sensor metadata 18 | var sensor = getSensorMetadata(telemetry.SensorId); 19 | 20 | // Retrieve the sensor reading 21 | var parseReading = JSON.parse(telemetry.Message); 22 | 23 | // Set the sensor reading as the current value for the sensor. 24 | setSensorValue(telemetry.SensorId, sensor.DataType, parseReading.SensorValue); 25 | 26 | // Get parent space 27 | var parentSpace = sensor.Space(); 28 | 29 | // Get children sensors from the same space 30 | var otherSensors = parentSpace.ChildSensors(); 31 | 32 | // Retrieve carbonDioxide, and motion sensors 33 | var carbonDioxideSensor = otherSensors.find(function(element) { 34 | return element.DataType === carbonDioxideType; 35 | }); 36 | 37 | var motionSensor = otherSensors.find(function(element) { 38 | return element.DataType === motionType; 39 | }); 40 | 41 | // Add your sensor variable here 42 | 43 | // get latest values for above sensors 44 | var motionValue = motionSensor.Value().Value; 45 | var presence = !!motionValue && motionValue.toLowerCase() === "true"; 46 | var carbonDioxideValue = getFloatValue(carbonDioxideSensor.Value().Value); 47 | 48 | // Add your sensor latest value here 49 | 50 | // Return if no motion or carbonDioxide found return 51 | // Modify this line to monitor your sensor value 52 | if(carbonDioxideValue === null || motionValue === null) { 53 | sendNotification(telemetry.SensorId, "Sensor", "Error: Carbon dioxide or motion are null, returning"); 54 | return; 55 | } 56 | 57 | // Modify these lines as per your sensor 58 | var availableFresh = "Room is available and air is fresh"; 59 | var noAvailableOrFresh = "Room is not available or air quality is poor"; 60 | 61 | // Modify this code block for your sensor 62 | // If carbonDioxide less than threshold and no presence in the room => log, notify and set parent space computed value 63 | if(carbonDioxideValue < carbonDioxideThreshold && !presence) { 64 | log(`${availableFresh}. Carbon Dioxide: ${carbonDioxideValue}. Presence: ${presence}.`); 65 | setSpaceValue(parentSpace.Id, spaceAvailFresh, availableFresh); 66 | } 67 | else { 68 | log(`${noAvailableOrFresh}. Carbon Dioxide: ${carbonDioxideValue}. Presence: ${presence}.`); 69 | setSpaceValue(parentSpace.Id, spaceAvailFresh, noAvailableOrFresh); 70 | 71 | // Set up custom notification for poor air quality 72 | parentSpace.Notify(JSON.stringify(noAvailableOrFresh)); 73 | } 74 | } 75 | catch (error) 76 | { 77 | log(`An error has occurred processing the UDF Error: ${error.name} Message ${error.message}.`); 78 | } 79 | } 80 | 81 | function getFloatValue(str) { 82 | if(!str) { 83 | return null; 84 | } 85 | 86 | return parseFloat(str); 87 | } 88 | -------------------------------------------------------------------------------- /occupancy-quickstart/src/actions/userDefinedFunctions/availabilityForTutorial.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | // Sample code for the "Monitor a building with Digital Twins" tutorials: 5 | 6 | var carbonDioxideType = "CarbonDioxide"; 7 | var motionType = "Motion"; 8 | var spaceAvailFresh = "AvailableAndFresh"; 9 | var carbonDioxideThreshold = 1000.0; 10 | var temperatureType = "Temperature"; 11 | var temperatureThreshold = 78; 12 | 13 | function process(telemetry, executionContext) { 14 | try { 15 | // Log SensorId and Message 16 | log(`Sensor ID: ${telemetry.SensorId}. `); 17 | log(`Sensor value: ${JSON.stringify(telemetry.Message)}.`); 18 | 19 | // Get sensor metadata 20 | var sensor = getSensorMetadata(telemetry.SensorId); 21 | 22 | // Retrieve the sensor reading 23 | var parseReading = JSON.parse(telemetry.Message); 24 | 25 | // Set the sensor reading as the current value for the sensor. 26 | setSensorValue(telemetry.SensorId, sensor.DataType, parseReading.SensorValue); 27 | 28 | // Get parent space 29 | var parentSpace = sensor.Space(); 30 | 31 | // Get children sensors from the same space 32 | var otherSensors = parentSpace.ChildSensors(); 33 | 34 | // Retrieve carbonDioxide, temperature, and motion sensors 35 | var carbonDioxideSensor = otherSensors.find(function(element) { 36 | return element.DataType === carbonDioxideType; 37 | }); 38 | var motionSensor = otherSensors.find(function(element) { 39 | return element.DataType === motionType; 40 | }); 41 | var temperatureSensor = otherSensors.find(function(element) { 42 | return element.DataType === temperatureType; 43 | }); 44 | 45 | // get latest values for above sensors 46 | var motionValue = motionSensor.Value().Value; 47 | var presence = !!motionValue && motionValue.toLowerCase() === "true"; 48 | var carbonDioxideValue = getFloatValue(carbonDioxideSensor.Value().Value); 49 | var temperatureValue = getFloatValue(temperatureSensor.Value().Value); 50 | 51 | // Return if no motion, temperature, or carbonDioxide found 52 | if(carbonDioxideValue === null || motionValue === null || temperatureValue === null){ 53 | sendNotification(telemetry.SensorId, "Sensor", "Error: Carbon dioxide, motion, or temperature are null, returning"); 54 | return; 55 | } 56 | 57 | var alert = "Room with fresh air and comfortable temperature is available."; 58 | var noAlert = "Either room is occupied, or working conditions are not right."; 59 | 60 | // If sensor values are within range and room is available 61 | if(carbonDioxideValue < carbonDioxideThreshold && temperatureValue < temperatureThreshold && !presence) { 62 | log(`${alert}. Carbon Dioxide: ${carbonDioxideValue}. Temperature: ${temperatureValue}. Presence: ${presence}.`); 63 | 64 | // log, notify and set parent space computed value 65 | setSpaceValue(parentSpace.Id, spaceAvailFresh, alert); 66 | 67 | // Set up notification for this alert 68 | parentSpace.Notify(JSON.stringify(alert)); 69 | } 70 | else { 71 | log(`${noAlert}. Carbon Dioxide: ${carbonDioxideValue}. Temperature: ${temperatureValue}. Presence: ${presence}.`); 72 | 73 | // log, notify and set parent space computed value 74 | setSpaceValue(parentSpace.Id, spaceAvailFresh, noAlert); 75 | } 76 | } 77 | catch (error) 78 | { 79 | log(`An error has occurred processing the UDF Error: ${error.name} Message ${error.message}.`); 80 | } 81 | } 82 | function getFloatValue(str) { 83 | if(!str) { 84 | return null; 85 | } 86 | return parseFloat(str); 87 | } 88 | -------------------------------------------------------------------------------- /occupancy-quickstart/src/actions/userDefinedFunctions/multiplemotionsensors.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | // Sample code for detecting motion in an area with multiple motion sensors: 5 | // This code is designed to work with the existing sample code. 6 | // To test this end to end, make the following changes: 7 | // 1) In the provisionSample.yaml, change the "script" key to point to this file (multiplemotionsensors.js). 8 | // 2) In the provisionSample.yaml, add a couple more motion sensors. For example, the sample "sensors" node could look like this: 9 | // sensors: 10 | // - dataType: Motion 11 | // hardwareId: SAMPLE_SENSOR_MOTION_ONE 12 | // - dataType: CarbonDioxide 13 | // hardwareId: SAMPLE_SENSOR_CARBONDIOXIDE 14 | // - dataType: Motion 15 | // hardwareId: SAMPLE_SENSOR_MOTION_TWO 16 | // - dataType: Motion 17 | // hardwareId: SAMPLE_SENSOR_MOTION_THREE 18 | // 3) Add these additional motion sensors to the device-connectivity project's appSettings.json. For example: 19 | // "Sensors": [{ 20 | // "DataType": "Motion", 21 | // "HardwareId": "SAMPLE_SENSOR_MOTION_ONE" 22 | // },{ 23 | // "DataType": "CarbonDioxide", 24 | // "HardwareId": "SAMPLE_SENSOR_CARBONDIOXIDE" 25 | // },{ 26 | // "DataType": "Motion", 27 | // "HardwareId": "SAMPLE_SENSOR_MOTION_TWO" 28 | // },{ 29 | // "DataType": "Motion", 30 | // "HardwareId": "SAMPLE_SENSOR_MOTION_THREE" 31 | // }] 32 | // 33 | // 34 | 35 | 36 | var motionType = "Motion"; 37 | var occupancyStatus = "AvailableAndFresh"; 38 | 39 | function process(telemetry, executionContext) { 40 | try { 41 | // Log SensorId and Message 42 | log(`Sensor ID: ${telemetry.SensorId}. `); 43 | log(`Sensor value: ${JSON.stringify(telemetry.Message)}.`); 44 | 45 | // Get sensor metadata 46 | var sensor = getSensorMetadata(telemetry.SensorId); 47 | 48 | // Retrieve the sensor reading 49 | var parseReading = JSON.parse(telemetry.Message); 50 | 51 | // Set the sensor reading as the current value for the sensor. 52 | setSensorValue(telemetry.SensorId, sensor.DataType, parseReading.SensorValue); 53 | 54 | // Get parent space 55 | var parentSpace = sensor.Space(); 56 | 57 | // Get all children sensors of the parent space 58 | var allSensors = parentSpace.ChildSensors(); 59 | 60 | // Get all motion sensors of the parent space 61 | var motionSensors = allSensors.filter(function(item){ 62 | return item.DataType === motionType; 63 | }); 64 | 65 | // Is any of the motion sensors detecting motion? 66 | var motionDetected = motionSensors.find(function(element) { 67 | return element.Value().Value.toLowerCase() === "true"; 68 | }); 69 | 70 | // Set OccupancyStatus for the space 71 | var occupied = "Room is occupied"; 72 | var empty = "Room is empty!"; 73 | 74 | if (motionDetected != undefined) 75 | { 76 | // Motion is detected by at least one sensor 77 | log(`${occupied}. Motion Detected: ${motionDetected}. Sensors: ${motionSensors}.`); 78 | setSpaceValue(parentSpace.Id, occupancyStatus, occupied); 79 | } 80 | else { 81 | // No motion detected by any sensor 82 | log(`${empty}. Motion Not Detected: ${motionDetected}. Sensors: ${motionSensors}.`); 83 | setSpaceValue(parentSpace.Id, occupancyStatus, empty); 84 | // You could try creating a Logic App mail notification in case none of the motion sensors 85 | // detect anything. In that case, uncomment the following line: 86 | // parentSpace.Notify(JSON.stringify(empty)); 87 | } 88 | } 89 | catch (error) 90 | { 91 | var errormsg = `An error has occurred processing the UDF Error: ${error.name} Message ${error.message}.`; 92 | log(errormsg); 93 | setSpaceValue(parentSpace.Id, occupancyStatus, errormsg); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /occupancy-quickstart/src/api/README.md: -------------------------------------------------------------------------------- 1 | # API 2 | 3 | This folder contains helpers that wrap Digital Twins REST API to produce and consume in memory [models](../models/README.md). Additional helpers are supplied to further simplify API interactions. 4 | 5 | Please note that the supplied API helpers demonstrate only a few of the full range of API capabilities. 6 | 7 | ## Swagger 8 | 9 | See the Swagger documentation that is part of your Azure Digital Twins instance for a complete description of API capabilities: 10 | 11 | A Swagger sneak preview is provided to demonstrate the API feature set. It's hosted at [docs.westcentralus.azuresmartspaces.net/management/swagger](https://docs.westcentralus.azuresmartspaces.net/management/swagger). 12 | 13 | You can access your own, generated, Management API Swagger documentation at: 14 | 15 | ```plaintext 16 | https://yourInstanceName.yourLocation.azuresmartspaces.net/management/swagger 17 | ``` 18 | 19 | * Replace `yourInstanceName` with the name of your Azure Digital Twins instance. 20 | * Replace `yourLocation` with which server region your instance is hosted on. 21 | 22 | ## AutoRest 23 | 24 | Also see [AutoRest](https://github.com/Azure/autorest) for how you can generate full C# models and APIs from that documentation. 25 | -------------------------------------------------------------------------------- /occupancy-quickstart/src/api/create.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System; 5 | using System.Net.Http; 6 | using System.Net.Http.Headers; 7 | using System.Text; 8 | using System.Threading.Tasks; 9 | using Microsoft.Extensions.Logging; 10 | using Newtonsoft.Json; 11 | 12 | namespace Microsoft.Azure.DigitalTwins.Samples 13 | { 14 | public partial class Api 15 | { 16 | public static async Task CreateDevice(HttpClient httpClient, ILogger logger, Models.DeviceCreate deviceCreate) 17 | { 18 | logger.LogInformation($"Creating Device: {JsonConvert.SerializeObject(deviceCreate, Formatting.Indented)}"); 19 | var content = JsonConvert.SerializeObject(deviceCreate); 20 | var response = await httpClient.PostAsync("devices", new StringContent(content, Encoding.UTF8, "application/json")); 21 | return await GetIdFromResponse(response, logger); 22 | } 23 | 24 | public static async Task CreateEndpoints(HttpClient httpClient, ILogger logger, Models.EndpointsCreate endpointCreate) 25 | { 26 | logger.LogInformation($"Creating Endpoint: {JsonConvert.SerializeObject(endpointCreate, Formatting.Indented)}"); 27 | var content = JsonConvert.SerializeObject(endpointCreate); 28 | var response = await httpClient.PostAsync("endpoints", new StringContent(content, Encoding.UTF8, "application/json")); 29 | return await GetIdFromResponse(response, logger); 30 | } 31 | 32 | public static async Task CreateKeyStore(HttpClient httpClient, ILogger logger, Models.KeyStoreCreate keyStoreCreate) 33 | { 34 | logger.LogInformation($"Creating KeyStore: {JsonConvert.SerializeObject(keyStoreCreate, Formatting.Indented)}"); 35 | var content = JsonConvert.SerializeObject(keyStoreCreate); 36 | var response = await httpClient.PostAsync("keystores", new StringContent(content, Encoding.UTF8, "application/json")); 37 | return await GetIdFromResponse(response, logger); 38 | } 39 | 40 | public static async Task CreateMatcher(HttpClient httpClient, ILogger logger, Models.MatcherCreate matcherCreate) 41 | { 42 | logger.LogInformation($"Creating Matcher: {JsonConvert.SerializeObject(matcherCreate, Formatting.Indented)}"); 43 | var content = JsonConvert.SerializeObject(matcherCreate); 44 | var response = await httpClient.PostAsync("matchers", new StringContent(content, Encoding.UTF8, "application/json")); 45 | return await GetIdFromResponse(response, logger); 46 | } 47 | 48 | public static async Task CreateResource(HttpClient httpClient, ILogger logger, Models.ResourceCreate resourceCreate) 49 | { 50 | logger.LogInformation($"Creating Resource: {JsonConvert.SerializeObject(resourceCreate, Formatting.Indented)}"); 51 | var content = JsonConvert.SerializeObject(resourceCreate); 52 | var response = await httpClient.PostAsync("resources", new StringContent(content, Encoding.UTF8, "application/json")); 53 | return await GetIdFromResponse(response, logger); 54 | } 55 | 56 | public static async Task CreateRoleAssignment(HttpClient httpClient, ILogger logger, Models.RoleAssignmentCreate roleAssignmentCreate) 57 | { 58 | logger.LogInformation($"Creating RoleAssignment: {JsonConvert.SerializeObject(roleAssignmentCreate, Formatting.Indented)}"); 59 | var content = JsonConvert.SerializeObject(roleAssignmentCreate); 60 | var response = await httpClient.PostAsync("roleassignments", new StringContent(content, Encoding.UTF8, "application/json")); 61 | return await GetIdFromResponse(response, logger); 62 | } 63 | 64 | public static async Task CreateSensor(HttpClient httpClient, ILogger logger, Models.SensorCreate sensorCreate) 65 | { 66 | logger.LogInformation($"Creating Sensor: {JsonConvert.SerializeObject(sensorCreate, Formatting.Indented)}"); 67 | var content = JsonConvert.SerializeObject(sensorCreate); 68 | var response = await httpClient.PostAsync("sensors", new StringContent(content, Encoding.UTF8, "application/json")); 69 | return await GetIdFromResponse(response, logger); 70 | } 71 | 72 | public static async Task CreateSpace(HttpClient httpClient, ILogger logger, Models.SpaceCreate spaceCreate) 73 | { 74 | logger.LogInformation($"Creating Space: {JsonConvert.SerializeObject(spaceCreate, Formatting.Indented)}"); 75 | var content = JsonConvert.SerializeObject(spaceCreate); 76 | var response = await httpClient.PostAsync("spaces", new StringContent(content, Encoding.UTF8, "application/json")); 77 | return await GetIdFromResponse(response, logger); 78 | } 79 | 80 | public static async Task CreateProperty(HttpClient httpClient, ILogger logger, Guid spaceId, Models.PropertyCreate propertyCreate) 81 | { 82 | logger.LogInformation($"Creating Property: {JsonConvert.SerializeObject(propertyCreate, Formatting.Indented)}"); 83 | var content = JsonConvert.SerializeObject(propertyCreate); 84 | var response = await httpClient.PostAsync($"spaces/{spaceId.ToString()}/properties", new StringContent(content, Encoding.UTF8, "application/json")); 85 | logger.LogInformation($"Creating Property Response: {response}"); 86 | } 87 | 88 | public static async Task CreatePropertyKey(HttpClient httpClient, ILogger logger, Models.PropertyKeyCreate propertyKeyCreate) 89 | { 90 | logger.LogInformation($"Creating PropertyKey: {JsonConvert.SerializeObject(propertyKeyCreate, Formatting.Indented)}"); 91 | var content = JsonConvert.SerializeObject(propertyKeyCreate); 92 | var response = await httpClient.PostAsync($"propertykeys", new StringContent(content, Encoding.UTF8, "application/json")); 93 | logger.LogInformation($"Creating PropertyKey Response: {response}"); 94 | } 95 | 96 | public static async Task CreateUserDefinedFunction( 97 | HttpClient httpClient, 98 | ILogger logger, 99 | Models.UserDefinedFunctionCreate userDefinedFunctionCreate, 100 | string js) 101 | { 102 | logger.LogInformation($"Creating UserDefinedFunction with Metadata: {JsonConvert.SerializeObject(userDefinedFunctionCreate, Formatting.Indented)}"); 103 | var displayContent = js.Length > 100 ? js.Substring(0, 100) + "..." : js; 104 | logger.LogInformation($"Creating UserDefinedFunction with Content: {displayContent}"); 105 | 106 | var metadataContent = new StringContent(JsonConvert.SerializeObject(userDefinedFunctionCreate), Encoding.UTF8, "application/json"); 107 | metadataContent.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json; charset=utf-8"); 108 | 109 | var multipartContent = new MultipartFormDataContent("userDefinedFunctionBoundary"); 110 | multipartContent.Add(metadataContent, "metadata"); 111 | multipartContent.Add(new StringContent(js), "contents"); 112 | 113 | var response = await httpClient.PostAsync("userdefinedfunctions", multipartContent); 114 | return await GetIdFromResponse(response, logger); 115 | } 116 | 117 | private static async Task GetIdFromResponse(HttpResponseMessage response, ILogger logger) 118 | { 119 | if (!response.IsSuccessStatusCode) 120 | return Guid.Empty; 121 | 122 | var content = await response.Content.ReadAsStringAsync(); 123 | 124 | // strip out the double quotes that come in the response and parse into a guid 125 | if (!Guid.TryParse(content.Substring(1, content.Length - 2), out var createdId)) 126 | { 127 | logger.LogError($"Returned value from POST did not parse into a guid: {content}"); 128 | return Guid.Empty; 129 | } 130 | 131 | return createdId; 132 | } 133 | } 134 | } -------------------------------------------------------------------------------- /occupancy-quickstart/src/api/find.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Net.Http; 8 | using System.Threading.Tasks; 9 | using Microsoft.Extensions.Logging; 10 | using Newtonsoft.Json; 11 | 12 | namespace Microsoft.Azure.DigitalTwins.Samples 13 | { 14 | public partial class Api 15 | { 16 | // Returns a device with same hardwareId and spaceId if there is exactly one. 17 | // Otherwise returns null. 18 | public static async Task FindDevice( 19 | HttpClient httpClient, 20 | ILogger logger, 21 | string hardwareId, 22 | Guid? spaceId, 23 | string includes = null) 24 | { 25 | var filterHardwareIds = $"hardwareIds={hardwareId}"; 26 | var filterSpaceId = spaceId != null ? $"&spaceIds={spaceId.ToString()}" : ""; 27 | var includesParam = includes != null ? $"&includes={includes}" : ""; 28 | var filter = $"{filterHardwareIds}{filterSpaceId}{includesParam}"; 29 | 30 | var response = await httpClient.GetAsync($"devices?{filter}"); 31 | if (response.IsSuccessStatusCode) 32 | { 33 | var content = await response.Content.ReadAsStringAsync(); 34 | var devices = JsonConvert.DeserializeObject>(content); 35 | var matchingDevice = devices.SingleOrDefault(); 36 | if (matchingDevice != null) 37 | { 38 | logger.LogInformation($"Retrieved Unique Device using 'hardwareId' and 'spaceId': {JsonConvert.SerializeObject(matchingDevice, Formatting.Indented)}"); 39 | return matchingDevice; 40 | } 41 | } 42 | return null; 43 | } 44 | 45 | // Returns a matcher with same name and spaceId if there is exactly one. 46 | // Otherwise returns null. 47 | public static async Task> FindMatchers( 48 | HttpClient httpClient, 49 | ILogger logger, 50 | IEnumerable names, 51 | Guid spaceId) 52 | { 53 | var commaDelimitedNames = names.Aggregate((string acc, string s) => acc + "," + s); 54 | var filterNames = $"names={commaDelimitedNames}"; 55 | var filterSpaceId = $"&spaceIds={spaceId.ToString()}"; 56 | var filter = $"{filterNames}{filterSpaceId}"; 57 | 58 | var response = await httpClient.GetAsync($"matchers?{filter}"); 59 | if (response.IsSuccessStatusCode) 60 | { 61 | var content = await response.Content.ReadAsStringAsync(); 62 | var matchers = JsonConvert.DeserializeObject>(content); 63 | if (matchers != null) 64 | { 65 | logger.LogInformation($"Retrieved Unique Matchers using 'names' and 'spaceId': {JsonConvert.SerializeObject(matchers, Formatting.Indented)}"); 66 | return matchers; 67 | } 68 | } 69 | return null; 70 | } 71 | 72 | // Returns a space with same name and parentId if there is exactly one 73 | // that maches that criteria. Otherwise returns null. 74 | public static async Task FindSpace( 75 | HttpClient httpClient, 76 | ILogger logger, 77 | string name, 78 | Guid parentId) 79 | { 80 | var filterName = $"Name eq '{name}'"; 81 | var filterParentSpaceId = parentId != Guid.Empty 82 | ? $"ParentSpaceId eq guid'{parentId}'" 83 | : $"ParentSpaceId eq null"; 84 | var odataFilter = $"$filter={filterName} and {filterParentSpaceId}"; 85 | 86 | var response = await httpClient.GetAsync($"spaces?{odataFilter}"); 87 | if (response.IsSuccessStatusCode) 88 | { 89 | var content = await response.Content.ReadAsStringAsync(); 90 | var spaces = JsonConvert.DeserializeObject>(content); 91 | var matchingSpace = spaces.SingleOrDefault(); 92 | if (matchingSpace != null) 93 | { 94 | logger.LogInformation($"Retrieved Unique Space using 'name' and 'parentSpaceId': {JsonConvert.SerializeObject(matchingSpace, Formatting.Indented)}"); 95 | return matchingSpace; 96 | } 97 | } 98 | return null; 99 | } 100 | 101 | // Returns a user defined fucntion with same name and spaceId if there is exactly one. 102 | // Otherwise returns null. 103 | public static async Task FindUserDefinedFunction( 104 | HttpClient httpClient, 105 | ILogger logger, 106 | string name, 107 | Guid spaceId) 108 | { 109 | var filterNames = $"names={name}"; 110 | var filterSpaceId = $"&spaceIds={spaceId.ToString()}"; 111 | var filter = $"{filterNames}{filterSpaceId}"; 112 | 113 | var response = await httpClient.GetAsync($"userdefinedfunctions?{filter}&includes=matchers"); 114 | if (response.IsSuccessStatusCode) 115 | { 116 | var content = await response.Content.ReadAsStringAsync(); 117 | var userDefinedFunctions = JsonConvert.DeserializeObject>(content); 118 | var userDefinedFunction = userDefinedFunctions.SingleOrDefault(); 119 | if (userDefinedFunction != null) 120 | { 121 | logger.LogInformation($"Retrieved Unique UserDefinedFunction using 'name' and 'spaceId': {JsonConvert.SerializeObject(userDefinedFunction, Formatting.Indented)}"); 122 | return userDefinedFunction; 123 | } 124 | } 125 | return null; 126 | } 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /occupancy-quickstart/src/api/get.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Net.Http; 8 | using System.Text; 9 | using System.Threading.Tasks; 10 | using Microsoft.Extensions.Logging; 11 | using Newtonsoft.Json; 12 | 13 | namespace Microsoft.Azure.DigitalTwins.Samples 14 | { 15 | public partial class Api 16 | { 17 | public static async Task> GetOntologies( 18 | HttpClient httpClient, 19 | ILogger logger) 20 | { 21 | var response = await httpClient.GetAsync($"ontologies"); 22 | if (response.IsSuccessStatusCode) 23 | { 24 | var content = await response.Content.ReadAsStringAsync(); 25 | var ontologies = JsonConvert.DeserializeObject>(content); 26 | logger.LogInformation($"Retrieved Ontologies: {JsonConvert.SerializeObject(ontologies, Formatting.Indented)}"); 27 | return ontologies; 28 | } 29 | else 30 | { 31 | return Array.Empty(); 32 | } 33 | } 34 | 35 | public static async Task> GetPropertyKeys( 36 | HttpClient httpClient, 37 | ILogger logger) 38 | { 39 | var response = await httpClient.GetAsync($"propertykeys"); 40 | if (response.IsSuccessStatusCode) 41 | { 42 | var content = await response.Content.ReadAsStringAsync(); 43 | var propertyKeys = JsonConvert.DeserializeObject>(content); 44 | logger.LogInformation($"Retrieved PropertyKeys: {JsonConvert.SerializeObject(propertyKeys, Formatting.Indented)}"); 45 | return propertyKeys; 46 | } 47 | else 48 | { 49 | return Array.Empty(); 50 | } 51 | } 52 | 53 | public static async Task GetResource( 54 | HttpClient httpClient, 55 | ILogger logger, 56 | Guid id) 57 | { 58 | if (id == Guid.Empty) 59 | throw new ArgumentException("GetResource requires a non empty guid as id"); 60 | 61 | var response = await httpClient.GetAsync($"resources/{id}"); 62 | if (response.IsSuccessStatusCode) 63 | { 64 | var content = await response.Content.ReadAsStringAsync(); 65 | var resource = JsonConvert.DeserializeObject(content); 66 | logger.LogInformation($"Retrieved Resource: {JsonConvert.SerializeObject(resource, Formatting.Indented)}"); 67 | return resource; 68 | } 69 | 70 | return null; 71 | } 72 | 73 | public static async Task GetSpace( 74 | HttpClient httpClient, 75 | ILogger logger, 76 | Guid id, 77 | string includes = null) 78 | { 79 | if (id == Guid.Empty) 80 | throw new ArgumentException("GetSpace requires a non empty guid as id"); 81 | 82 | var response = await httpClient.GetAsync($"spaces/{id}/" + (includes != null ? $"?includes={includes}" : "")); 83 | if (response.IsSuccessStatusCode) 84 | { 85 | var content = await response.Content.ReadAsStringAsync(); 86 | var space = JsonConvert.DeserializeObject(content); 87 | logger.LogInformation($"Retrieved Space: {JsonConvert.SerializeObject(space, Formatting.Indented)}"); 88 | return space; 89 | } 90 | 91 | return null; 92 | } 93 | 94 | public static async Task GetDevice( 95 | HttpClient httpClient, 96 | ILogger logger, 97 | Guid id, 98 | string includes = null) 99 | { 100 | if (id == Guid.Empty) 101 | throw new ArgumentException("GetDevice requires a non empty guid as id"); 102 | 103 | var response = await httpClient.GetAsync($"devices/{id}/" + (includes != null ? $"?includes={includes}" : "")); 104 | if (response.IsSuccessStatusCode) 105 | { 106 | var content = await response.Content.ReadAsStringAsync(); 107 | var device = JsonConvert.DeserializeObject(content); 108 | logger.LogInformation($"Retrieved Device: {JsonConvert.SerializeObject(device, Formatting.Indented)}"); 109 | return device; 110 | } 111 | 112 | return null; 113 | } 114 | 115 | public static async Task> GetSpaces( 116 | HttpClient httpClient, 117 | ILogger logger, 118 | int maxNumberToGet = 10, 119 | string includes = null, 120 | string propertyKey = null) 121 | { 122 | var includesFilter = (includes != null ? $"includes={includes}" : ""); 123 | var propertyKeyFilter = (propertyKey != null ? $"propertyKey={propertyKey}" : ""); 124 | var topFilter = $"$top={maxNumberToGet}"; 125 | var response = await httpClient.GetAsync($"spaces{MakeQueryParams(new [] {includesFilter, propertyKeyFilter, topFilter})}"); 126 | if (response.IsSuccessStatusCode) 127 | { 128 | var content = await response.Content.ReadAsStringAsync(); 129 | var spaces = JsonConvert.DeserializeObject>(content); 130 | logger.LogInformation($"Retrieved {spaces.Count()} Spaces"); 131 | return spaces; 132 | } 133 | else 134 | { 135 | return Array.Empty(); 136 | } 137 | } 138 | 139 | public static async Task> GetSensorsOfSpace( 140 | HttpClient httpClient, 141 | ILogger logger, 142 | Guid spaceId) 143 | { 144 | var response = await httpClient.GetAsync($"sensors?spaceId={spaceId.ToString()}&includes=Types"); 145 | if (response.IsSuccessStatusCode) 146 | { 147 | var content = await response.Content.ReadAsStringAsync(); 148 | var sensors = JsonConvert.DeserializeObject>(content); 149 | logger.LogInformation($"Retrieved {sensors.Count()} Sensors"); 150 | return sensors; 151 | } 152 | else 153 | { 154 | return Array.Empty(); 155 | } 156 | } 157 | 158 | private static string MakeQueryParams(IEnumerable queryParams) 159 | { 160 | return queryParams 161 | .Where(s => !string.IsNullOrWhiteSpace(s)) 162 | .Select((s, i) => (i == 0 ? '?' : '&') + s) 163 | .Aggregate((result, cur) => result + cur); 164 | } 165 | } 166 | } -------------------------------------------------------------------------------- /occupancy-quickstart/src/api/isResourceProvisioning.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System; 5 | using System.Net.Http; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | using Microsoft.Extensions.Logging; 9 | using Newtonsoft.Json; 10 | 11 | namespace Microsoft.Azure.DigitalTwins.Samples 12 | { 13 | public partial class Api 14 | { 15 | public static async Task IsResourceProvisioning( 16 | HttpClient httpClient, 17 | ILogger logger, 18 | Guid id) 19 | { 20 | var resource = await GetResource(httpClient, logger, id); 21 | if (resource == null) 22 | { 23 | logger.LogError($"Failed to find expected resource, {id.ToString()}"); 24 | return false; 25 | } 26 | 27 | return resource.Status.ToLower() == "provisioning"; 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /occupancy-quickstart/src/api/update.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System; 5 | using System.Net.Http; 6 | using System.Net.Http.Headers; 7 | using System.Text; 8 | using System.Threading.Tasks; 9 | using Microsoft.Extensions.Logging; 10 | using Newtonsoft.Json; 11 | 12 | namespace Microsoft.Azure.DigitalTwins.Samples 13 | { 14 | public partial class Api 15 | { 16 | public static async Task UpdateUserDefinedFunction( 17 | HttpClient httpClient, 18 | ILogger logger, 19 | Models.UserDefinedFunctionUpdate userDefinedFunction, 20 | string js) 21 | { 22 | logger.LogInformation($"Updating UserDefinedFunction with Metadata: {JsonConvert.SerializeObject(userDefinedFunction, Formatting.Indented)}"); 23 | var displayContent = js.Length > 100 ? js.Substring(0, 100) + "..." : js; 24 | logger.LogInformation($"Updating UserDefinedFunction with Content: {displayContent}"); 25 | 26 | var metadataContent = new StringContent(JsonConvert.SerializeObject(userDefinedFunction), Encoding.UTF8, "application/json"); 27 | metadataContent.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json; charset=utf-8"); 28 | 29 | var multipartContent = new MultipartFormDataContent("userDefinedFunctionBoundary"); 30 | multipartContent.Add(metadataContent, "metadata"); 31 | multipartContent.Add(new StringContent(js), "contents"); 32 | 33 | await httpClient.PatchAsync($"userdefinedfunctions/{userDefinedFunction.Id}", multipartContent); 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /occupancy-quickstart/src/appSettings.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System; 5 | using System.IO; 6 | using Microsoft.Extensions.Configuration; 7 | 8 | namespace Microsoft.Azure.DigitalTwins.Samples 9 | { 10 | public class AppSettings { 11 | // Note: this is a constant because it is the same for every user authorizing 12 | // against the Digital Twins Apis 13 | private static string DigitalTwinsAppId = "0b07f429-9f4b-4714-9392-cc5e8e80c8b0"; 14 | private static string[] AadScopes = new string[] { DigitalTwinsAppId + "/Read.Write" }; 15 | 16 | public string AADInstance { get; set; } 17 | public string AadRedirectUri { get; set; } 18 | public string ClientId { get; set; } 19 | public string ClientSecret { get; set; } 20 | public string Resource { get; set; } = DigitalTwinsAppId; 21 | public string Tenant { get; set; } 22 | public string BaseUrl { get; set; } 23 | public string Authority => AADInstance + Tenant; 24 | public string[] Scopes { get; set; } = AadScopes; 25 | 26 | public static AppSettings Load() 27 | { 28 | var appSettings = new ConfigurationBuilder() 29 | .SetBasePath(Directory.GetCurrentDirectory()) 30 | .AddJsonFile("appSettings.json") 31 | .AddJsonFile("appSettings.dev.json", optional: true) 32 | .Build() 33 | .Get(); 34 | 35 | // Sanitize input 36 | 37 | // This is because httpClient will behave differently if the 38 | // passed in Uri has a trailing slash when using GetAsync. 39 | appSettings.BaseUrl = EnsureTrailingSlash(appSettings.BaseUrl); 40 | 41 | return appSettings; 42 | } 43 | 44 | private static string EnsureTrailingSlash(string baseUrl) 45 | => baseUrl.Length == 0 || baseUrl[baseUrl.Length-1] == '/' 46 | ? baseUrl 47 | : baseUrl + '/'; 48 | } 49 | } -------------------------------------------------------------------------------- /occupancy-quickstart/src/appSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "AADInstance": "https://login.microsoftonline.com/", 3 | "AadRedirectUri": "http://localhost:8080/", 4 | "ClientId": "", 5 | "Tenant": "", 6 | "BaseUrl": "https://..azuresmartspaces.net/management/api/v1.0/" 7 | } -------------------------------------------------------------------------------- /occupancy-quickstart/src/auth.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System; 5 | using System.Threading.Tasks; 6 | using Microsoft.Extensions.Logging; 7 | using Microsoft.Identity.Client; 8 | using System.Net.Http; 9 | 10 | namespace Microsoft.Azure.DigitalTwins.Samples 11 | { 12 | internal static class Authentication 13 | { 14 | // Gets an access token 15 | // First tries (by making a request) using a cached token and if that 16 | // fails we generated a new one using device login and cache it. 17 | internal static async Task GetToken(AppSettings appSettings) 18 | { 19 | var accessTokenFilename = ".accesstoken"; 20 | var accessToken = ReadAccessTokenFromFile(accessTokenFilename); 21 | if (accessToken == null || !(await TryRequestWithAccessToken(new Uri(appSettings.BaseUrl), accessToken))) 22 | { 23 | accessToken = await Authentication.GetNewToken(appSettings); 24 | System.IO.File.WriteAllText(accessTokenFilename, accessToken); 25 | } 26 | 27 | return accessToken; 28 | } 29 | 30 | private static async Task TryRequestWithAccessToken(Uri baseAddress, string accessToken) 31 | { 32 | // We create a new httpClient so we can force console logging for this operation 33 | var httpClient = new HttpClient(new LoggingHttpHandler(Loggers.ConsoleLogger)) 34 | { 35 | BaseAddress = baseAddress, 36 | }; 37 | httpClient.DefaultRequestHeaders.Add("Authorization", "Bearer " + accessToken); 38 | 39 | Loggers.ConsoleLogger.LogInformation("Checking if previous access token is valid..."); 40 | 41 | return (await httpClient.GetAsync("ontologies")).IsSuccessStatusCode; 42 | } 43 | 44 | private static string ReadAccessTokenFromFile(string filename) 45 | => System.IO.File.Exists(filename) ? System.IO.File.ReadAllText(filename) : null; 46 | 47 | // MSAL.NET configuration. Review the product documentation for more information about MSAL.NET authentication options. 48 | // https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/wiki/ 49 | private static async Task GetNewToken(AppSettings appSettings) 50 | { 51 | IPublicClientApplication app = PublicClientApplicationBuilder 52 | .Create(appSettings.ClientId) 53 | .WithRedirectUri(appSettings.AadRedirectUri) 54 | .WithAuthority(appSettings.Authority) 55 | .Build(); 56 | 57 | AuthenticationResult result = await app 58 | .AcquireTokenInteractive(appSettings.Scopes) 59 | .ExecuteAsync(); 60 | 61 | Console.WriteLine(""); 62 | Console.WriteLine("MSAL Authentication Token Acquired: {0}", result.AccessToken); 63 | Console.WriteLine(""); 64 | return result.AccessToken; 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /occupancy-quickstart/src/loggers.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using Microsoft.Extensions.Logging; 5 | 6 | namespace Microsoft.Azure.DigitalTwins.Samples 7 | { 8 | public static class Loggers 9 | { 10 | public static ILogger SilentLogger = 11 | new Microsoft.Extensions.Logging.LoggerFactory() 12 | .CreateLogger("DigitalTwinsQuickstart"); 13 | public static ILogger ConsoleLogger = 14 | new Microsoft.Extensions.Logging.LoggerFactory() 15 | .AddConsole(LogLevel.Trace) 16 | .CreateLogger("DigitalTwinsQuickstart"); 17 | } 18 | } -------------------------------------------------------------------------------- /occupancy-quickstart/src/loggingHttpHandler.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Net; 8 | using System.Net.Http; 9 | using System.Threading; 10 | using System.Threading.Tasks; 11 | using Microsoft.Extensions.Logging; 12 | using Newtonsoft.Json; 13 | 14 | namespace Microsoft.Azure.DigitalTwins.Samples 15 | { 16 | public class LoggingHttpHandler : DelegatingHandler 17 | { 18 | private ILogger logger; 19 | 20 | public LoggingHttpHandler(ILogger logger) 21 | : base(new HttpClientHandler()) 22 | { 23 | this.logger = logger; 24 | } 25 | 26 | protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) 27 | { 28 | LogRequest(request); 29 | 30 | var response = await base.SendAsync(request, cancellationToken); 31 | 32 | await LogResponse(response); 33 | 34 | return response; 35 | } 36 | 37 | private void LogRequest(HttpRequestMessage request) 38 | { 39 | logger.LogTrace($"Request: {request.Method} {request.RequestUri}"); 40 | 41 | // logger.LogDebug($"More Info: {Serialize(request)}"); 42 | } 43 | 44 | private async Task LogResponse(HttpResponseMessage response) 45 | { 46 | const int maxContentLength = 200; 47 | var content = await response.Content?.ReadAsStringAsync(); 48 | 49 | var statusCode = (int)response.StatusCode; 50 | 51 | // Truncate responses if they are successful. 52 | if (statusCode < 400) 53 | { 54 | var contentMaxLength = content == null || content.Length < maxContentLength 55 | ? content 56 | : content.Substring(0, maxContentLength - 3) + "..."; 57 | 58 | content = contentMaxLength == null ? "" : $", {contentMaxLength}"; 59 | } 60 | 61 | logger.LogTrace($"Response Status: {(int)response.StatusCode}, {response.StatusCode} {content}"); 62 | 63 | // Enable to get more details: 64 | // logger.LogTrace($"Full Response: {Serialize(response)}"); 65 | // logger.LogTrace($"Full Response Content: {content}"); 66 | } 67 | 68 | private static string Serialize(object o) 69 | => JsonConvert.SerializeObject(o, Formatting.Indented); 70 | } 71 | } -------------------------------------------------------------------------------- /occupancy-quickstart/src/models/README.md: -------------------------------------------------------------------------------- 1 | # Models 2 | 3 | This folder contains models used when interacting with Azure Digital Twins Management APIs. 4 | 5 | Note that the supplied models demonstrate only a limited number of the total available fields. 6 | 7 | ## Swagger 8 | 9 | See the included Swagger documentation to see all available models and fields: 10 | 11 | A Swagger sneak preview is provided to demonstrate the API feature set. It's hosted at [docs.westcentralus.azuresmartspaces.net/management/swagger](https://docs.westcentralus.azuresmartspaces.net/management/swagger). 12 | 13 | You can access your own, generated, Management API Swagger documentation at: 14 | 15 | ```plaintext 16 | https://yourInstanceName.yourLocation.azuresmartspaces.net/management/swagger 17 | ``` 18 | 19 | * Replace `yourInstanceName` with the name of your Azure Digital Twins instance. 20 | * Replace `yourLocation` with which server region your instance is hosted on. 21 | 22 | ## AutoRest 23 | 24 | Also see [AutoRest](https://github.com/Azure/autorest) to learn how to generate full C# models from that documentation. 25 | -------------------------------------------------------------------------------- /occupancy-quickstart/src/models/device.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Microsoft.Azure.DigitalTwins.Samples.Models 5 | { 6 | // TODO: Need to fixup ids in models to be Guids (were approriate) 7 | // Then we can change upstream apis like DescriptionExtensions 8 | public class Device 9 | { 10 | public string ConnectionString { get; set; } 11 | public string HardwareId { get; set; } 12 | public string Id { get; set; } 13 | public string Name { get; set; } 14 | public string SpaceId { get; set; } 15 | } 16 | } -------------------------------------------------------------------------------- /occupancy-quickstart/src/models/deviceCreate.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Microsoft.Azure.DigitalTwins.Samples.Models 5 | { 6 | public class DeviceCreate 7 | { 8 | public string HardwareId { get; set; } 9 | public string Name { get; set; } 10 | public string SpaceId { get; set; } 11 | } 12 | } -------------------------------------------------------------------------------- /occupancy-quickstart/src/models/endpointsCreate.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Microsoft.Azure.DigitalTwins.Samples.Models 5 | { 6 | public class EndpointsCreate 7 | { 8 | public string ConnectionString { get; set; } 9 | public string[] EventTypes { get; set; } 10 | public string Path { get; set; } 11 | public string SecondaryConnectionString { get; set; } 12 | public string Type { get; set; } 13 | } 14 | } -------------------------------------------------------------------------------- /occupancy-quickstart/src/models/historicalValues.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System.Collections.Generic; 5 | 6 | namespace Microsoft.Azure.DigitalTwins.Samples.Models 7 | { 8 | public class HistoricalValues 9 | { 10 | public string Description; 11 | public string Value; 12 | } 13 | } -------------------------------------------------------------------------------- /occupancy-quickstart/src/models/keyStoreCreate.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Microsoft.Azure.DigitalTwins.Samples.Models 5 | { 6 | public class KeyStoreCreate 7 | { 8 | public string Name { get; set; } 9 | public string SpaceId { get; set; } 10 | } 11 | } -------------------------------------------------------------------------------- /occupancy-quickstart/src/models/matcher.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System.Collections.Generic; 5 | 6 | namespace Microsoft.Azure.DigitalTwins.Samples.Models 7 | { 8 | public class Matcher 9 | { 10 | public string Id { get; set; } 11 | public string Name { get; set; } 12 | public string SpaceId { get; set; } 13 | } 14 | } -------------------------------------------------------------------------------- /occupancy-quickstart/src/models/matcherCreate.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System.Collections.Generic; 5 | 6 | namespace Microsoft.Azure.DigitalTwins.Samples.Models 7 | { 8 | public class ConditionCreate 9 | { 10 | public string Target { get; set; } 11 | public string Path { get; set; } 12 | public string Value { get; set; } 13 | public string Comparison { get; set; } 14 | } 15 | 16 | public class MatcherCreate 17 | { 18 | public string Name { get; set; } 19 | public string SpaceId { get; set; } 20 | public IEnumerable Conditions { get; set; } 21 | } 22 | } -------------------------------------------------------------------------------- /occupancy-quickstart/src/models/ontology.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Microsoft.Azure.DigitalTwins.Samples.Models 5 | { 6 | public class Ontology 7 | { 8 | public int Id {get; set;} 9 | public string Name { get; set; } 10 | public bool Loaded { get; set; } 11 | } 12 | } -------------------------------------------------------------------------------- /occupancy-quickstart/src/models/property.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Microsoft.Azure.DigitalTwins.Samples.Models 5 | { 6 | public class Property 7 | { 8 | public string DataType { get; set; } 9 | public string Name { get; set; } 10 | public string Value { get; set; } 11 | } 12 | } -------------------------------------------------------------------------------- /occupancy-quickstart/src/models/propertyCreate.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Microsoft.Azure.DigitalTwins.Samples.Models 5 | { 6 | public class PropertyCreate 7 | { 8 | public string Name { get; set; } 9 | public string Value { get; set; } 10 | } 11 | } -------------------------------------------------------------------------------- /occupancy-quickstart/src/models/propertyKeyCreate.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System; 5 | 6 | namespace Microsoft.Azure.DigitalTwins.Samples.Models 7 | { 8 | public class PropertyKeyCreate 9 | { 10 | public string Name { get; set; } 11 | public string Scope { get; set; } 12 | public Guid SpaceId { get; set; } 13 | } 14 | } -------------------------------------------------------------------------------- /occupancy-quickstart/src/models/propertyKeys.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Microsoft.Azure.DigitalTwins.Samples.Models 5 | { 6 | public class PropertyKeys 7 | { 8 | public string Id { get; set; } 9 | public string Name { get; set; } 10 | } 11 | } -------------------------------------------------------------------------------- /occupancy-quickstart/src/models/resource.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Microsoft.Azure.DigitalTwins.Samples.Models 5 | { 6 | public class Resource 7 | { 8 | public string Id { get; set; } 9 | public string Status { get; set; } 10 | } 11 | } -------------------------------------------------------------------------------- /occupancy-quickstart/src/models/resourceCreate.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Microsoft.Azure.DigitalTwins.Samples.Models 5 | { 6 | public class ResourceCreate 7 | { 8 | public string SpaceId { get; set; } 9 | public string Type { get; set; } 10 | } 11 | } -------------------------------------------------------------------------------- /occupancy-quickstart/src/models/roleAssignmentCreate.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System.Collections.Generic; 5 | 6 | namespace Microsoft.Azure.DigitalTwins.Samples.Models 7 | { 8 | public class RoleAssignmentCreate 9 | { 10 | public string ObjectId { get; set; } 11 | public string ObjectIdType { get; set; } 12 | public string Path { get; set; } 13 | public string RoleId { get; set; } 14 | public string TenantId { get; set; } 15 | } 16 | } -------------------------------------------------------------------------------- /occupancy-quickstart/src/models/sensor.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Microsoft.Azure.DigitalTwins.Samples.Models 5 | { 6 | public class Sensor 7 | { 8 | public string DataType { get; set; } 9 | public string DeviceId { get; set; } 10 | public string HardwareId { get; set; } 11 | public string Id { get; set; } 12 | } 13 | } -------------------------------------------------------------------------------- /occupancy-quickstart/src/models/sensorCreate.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Microsoft.Azure.DigitalTwins.Samples.Models 5 | { 6 | public class SensorCreate 7 | { 8 | public string DataType { get; set; } 9 | public string DeviceId { get; set; } 10 | public string HardwareId { get; set; } 11 | } 12 | } -------------------------------------------------------------------------------- /occupancy-quickstart/src/models/sensorValues.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System.Collections.Generic; 5 | 6 | namespace Microsoft.Azure.DigitalTwins.Samples.Models 7 | { 8 | public class SpaceValue 9 | { 10 | public string Type; 11 | public string Value; 12 | public string Timestamp; 13 | public IEnumerable HistoricalValues; 14 | } 15 | } -------------------------------------------------------------------------------- /occupancy-quickstart/src/models/space.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System.Collections.Generic; 5 | 6 | namespace Microsoft.Azure.DigitalTwins.Samples.Models 7 | { 8 | public class Space 9 | { 10 | public string Id { get; set; } 11 | public string Name { get; set; } 12 | public string FriendlyName { get; set; } 13 | public string Type { get; set; } 14 | public int TypeId { get; set; } 15 | public string ParentSpaceId { get; set; } 16 | public string Subtype { get; set; } 17 | public int SubtypeId { get; set; } 18 | public string Status { get; set; } 19 | public int StatusId { get; set; } 20 | public IEnumerable Properties { get; set; } 21 | public IEnumerable Children { get; set; } 22 | public IEnumerable SpacePaths { get; set; } 23 | public IEnumerable Values { get; set; } 24 | } 25 | } -------------------------------------------------------------------------------- /occupancy-quickstart/src/models/spaceCreate.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Microsoft.Azure.DigitalTwins.Samples.Models 5 | { 6 | public class SpaceCreate 7 | { 8 | public string Name { get; set; } 9 | public string Type { get; set; } 10 | public string ParentSpaceId { get; set; } 11 | public string Subtype { get; set; } 12 | } 13 | } -------------------------------------------------------------------------------- /occupancy-quickstart/src/models/userDefinedFunction.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System.Collections.Generic; 5 | 6 | namespace Microsoft.Azure.DigitalTwins.Samples.Models 7 | { 8 | public class UserDefinedFunction 9 | { 10 | public string Id { get; set; } 11 | public IEnumerable Matchers { get; set; } 12 | public string Name { get; set; } 13 | public string SpaceId { get; set; } 14 | } 15 | } -------------------------------------------------------------------------------- /occupancy-quickstart/src/models/userDefinedFunctionCreate.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System.Collections.Generic; 5 | 6 | namespace Microsoft.Azure.DigitalTwins.Samples.Models 7 | { 8 | public class UserDefinedFunctionCreate 9 | { 10 | public IEnumerable Matchers { get; set; } 11 | public string Name { get; set; } 12 | public string SpaceId { get; set; } 13 | } 14 | } -------------------------------------------------------------------------------- /occupancy-quickstart/src/models/userDefinedFunctionUpdate.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System.Collections.Generic; 5 | 6 | namespace Microsoft.Azure.DigitalTwins.Samples.Models 7 | { 8 | public class UserDefinedFunctionUpdate 9 | { 10 | public string Id { get; set; } 11 | public IEnumerable Matchers { get; set; } 12 | public string Name { get; set; } 13 | public string SpaceId { get; set; } 14 | } 15 | } -------------------------------------------------------------------------------- /occupancy-quickstart/src/occupancyQuickstart.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | netcoreapp2.1 6 | latest 7 | Microsoft.Azure.DigitalTwins.Samples 8 | $(MSBuildThisFileDirectory) 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | PreserveNewest 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /occupancy-quickstart/src/program.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.IO; 7 | using System.Linq; 8 | using System.Net; 9 | using System.Net.Http; 10 | using System.Text; 11 | using System.Threading; 12 | using System.Threading.Tasks; 13 | using Microsoft.Extensions.Configuration; 14 | using Newtonsoft.Json; 15 | using YamlDotNet.Serialization; 16 | using Microsoft.Extensions.Logging; 17 | using Microsoft.Identity.Client; 18 | 19 | namespace Microsoft.Azure.DigitalTwins.Samples 20 | { 21 | static class Program 22 | { 23 | static async Task Main(string[] args) 24 | { 25 | try 26 | { 27 | var appSettings = AppSettings.Load(); 28 | 29 | var actionName = ParseArgs(args); 30 | if (actionName == null) 31 | return; 32 | 33 | switch (actionName) 34 | { 35 | case ActionName.CreateEndpoints: 36 | await Actions.CreateEndpoints(await SetupHttpClient(Loggers.ConsoleLogger, appSettings), Loggers.ConsoleLogger); 37 | break; 38 | case ActionName.CreateRoleAssignments: 39 | await Actions.CreateRoleAssignments(await SetupHttpClient(Loggers.ConsoleLogger, appSettings), Loggers.ConsoleLogger); 40 | break; 41 | case ActionName.GetAvailableAndFreshSpaces: 42 | await Actions.GetAvailableAndFreshSpaces(await SetupHttpClient(Loggers.SilentLogger, appSettings)); 43 | break; 44 | case ActionName.GetOntologies: 45 | await Api.GetOntologies(await SetupHttpClient(Loggers.ConsoleLogger, appSettings), Loggers.ConsoleLogger); 46 | break; 47 | case ActionName.GetSpaces: 48 | await Actions.GetSpaces(await SetupHttpClient(Loggers.ConsoleLogger, appSettings), Loggers.ConsoleLogger); 49 | break; 50 | case ActionName.ProvisionSample: 51 | await Actions.ProvisionSample(await SetupHttpClient(Loggers.ConsoleLogger, appSettings), Loggers.ConsoleLogger); 52 | break; 53 | 54 | default: 55 | throw new NotImplementedException(); 56 | } 57 | } 58 | catch (Exception e) 59 | { 60 | Console.WriteLine($"Exception: {e}"); 61 | } 62 | } 63 | 64 | private static ActionName? ParseArgs(string[] args) 65 | { 66 | if (args.Length >= 1 && Enum.TryParse(args[0], out ActionName actionName)) 67 | { 68 | return actionName; 69 | } 70 | else 71 | { 72 | // Generate the list of available action names from the enum 73 | // and output them in the usage string 74 | var actionNames = Enum.GetNames(typeof(ActionName)) 75 | .Aggregate((string acc, string s) => acc + " | " + s); 76 | Console.WriteLine($"Usage: dotnet run [{actionNames}]"); 77 | 78 | return null; 79 | } 80 | } 81 | 82 | private static async Task SetupHttpClient(ILogger logger, AppSettings appSettings) 83 | { 84 | var httpClient = new HttpClient(new LoggingHttpHandler(logger)) 85 | { 86 | BaseAddress = new Uri(appSettings.BaseUrl), 87 | }; 88 | 89 | var accessToken = await Authentication.GetToken(appSettings); 90 | httpClient.DefaultRequestHeaders.Add("Authorization", "Bearer " + accessToken); 91 | return httpClient; 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /occupancy-quickstart/tests/createEndpointsTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System; 5 | using System.Linq; 6 | using Xunit; 7 | using Microsoft.Azure.DigitalTwins.Samples; 8 | using System.Net.Http; 9 | using System.Net; 10 | using System.Threading.Tasks; 11 | using Newtonsoft.Json; 12 | using System.Collections.Generic; 13 | using Moq; 14 | using System.IO; 15 | using System.Collections; 16 | using YamlDotNet.Serialization; 17 | using Microsoft.Extensions.Logging; 18 | 19 | namespace Microsoft.Azure.DigitalTwins.Samples.Tests 20 | { 21 | public class CreateEndpointsTests 22 | { 23 | private static Serializer yamlSerializer = new Serializer(); 24 | private static Guid endpoint1Guid = new Guid("00000000-0000-0000-0000-000000000001"); 25 | private static Guid endpoint2Guid = new Guid("00000000-0000-0000-0000-000000000002"); 26 | 27 | [Fact] 28 | public async Task GetCreateEndpointsCreatesDescriptions() 29 | { 30 | var yaml = @" 31 | - type: Type1 32 | eventTypes: 33 | - EventType1 34 | - EventType2 35 | connectionString: connectionString1 36 | secondaryConnectionString: connectionString2 37 | path: path1 38 | "; 39 | var expectedDescriptions = new [] { new EndpointDescription() 40 | { 41 | type = "Type1", 42 | eventTypes = new [] { "EventType1", "EventType2" }, 43 | connectionString = "connectionString1", 44 | secondaryConnectionString = "connectionString2", 45 | path = "path1", 46 | }}; 47 | var actualDescriptions = await Actions.GetCreateEndpointsDescriptions(new StringReader(yaml)); 48 | Assert.Equal(yamlSerializer.Serialize(expectedDescriptions), yamlSerializer.Serialize(actualDescriptions)); 49 | } 50 | 51 | [Fact] 52 | public async Task CreateTwoEndpoints() 53 | { 54 | var endpointGuids = new[] { endpoint1Guid, endpoint2Guid }; 55 | (var httpClient, var httpHandler) = FakeDigitalTwinsHttpClient.Create( 56 | postResponseGuids: endpointGuids 57 | ); 58 | 59 | var descriptions = new [] { 60 | new EndpointDescription() 61 | { 62 | type = "Type1", 63 | eventTypes = new [] { "EventType1", "EventType2" }, 64 | connectionString = "connectionString1", 65 | secondaryConnectionString = "connectionString2", 66 | path = "path1", 67 | }, 68 | new EndpointDescription() 69 | { 70 | type = "TypeA", 71 | eventTypes = new [] { "EventTypeA", "EventTypeB" }, 72 | connectionString = "connectionStringA", 73 | secondaryConnectionString = "connectionStringB", 74 | path = "pathA", 75 | }, 76 | }; 77 | 78 | Assert.Equal(endpointGuids, 79 | await Actions.CreateEndpoints(httpClient, Loggers.SilentLogger, descriptions)); 80 | Assert.Equal(2, httpHandler.PostRequests["endpoints"].Count); 81 | Assert.False(httpHandler.GetRequests.ContainsKey("endpoints")); 82 | } 83 | } 84 | } -------------------------------------------------------------------------------- /occupancy-quickstart/tests/createRoleAssignmentsTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System; 5 | using System.Linq; 6 | using Xunit; 7 | using Microsoft.Azure.DigitalTwins.Samples; 8 | using System.Net.Http; 9 | using System.Net; 10 | using System.Threading.Tasks; 11 | using Newtonsoft.Json; 12 | using System.Collections.Generic; 13 | using Moq; 14 | using System.IO; 15 | using System.Collections; 16 | using YamlDotNet.Serialization; 17 | using Microsoft.Extensions.Logging; 18 | 19 | namespace Microsoft.Azure.DigitalTwins.Samples.Tests 20 | { 21 | public class CreateRoleAssignmentsTests 22 | { 23 | private static Serializer yamlSerializer = new Serializer(); 24 | private static Guid roleAssignment1Guid = new Guid("00000000-0000-0000-0000-000000000001"); 25 | private static Guid roleAssignment2Guid = new Guid("00000000-0000-0000-0000-000000000002"); 26 | 27 | [Fact] 28 | public async Task GetCreateRoleAssignmentsCreatesDescriptions() 29 | { 30 | var yaml = @" 31 | - objectId: Id1 32 | objectIdType: Type1 33 | path: Path1 34 | roleId: RoleId1 35 | tenantId: TenantId1 36 | "; 37 | var expectedDescriptions = new [] { new RoleAssignmentDescription() 38 | { 39 | objectId = "Id1", 40 | objectIdType = "Type1", 41 | path = "Path1", 42 | roleId = "RoleId1", 43 | tenantId = "TenantId1", 44 | }}; 45 | var actualDescriptions = await Actions.GetCreateRoleAssignmentsDescriptions(new StringReader(yaml)); 46 | Assert.Equal(yamlSerializer.Serialize(expectedDescriptions), yamlSerializer.Serialize(actualDescriptions)); 47 | } 48 | 49 | [Fact] 50 | public async Task CreateTwoRoleAssignments() 51 | { 52 | var roleAssignmentGuids = new[] { roleAssignment1Guid, roleAssignment2Guid }; 53 | (var httpClient, var httpHandler) = FakeDigitalTwinsHttpClient.Create( 54 | postResponseGuids: roleAssignmentGuids 55 | ); 56 | 57 | var descriptions = new [] { 58 | new RoleAssignmentDescription() 59 | { 60 | objectId = "10000000-0000-0000-0000-000000000001", 61 | objectIdType = "UserId", 62 | path = "/", 63 | roleId = "10000000-0000-0000-0000-000000000002", 64 | tenantId = "10000000-0000-0000-0000-000000000003", 65 | }, 66 | new RoleAssignmentDescription() 67 | { 68 | objectId = "20000000-0000-0000-0000-000000000001", 69 | objectIdType = "UserId", 70 | path = "/", 71 | roleId = "20000000-0000-0000-0000-000000000002", 72 | tenantId = "20000000-0000-0000-0000-000000000003", 73 | }, 74 | }; 75 | 76 | Assert.Equal(roleAssignmentGuids, 77 | await Actions.CreateRoleAssignments(httpClient, Loggers.SilentLogger, descriptions)); 78 | Assert.Equal(2, httpHandler.PostRequests["roleassignments"].Count); 79 | Assert.False(httpHandler.GetRequests.ContainsKey("roleassignments")); 80 | } 81 | } 82 | } -------------------------------------------------------------------------------- /occupancy-quickstart/tests/fakeDigitalTwinsHttpClient.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using Newtonsoft.Json; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | using System.Net.Http; 9 | using System.Net; 10 | 11 | namespace Microsoft.Azure.DigitalTwins.Samples.Tests 12 | { 13 | public static class FakeDigitalTwinsHttpClient 14 | { 15 | public static Models.Space Space = new Models.Space() 16 | { 17 | Name = "Space1_HttpClient", 18 | Id = new Guid("90000000-0000-0000-0000-000000000001").ToString(), 19 | Type = "Space1Type", 20 | }; 21 | 22 | public static Models.Device Device = new Models.Device() 23 | { 24 | Name = "Device1_HttpClient", 25 | Id = new Guid("90000000-0000-0000-0000-000000000001").ToString(), 26 | HardwareId = "DeviceHardwareId1_HttpClient", 27 | SpaceId = Space.Id, 28 | }; 29 | 30 | public static (HttpClient, FakeHttpHandler) Create( 31 | IEnumerable postResponseGuids = null, 32 | IEnumerable getResponses = null) 33 | { 34 | return FakeHttpHandler.CreateHttpClient( 35 | postResponses: postResponseGuids != null ? CreateGuidResponses(postResponseGuids) : null, 36 | getResponses: getResponses); 37 | } 38 | 39 | // Creates an httpClient that will respond with a space (this.Space or passed in space) 40 | public static (HttpClient, FakeHttpHandler) CreateWithSpace( 41 | IEnumerable postResponseGuids, 42 | IEnumerable getResponses = null, 43 | IEnumerable patchResponses = null, 44 | Models.Space space = null) 45 | { 46 | postResponseGuids = postResponseGuids ?? Array.Empty(); 47 | getResponses = getResponses ?? Array.Empty(); 48 | patchResponses = patchResponses ?? Array.Empty(); 49 | space = space ?? Space; 50 | 51 | var getRootSpaceResponse = new HttpResponseMessage() 52 | { 53 | StatusCode = HttpStatusCode.OK, 54 | Content = new StringContent(JsonConvert.SerializeObject(new [] { space })), 55 | }; 56 | 57 | var getSensorsForResultsResponse = new [] { Responses.NotFound }; 58 | 59 | return FakeHttpHandler.CreateHttpClient( 60 | postResponses: CreateGuidResponses(postResponseGuids), 61 | getResponses: new [] { getRootSpaceResponse } 62 | .Concat(getResponses) 63 | .Concat(getSensorsForResultsResponse), 64 | patchResponses: patchResponses); 65 | } 66 | 67 | // Creates an httpClient that will respond with a space and device 68 | // (this.Space and this.Device) 69 | public static (HttpClient, FakeHttpHandler) CreateWithDevice( 70 | IEnumerable postResponseGuids, 71 | IEnumerable getResponses = null) 72 | { 73 | getResponses = getResponses ?? Array.Empty(); 74 | 75 | var getDeviceResponse = new HttpResponseMessage() 76 | { 77 | StatusCode = HttpStatusCode.OK, 78 | Content = new StringContent(JsonConvert.SerializeObject(Device)), 79 | }; 80 | var getDevicesResponse = new HttpResponseMessage() 81 | { 82 | StatusCode = HttpStatusCode.OK, 83 | Content = new StringContent(JsonConvert.SerializeObject(new [] { Device })), 84 | }; 85 | return CreateWithSpace( 86 | postResponseGuids: postResponseGuids, 87 | getResponses: new [] { getDevicesResponse, getDeviceResponse }.Concat(getResponses) ); 88 | } 89 | 90 | private static IEnumerable CreateGuidResponses(IEnumerable guids) 91 | => guids.Select(guid => new HttpResponseMessage() 92 | { 93 | StatusCode = HttpStatusCode.OK, 94 | Content = new StringContent($"\"{guid.ToString()}\""), 95 | }); 96 | } 97 | } -------------------------------------------------------------------------------- /occupancy-quickstart/tests/fakeHttpHandler.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Net; 8 | using System.Net.Http; 9 | using System.Threading; 10 | using System.Threading.Tasks; 11 | using Newtonsoft.Json; 12 | 13 | namespace Microsoft.Azure.DigitalTwins.Samples 14 | { 15 | // TODO: we should find something better than manually implementing this but I 16 | // haven't been able to yet: 17 | // See https://github.com/dotnet/corefx/issues/1624 for an example of non trivial discussion around this 18 | public class FakeHttpHandler : DelegatingHandler 19 | { 20 | public static (HttpClient, FakeHttpHandler) CreateHttpClient( 21 | IEnumerable postResponses = null, 22 | IEnumerable getResponses = null, 23 | IEnumerable patchResponses = null) 24 | { 25 | var httpHandler = new FakeHttpHandler() 26 | { 27 | PostResponses = postResponses, 28 | GetResponses = getResponses, 29 | PatchResponses = patchResponses, 30 | }; 31 | return ( 32 | new HttpClient(httpHandler) 33 | { 34 | BaseAddress = new Uri("http://bing.com"), 35 | }, 36 | httpHandler); 37 | } 38 | 39 | public FakeHttpHandler() 40 | : base(new HttpClientHandler()) 41 | { 42 | requests[HttpMethod.Post] = new Dictionary>(); 43 | requests[HttpMethod.Get] = new Dictionary>(); 44 | requests[HttpMethod.Patch] = new Dictionary>(); 45 | } 46 | 47 | public IReadOnlyDictionary> PatchRequests => requests[HttpMethod.Patch]; 48 | public IReadOnlyDictionary> GetRequests => requests[HttpMethod.Get]; 49 | public IReadOnlyDictionary> PostRequests => requests[HttpMethod.Post]; 50 | private Dictionary>> requests = new Dictionary>>(); 51 | 52 | public IEnumerable PostResponses { get; set; } 53 | private IEnumerator enumeratePostResponses; 54 | 55 | public IEnumerable GetResponses { get; set; } 56 | private IEnumerator enumerateGetResponses; 57 | 58 | public IEnumerable PatchResponses { get; set; } 59 | private IEnumerator enumeratePatchResponses; 60 | 61 | protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) 62 | { 63 | var requestName = request.RequestUri.AbsolutePath.Split('/', StringSplitOptions.RemoveEmptyEntries)[0]; 64 | if (!requests[request.Method].ContainsKey(requestName)) 65 | requests[request.Method].Add(requestName, new List()); 66 | requests[request.Method][requestName].Add(request); 67 | 68 | return Task.FromResult(GetNextResponse(ChooseResponseEnumerator(request))); 69 | } 70 | 71 | private static HttpResponseMessage GetNextResponse(IEnumerator enumerator) 72 | { 73 | if (enumerator == null || !enumerator.MoveNext()) 74 | throw new InvalidOperationException("FakeHttpHandler ran out of responses"); 75 | return enumerator.Current; 76 | } 77 | 78 | private IEnumerator ChooseResponseEnumerator(HttpRequestMessage request) 79 | { 80 | if (request.Method == HttpMethod.Get) 81 | { 82 | if (enumerateGetResponses == null) 83 | enumerateGetResponses = GetResponses.GetEnumerator(); 84 | return enumerateGetResponses; 85 | } 86 | else if (request.Method == HttpMethod.Patch) 87 | { 88 | if (enumeratePatchResponses == null) 89 | enumeratePatchResponses = PatchResponses.GetEnumerator(); 90 | return enumeratePatchResponses; 91 | } 92 | else if (request.Method == HttpMethod.Post) 93 | { 94 | if (enumeratePostResponses == null) 95 | enumeratePostResponses = PostResponses.GetEnumerator(); 96 | return enumeratePostResponses; 97 | } 98 | else 99 | { 100 | throw new NotImplementedException(); 101 | } 102 | } 103 | } 104 | } -------------------------------------------------------------------------------- /occupancy-quickstart/tests/loggers.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using Microsoft.Extensions.Logging; 5 | using Moq; 6 | 7 | namespace Microsoft.Azure.DigitalTwins.Samples.Tests 8 | { 9 | public static class Loggers 10 | { 11 | public static ILogger SilentLogger = new Mock().Object; 12 | public static ILogger ConsoleLogger = 13 | new Microsoft.Extensions.Logging.LoggerFactory() 14 | .AddConsole(LogLevel.Trace) 15 | .CreateLogger("DigitalTwinsQuickstartTests"); 16 | } 17 | } -------------------------------------------------------------------------------- /occupancy-quickstart/tests/occupancyQuickstart.tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp2.1 5 | false 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | PreserveNewest 17 | 18 | 19 | PreserveNewest 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /occupancy-quickstart/tests/provisionSampleDevicesTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System; 5 | using System.Linq; 6 | using Xunit; 7 | using Microsoft.Azure.DigitalTwins.Samples; 8 | using System.Net.Http; 9 | using System.Net; 10 | using System.Threading.Tasks; 11 | using Newtonsoft.Json; 12 | using System.Collections.Generic; 13 | using Moq; 14 | using System.IO; 15 | using System.Collections; 16 | using YamlDotNet.Serialization; 17 | using Microsoft.Extensions.Logging; 18 | 19 | namespace Microsoft.Azure.DigitalTwins.Samples.Tests 20 | { 21 | public class ProvisionSampleDevicesTests 22 | { 23 | private static Serializer yamlSerializer = new Serializer(); 24 | private static Guid device1Guid = new Guid("00000000-0000-0000-0000-000000000001"); 25 | private static Guid device2Guid = new Guid("00000000-0000-0000-0000-000000000002"); 26 | private static Models.Device device1 = new Models.Device() 27 | { 28 | HardwareId = "HardwareId1", 29 | Id = device1Guid.ToString(), 30 | Name = "Device1", 31 | ConnectionString = "ConnectionString1", 32 | }; 33 | private static Models.Device device2 = new Models.Device() 34 | { 35 | HardwareId = "HardwareId2", 36 | Id = device2Guid.ToString(), 37 | Name = "Device2", 38 | ConnectionString = "ConnectionString2", 39 | }; 40 | private static HttpResponseMessage getDevicesResponse_device1 = new HttpResponseMessage() 41 | { 42 | StatusCode = HttpStatusCode.OK, 43 | Content = new StringContent(JsonConvert.SerializeObject(new [] { device1 })), 44 | }; 45 | private static HttpResponseMessage getDeviceResponse_device1 = new HttpResponseMessage() 46 | { 47 | StatusCode = HttpStatusCode.OK, 48 | Content = new StringContent(JsonConvert.SerializeObject(device1)), 49 | }; 50 | private static HttpResponseMessage getDevicesResponse_device2 = new HttpResponseMessage() 51 | { 52 | StatusCode = HttpStatusCode.OK, 53 | Content = new StringContent(JsonConvert.SerializeObject(new [] { device2 })), 54 | }; 55 | private static HttpResponseMessage getDeviceResponse_device2 = new HttpResponseMessage() 56 | { 57 | StatusCode = HttpStatusCode.OK, 58 | Content = new StringContent(JsonConvert.SerializeObject(device2)), 59 | }; 60 | 61 | [Fact] 62 | public async Task GetProvisionSampleCreatesDescriptions() 63 | { 64 | var yaml = @" 65 | - name: Test1 66 | devices: 67 | - name: Device1 68 | hardwareId: HardwareId1 69 | - name: Device2 70 | hardwareId: HardwareId2 71 | "; 72 | var expectedDescriptions = new [] { new SpaceDescription() 73 | { 74 | name = "Test1", 75 | devices = new [] { 76 | new DeviceDescription() 77 | { 78 | name = "Device1", 79 | hardwareId = "HardwareId1", 80 | }, 81 | new DeviceDescription() 82 | { 83 | name = "Device2", 84 | hardwareId = "HardwareId2", 85 | }, 86 | }, 87 | }}; 88 | var actualDescriptions = await Actions.GetProvisionSampleTopology(new StringReader(yaml)); 89 | Assert.Equal(yamlSerializer.Serialize(expectedDescriptions), yamlSerializer.Serialize(actualDescriptions)); 90 | } 91 | 92 | [Fact] 93 | public async Task CreateTwoDevices() 94 | { 95 | (var httpClient, var httpHandler) = FakeDigitalTwinsHttpClient.CreateWithSpace( 96 | postResponseGuids: new [] { device1Guid, device2Guid }, 97 | getResponses: new [] { Responses.NotFound, getDeviceResponse_device1, Responses.NotFound, getDeviceResponse_device2 } 98 | ); 99 | 100 | var descriptions = new [] { new SpaceDescription() 101 | { 102 | name = FakeDigitalTwinsHttpClient.Space.Name, 103 | devices = new [] { 104 | new DeviceDescription() 105 | { 106 | name = "Device1", 107 | hardwareId = "HardwareId1", 108 | }, 109 | new DeviceDescription() 110 | { 111 | name = "Device2", 112 | hardwareId = "HardwareId2", 113 | }}, 114 | }}; 115 | 116 | var spaceResult = (await Actions.CreateSpaces(httpClient, Loggers.SilentLogger, descriptions, Guid.Empty)) 117 | .Single(); 118 | Assert.Equal(2, httpHandler.PostRequests["devices"].Count); 119 | Assert.Equal(4, httpHandler.GetRequests["devices"].Count); 120 | Assert.Equal(new [] { 121 | new ProvisionResults.Device () { 122 | ConnectionString = "ConnectionString1", 123 | HardwareId = "HardwareId1", 124 | }, 125 | new ProvisionResults.Device () { 126 | ConnectionString = "ConnectionString2", 127 | HardwareId = "HardwareId2", 128 | }}, 129 | spaceResult.Devices); 130 | } 131 | 132 | [Fact] 133 | public async Task CreateDeviceReusesMatchingPreexistingDevice() 134 | { 135 | (var httpClient, var httpHandler) = FakeDigitalTwinsHttpClient.CreateWithSpace( 136 | postResponseGuids: null, 137 | getResponses: new [] { getDevicesResponse_device1, getDeviceResponse_device1 } 138 | ); 139 | 140 | var descriptions = new [] { new SpaceDescription() 141 | { 142 | name = FakeDigitalTwinsHttpClient.Space.Name, 143 | devices = new [] { 144 | new DeviceDescription() 145 | { 146 | name = device1.Name, 147 | hardwareId = device1.HardwareId, 148 | }}, 149 | }}; 150 | 151 | await Actions.CreateSpaces(httpClient, Loggers.SilentLogger, descriptions, Guid.Empty); 152 | Assert.False(httpHandler.PostRequests.ContainsKey("devices")); 153 | Assert.Equal(2, httpHandler.GetRequests["devices"].Count); 154 | } 155 | } 156 | } -------------------------------------------------------------------------------- /occupancy-quickstart/tests/provisionSampleMatchersTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System; 5 | using System.Linq; 6 | using Xunit; 7 | using Microsoft.Azure.DigitalTwins.Samples; 8 | using System.Net.Http; 9 | using System.Net; 10 | using System.Threading.Tasks; 11 | using Newtonsoft.Json; 12 | using System.Collections.Generic; 13 | using Moq; 14 | using System.IO; 15 | using System.Collections; 16 | using YamlDotNet.Serialization; 17 | using Microsoft.Extensions.Logging; 18 | 19 | namespace Microsoft.Azure.DigitalTwins.Samples.Tests 20 | { 21 | public class ProvisionSampleMatchersTests 22 | { 23 | private static Serializer yamlSerializer = new Serializer(); 24 | private static Guid matcher1Guid = new Guid("00000000-0000-0000-0000-000000000001"); 25 | private static Guid matcher2Guid = new Guid("00000000-0000-0000-0000-000000000002"); 26 | 27 | [Fact] 28 | public async Task GetProvisionSampleCreatesDescriptions() 29 | { 30 | var yaml = @" 31 | - name: Test1 32 | matchers: 33 | - name: Matcher1 34 | dataTypeValue: dataType1 35 | - name: Matcher2 36 | dataTypeValue: dataType2 37 | "; 38 | var expectedDescriptions = new [] { new SpaceDescription() 39 | { 40 | name = "Test1", 41 | matchers = new [] { 42 | new MatcherDescription() 43 | { 44 | name = "Matcher1", 45 | dataTypeValue = "dataType1", 46 | }, 47 | new MatcherDescription() 48 | { 49 | name = "Matcher2", 50 | dataTypeValue = "dataType2", 51 | }, 52 | }, 53 | }}; 54 | var actualDescriptions = await Actions.GetProvisionSampleTopology(new StringReader(yaml)); 55 | Assert.Equal(yamlSerializer.Serialize(expectedDescriptions), yamlSerializer.Serialize(actualDescriptions)); 56 | } 57 | 58 | [Fact] 59 | public async Task CreateTwoMatchers() 60 | { 61 | (var httpClient, var httpHandler) = FakeDigitalTwinsHttpClient.CreateWithSpace( 62 | postResponseGuids: new [] { matcher1Guid, matcher2Guid }, 63 | getResponses: null 64 | ); 65 | 66 | var descriptions = new [] { new SpaceDescription() 67 | { 68 | name = FakeDigitalTwinsHttpClient.Space.Name, 69 | matchers = new [] { 70 | new MatcherDescription() 71 | { 72 | name = "Matcher1", 73 | dataTypeValue = "DataType1", 74 | }, 75 | new MatcherDescription() 76 | { 77 | name = "Matcher2", 78 | dataTypeValue = "DataType2", 79 | }}, 80 | }}; 81 | 82 | await Actions.CreateSpaces(httpClient, Loggers.SilentLogger, descriptions, Guid.Empty); 83 | Assert.Equal(2, httpHandler.PostRequests["matchers"].Count); 84 | Assert.False(httpHandler.GetRequests.ContainsKey("matchers")); 85 | } 86 | } 87 | } -------------------------------------------------------------------------------- /occupancy-quickstart/tests/provisionSampleResourcesTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System; 5 | using System.Linq; 6 | using Xunit; 7 | using Microsoft.Azure.DigitalTwins.Samples; 8 | using System.Net.Http; 9 | using System.Net; 10 | using System.Threading.Tasks; 11 | using Newtonsoft.Json; 12 | using System.Collections.Generic; 13 | using Moq; 14 | using System.IO; 15 | using System.Collections; 16 | using YamlDotNet.Serialization; 17 | using Microsoft.Extensions.Logging; 18 | 19 | namespace Microsoft.Azure.DigitalTwins.Samples.Tests 20 | { 21 | public class ProvisionSampleResourcesTests 22 | { 23 | private static Serializer yamlSerializer = new Serializer(); 24 | private static Guid resource1Guid = new Guid("00000000-0000-0000-0000-000000000001"); 25 | private static Models.Resource resource1 = new Models.Resource() 26 | { 27 | Id = resource1Guid.ToString(), 28 | Status = "Something", 29 | }; 30 | private static HttpResponseMessage resource1GetResponse = new HttpResponseMessage() 31 | { 32 | StatusCode = HttpStatusCode.OK, 33 | Content = new StringContent(JsonConvert.SerializeObject(resource1)), 34 | }; 35 | 36 | [Fact] 37 | public async Task GetProvisionSampleCreatesDescriptions() 38 | { 39 | var yaml = @" 40 | - name: Test1 41 | resources: 42 | - type: Type1 43 | - type: Type2 44 | "; 45 | var expectedDescriptions = new [] { new SpaceDescription() 46 | { 47 | name = "Test1", 48 | resources = new [] { 49 | new ResourceDescription() 50 | { 51 | type = "Type1", 52 | }, 53 | new ResourceDescription() 54 | { 55 | type = "Type2", 56 | }, 57 | }, 58 | }}; 59 | var actualDescriptions = await Actions.GetProvisionSampleTopology(new StringReader(yaml)); 60 | Assert.Equal(yamlSerializer.Serialize(expectedDescriptions), yamlSerializer.Serialize(actualDescriptions)); 61 | } 62 | 63 | [Fact] 64 | public async Task CreateSingleResource() 65 | { 66 | (var httpClient, var httpHandler) = FakeDigitalTwinsHttpClient.CreateWithSpace( 67 | postResponseGuids: new [] { resource1Guid }, 68 | getResponses: new [] { resource1GetResponse }); 69 | 70 | var descriptions = new [] { new SpaceDescription() 71 | { 72 | name = FakeDigitalTwinsHttpClient.Space.Name, 73 | resources = new [] { new ResourceDescription() 74 | { 75 | type = "ResourceType", 76 | }}, 77 | }}; 78 | 79 | await Actions.CreateSpaces(httpClient, Loggers.SilentLogger, descriptions, Guid.Empty); 80 | Assert.Equal(1, httpHandler.PostRequests["resources"].Count); 81 | Assert.Equal(1, httpHandler.GetRequests["resources"].Count); 82 | } 83 | } 84 | } -------------------------------------------------------------------------------- /occupancy-quickstart/tests/provisionSampleRoleAssignmentsTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System; 5 | using System.Linq; 6 | using Xunit; 7 | using Microsoft.Azure.DigitalTwins.Samples; 8 | using System.Net.Http; 9 | using System.Net; 10 | using System.Threading.Tasks; 11 | using Newtonsoft.Json; 12 | using System.Collections.Generic; 13 | using Moq; 14 | using System.IO; 15 | using System.Collections; 16 | using YamlDotNet.Serialization; 17 | using Microsoft.Extensions.Logging; 18 | 19 | namespace Microsoft.Azure.DigitalTwins.Samples.Tests 20 | { 21 | public class ProvisionSampleRoleAssignmentsTests 22 | { 23 | private static Serializer yamlSerializer = new Serializer(); 24 | private static Guid roleAssignmentGuid1 = new Guid("00000000-0000-0000-0000-000000000001"); 25 | private static Guid roleAssignmentGuid2 = new Guid("00000000-0000-0000-0000-000000000002"); 26 | private static Guid roleIdGuid = new Guid("99999999-0000-0000-0000-000000000000"); 27 | private static Models.Space space1 = new Models.Space() 28 | { 29 | Id = new Guid("80000000-0000-0000-0000-000000000001").ToString(), 30 | Name = "Space 1", 31 | SpacePaths = new [] 32 | { 33 | "Some1\\Path1", 34 | }, 35 | }; 36 | private static Models.UserDefinedFunction function1 = new Models.UserDefinedFunction() 37 | { 38 | Id = new Guid("90000000-0000-0000-0000-000000000001").ToString(), 39 | Name = "Function 1", 40 | }; 41 | private static Models.UserDefinedFunction function2 = new Models.UserDefinedFunction() 42 | { 43 | Id = new Guid("90000000-0000-0000-0000-000000000002").ToString(), 44 | Name = "Function 2", 45 | }; 46 | private static HttpResponseMessage function1GetResponse = new HttpResponseMessage() 47 | { 48 | StatusCode = HttpStatusCode.OK, 49 | Content = new StringContent(JsonConvert.SerializeObject(new [] { function1 })), 50 | }; 51 | private static HttpResponseMessage function2GetResponse = new HttpResponseMessage() 52 | { 53 | StatusCode = HttpStatusCode.OK, 54 | Content = new StringContent(JsonConvert.SerializeObject(new [] { function2 })), 55 | }; 56 | private static HttpResponseMessage space1GetResponse = new HttpResponseMessage() 57 | { 58 | StatusCode = HttpStatusCode.OK, 59 | Content = new StringContent(JsonConvert.SerializeObject(space1)), 60 | }; 61 | 62 | [Fact] 63 | public async Task GetProvisionSampleCreatesDescriptions() 64 | { 65 | var yaml = @" 66 | - name: Test1 67 | roleassignments: 68 | - roleId: Id1 69 | objectName: Name1 70 | objectIdType: Type1 71 | - roleId: Id2 72 | objectName: Name2 73 | objectIdType: Type2 74 | "; 75 | var expectedDescriptions = new [] { new SpaceDescription() 76 | { 77 | name = "Test1", 78 | roleassignments = new [] { 79 | new RoleAssignmentDescription() 80 | { 81 | roleId = "Id1", 82 | objectName = "Name1", 83 | objectIdType = "Type1", 84 | }, 85 | new RoleAssignmentDescription() 86 | { 87 | roleId = "Id2", 88 | objectName = "Name2", 89 | objectIdType = "Type2", 90 | }, 91 | }, 92 | }}; 93 | var actualDescriptions = await Actions.GetProvisionSampleTopology(new StringReader(yaml)); 94 | Assert.Equal(yamlSerializer.Serialize(expectedDescriptions), yamlSerializer.Serialize(actualDescriptions)); 95 | } 96 | 97 | [Fact] 98 | public async Task CreateRoleAssignmentWithUnknownTypeAndNameFails() 99 | { 100 | // How 'objectName' is used is based on objectIdType. In this test we choose an unknow type 101 | // and expect it then to fail the role assignment creation since it doesn't know how to use the name 102 | 103 | (var httpClient, var httpHandler) = FakeDigitalTwinsHttpClient.CreateWithSpace( 104 | postResponseGuids: null, 105 | getResponses: new [] { space1GetResponse } 106 | ); 107 | 108 | var descriptions = new [] { new SpaceDescription() 109 | { 110 | name = FakeDigitalTwinsHttpClient.Space.Name, 111 | roleassignments = new [] { 112 | new RoleAssignmentDescription() 113 | { 114 | roleId = roleIdGuid.ToString(), 115 | objectName = "SomeName", 116 | objectIdType = "UnknownObjectIdType" 117 | }}, 118 | }}; 119 | 120 | await Actions.CreateSpaces(httpClient, Loggers.SilentLogger, descriptions, Guid.Empty); 121 | Assert.False(httpHandler.PostRequests.ContainsKey("roleassignments")); 122 | } 123 | 124 | [Fact] 125 | public async Task CreateTwoUdfRoleAssignments() 126 | { 127 | (var httpClient, var httpHandler) = FakeDigitalTwinsHttpClient.CreateWithSpace( 128 | postResponseGuids: new [] { roleAssignmentGuid1, roleAssignmentGuid2 }, 129 | getResponses: new [] { space1GetResponse, function1GetResponse, function2GetResponse } 130 | ); 131 | 132 | var descriptions = new [] { new SpaceDescription() 133 | { 134 | name = FakeDigitalTwinsHttpClient.Space.Name, 135 | roleassignments = new [] { 136 | new RoleAssignmentDescription() 137 | { 138 | roleId = roleIdGuid.ToString(), 139 | objectName = "Function 1", 140 | objectIdType = "UserDefinedFunctionId" 141 | }, 142 | new RoleAssignmentDescription() 143 | { 144 | roleId = roleIdGuid.ToString(), 145 | objectName = "Function 2", 146 | objectIdType = "UserDefinedFunctionId" 147 | }}, 148 | }}; 149 | 150 | await Actions.CreateSpaces(httpClient, Loggers.SilentLogger, descriptions, Guid.Empty); 151 | Assert.Equal(2, httpHandler.PostRequests["roleassignments"].Count); 152 | Assert.Equal(2, httpHandler.GetRequests["userdefinedfunctions"].Count); 153 | Assert.False(httpHandler.GetRequests.ContainsKey("roleassignments")); 154 | } 155 | } 156 | } -------------------------------------------------------------------------------- /occupancy-quickstart/tests/provisionSampleSensorsTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System; 5 | using System.Linq; 6 | using Xunit; 7 | using Microsoft.Azure.DigitalTwins.Samples; 8 | using System.Net.Http; 9 | using System.Net; 10 | using System.Threading.Tasks; 11 | using Newtonsoft.Json; 12 | using System.Collections.Generic; 13 | using Moq; 14 | using System.IO; 15 | using System.Collections; 16 | using YamlDotNet.Serialization; 17 | using Microsoft.Extensions.Logging; 18 | 19 | namespace Microsoft.Azure.DigitalTwins.Samples.Tests 20 | { 21 | public class ProvisionSampleSensorsTests 22 | { 23 | private static Serializer yamlSerializer = new Serializer(); 24 | private static Guid sensor1Guid = new Guid("00000000-0000-0000-0000-000000000001"); 25 | private static Guid sensor2Guid = new Guid("00000000-0000-0000-0000-000000000002"); 26 | 27 | [Fact] 28 | public async Task GetProvisionSampleCreatesDescriptions() 29 | { 30 | var yaml = @" 31 | - name: Test1 32 | devices: 33 | - name: Device1 34 | hardwareId: DeviceHardwareId1 35 | sensors: 36 | - dataType: SensorType1 37 | hardwareId: SensorHardwareId1 38 | "; 39 | var expectedDescriptions = new [] { new SpaceDescription() 40 | { 41 | name = "Test1", 42 | devices = new [] { 43 | new DeviceDescription() 44 | { 45 | name = "Device1", 46 | hardwareId = "DeviceHardwareId1", 47 | sensors = new [] { 48 | new SensorDescription() 49 | { 50 | dataType = "SensorType1", 51 | hardwareId = "SensorHardwareId1", 52 | } 53 | }, 54 | }, 55 | }, 56 | }}; 57 | var actualDescriptions = await Actions.GetProvisionSampleTopology(new StringReader(yaml)); 58 | Assert.Equal(yamlSerializer.Serialize(expectedDescriptions), yamlSerializer.Serialize(actualDescriptions)); 59 | } 60 | 61 | [Fact] 62 | public async Task CreateTwoSensors() 63 | { 64 | (var httpClient, var httpHandler) = FakeDigitalTwinsHttpClient.CreateWithDevice( 65 | postResponseGuids: new [] { sensor1Guid, sensor2Guid }, 66 | getResponses: Enumerable.Repeat(Responses.NotFound, 2) 67 | ); 68 | 69 | var descriptions = new [] { new SpaceDescription() 70 | { 71 | name = FakeDigitalTwinsHttpClient.Space.Name, 72 | devices = new [] { 73 | new DeviceDescription() 74 | { 75 | name = FakeDigitalTwinsHttpClient.Device.Name, 76 | hardwareId = FakeDigitalTwinsHttpClient.Device.HardwareId, 77 | sensors = new [] { 78 | new SensorDescription() 79 | { 80 | dataType = "SensorType1", 81 | hardwareId = "SensorHardwareId1", 82 | }, 83 | new SensorDescription() 84 | { 85 | dataType = "SensorType2", 86 | hardwareId = "SensorHardwareId2", 87 | } 88 | } 89 | } 90 | }, 91 | }}; 92 | 93 | await Actions.CreateSpaces(httpClient, Loggers.SilentLogger, descriptions, Guid.Empty); 94 | Assert.Equal(2, httpHandler.PostRequests["sensors"].Count); 95 | } 96 | } 97 | } -------------------------------------------------------------------------------- /occupancy-quickstart/tests/provisionSampleSpacesTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System; 5 | using System.Linq; 6 | using Xunit; 7 | using Microsoft.Azure.DigitalTwins.Samples; 8 | using System.Net.Http; 9 | using System.Net; 10 | using System.Threading.Tasks; 11 | using Newtonsoft.Json; 12 | using System.Collections.Generic; 13 | using Moq; 14 | using System.IO; 15 | using System.Collections; 16 | using YamlDotNet.Serialization; 17 | using Microsoft.Extensions.Logging; 18 | 19 | namespace Microsoft.Azure.DigitalTwins.Samples.Tests 20 | { 21 | public class ProvisionSampleSpacesTests 22 | { 23 | private static Serializer yamlSerializer = new Serializer(); 24 | private static Guid guid1 = new Guid("00000000-0000-0000-0000-000000000001"); 25 | private static Guid guid2 = new Guid("00000000-0000-0000-0000-000000000002"); 26 | private static Guid guid3 = new Guid("00000000-0000-0000-0000-000000000003"); 27 | private static Models.Space space1 = new Models.Space() 28 | { 29 | Name = "Space1", 30 | Id = guid1.ToString(), 31 | Type = "Space1Type", 32 | }; 33 | 34 | [Fact] 35 | public async Task GetProvisionSampleCreatesDescriptions() 36 | { 37 | var yaml = @" 38 | - name: Test1 39 | type: TestType1 40 | spaces: 41 | - name: Child1 42 | - name: Child2 43 | "; 44 | var expectedDescriptions = new [] { new SpaceDescription() 45 | { 46 | name = "Test1", 47 | type = "TestType1", 48 | spaces = new [] { 49 | new SpaceDescription() 50 | { 51 | name = "Child1", 52 | }, 53 | new SpaceDescription() 54 | { 55 | name = "Child2", 56 | }}, 57 | }}; 58 | var actualDescriptions = await Actions.GetProvisionSampleTopology(new StringReader(yaml)); 59 | Assert.Equal(yamlSerializer.Serialize(expectedDescriptions), yamlSerializer.Serialize(actualDescriptions)); 60 | } 61 | 62 | [Fact] 63 | public async Task CreateSpacesWithNoDescriptionsReturnsEmptyAndMakesNoRequests() 64 | { 65 | (var httpClient, var httpHandler) = FakeDigitalTwinsHttpClient.Create(); 66 | 67 | var results = await Actions.CreateSpaces(httpClient, Loggers.SilentLogger, Array.Empty(), Guid.Empty); 68 | 69 | Assert.Equal(0, results.Count()); 70 | Assert.False(httpHandler.PostRequests.ContainsKey("spaces")); 71 | Assert.False(httpHandler.GetRequests.ContainsKey("spaces")); 72 | } 73 | 74 | [Fact] 75 | public async Task CreateSpacesWithSingleSpaceMakesRequestsAndReturnsRootId() 76 | { 77 | (var httpClient, var httpHandler) = FakeDigitalTwinsHttpClient.Create( 78 | postResponseGuids: new [] { guid1 }, 79 | getResponses: Enumerable.Repeat(Responses.NotFound, 1000)); 80 | 81 | var descriptions = new [] { new SpaceDescription() 82 | { 83 | name = "Test1", 84 | }}; 85 | 86 | var results = await Actions.CreateSpaces(httpClient, Loggers.SilentLogger, descriptions, Guid.Empty); 87 | Assert.Equal(guid1, results.Single().Id); 88 | Assert.Equal(1, httpHandler.PostRequests["spaces"].Count); 89 | Assert.Equal(1, httpHandler.GetRequests["spaces"].Count); 90 | } 91 | 92 | [Fact] 93 | public async Task CreateSpacesWithAlreadyCreatedSpaceUsesIt() 94 | { 95 | (var httpClient, var httpHandler) = FakeDigitalTwinsHttpClient.CreateWithSpace( 96 | postResponseGuids: new [] { guid1 }, 97 | space: space1); 98 | 99 | var descriptions = new [] { new SpaceDescription() 100 | { 101 | name = space1.Name, 102 | }}; 103 | 104 | var results = await Actions.CreateSpaces(httpClient, Loggers.SilentLogger, descriptions, Guid.Empty); 105 | Assert.Equal(guid1, results.Single().Id); 106 | Assert.False(httpHandler.PostRequests.ContainsKey("spaces")); 107 | Assert.Equal(1, httpHandler.GetRequests["spaces"].Count); 108 | } 109 | 110 | [Fact] 111 | public async Task CreateSpacesWithSingleRootAndChildrenMakesRequestsAndReturnsRootId() 112 | { 113 | (var httpClient, var httpHandler) = FakeDigitalTwinsHttpClient.Create( 114 | postResponseGuids: new [] { guid1, guid2, guid3 }, 115 | getResponses: Enumerable.Repeat(Responses.NotFound, 1000)); 116 | 117 | var descriptions = new [] { new SpaceDescription() 118 | { 119 | name = "Test1", 120 | spaces = new [] { 121 | new SpaceDescription() 122 | { 123 | name = "Child1", 124 | }, 125 | new SpaceDescription() 126 | { 127 | name = "Child2", 128 | }}, 129 | }}; 130 | 131 | var results = await Actions.CreateSpaces(httpClient, Loggers.SilentLogger, descriptions, Guid.Empty); 132 | Assert.Equal(guid1, results.Single().Id); 133 | Assert.Equal(3, httpHandler.PostRequests["spaces"].Count); 134 | Assert.Equal(3, httpHandler.GetRequests["spaces"].Count); 135 | } 136 | 137 | [Fact] 138 | public async Task CreateSpacesWithMultipleRootsMakesRequestsAndReturnsAllRootIds() 139 | { 140 | (var httpClient, var httpHandler) = FakeDigitalTwinsHttpClient.Create( 141 | postResponseGuids: new [] { guid1, guid2 }, 142 | getResponses: Enumerable.Repeat(Responses.NotFound, 1000)); 143 | 144 | var descriptions = new [] 145 | { 146 | new SpaceDescription() 147 | { 148 | name = "Test1", 149 | }, 150 | new SpaceDescription() 151 | { 152 | name = "Test2", 153 | } 154 | }; 155 | 156 | var results = await Actions.CreateSpaces(httpClient, Loggers.SilentLogger, descriptions, Guid.Empty); 157 | Assert.Equal(new [] { guid1, guid2 }, results.Select(r => r.Id)); 158 | Assert.Equal(2, httpHandler.PostRequests["spaces"].Count); 159 | Assert.Equal(2, httpHandler.GetRequests["spaces"].Count); 160 | } 161 | } 162 | } -------------------------------------------------------------------------------- /occupancy-quickstart/tests/provisionSampleUserDefinedFunctionsTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System; 5 | using System.Linq; 6 | using Xunit; 7 | using Microsoft.Azure.DigitalTwins.Samples; 8 | using System.Net.Http; 9 | using System.Net; 10 | using System.Threading.Tasks; 11 | using Newtonsoft.Json; 12 | using System.Collections.Generic; 13 | using Moq; 14 | using System.IO; 15 | using System.Collections; 16 | using YamlDotNet.Serialization; 17 | using Microsoft.Extensions.Logging; 18 | 19 | namespace Microsoft.Azure.DigitalTwins.Samples.Tests 20 | { 21 | public class ProvisionSampleUserDefinedFunctionsTests 22 | { 23 | private static Serializer yamlSerializer = new Serializer(); 24 | private static Guid udfGuid1 = new Guid("00000000-0000-0000-0000-000000000001"); 25 | private static Guid udfGuid2 = new Guid("00000000-0000-0000-0000-000000000002"); 26 | private static Models.Matcher matcher1 = new Models.Matcher() 27 | { 28 | Id = new Guid("90000000-0000-0000-0000-000000000001").ToString(), 29 | Name = "Matcher1", 30 | SpaceId = null, 31 | }; 32 | private static Models.Matcher matcher2 = new Models.Matcher() 33 | { 34 | Id = new Guid("90000000-0000-0000-0000-000000000002").ToString(), 35 | Name = "Matcher2", 36 | SpaceId = null, 37 | }; 38 | private static HttpResponseMessage matcher1GetResponse = new HttpResponseMessage() 39 | { 40 | StatusCode = HttpStatusCode.OK, 41 | Content = new StringContent(JsonConvert.SerializeObject(new [] { matcher1 })), 42 | }; 43 | private static HttpResponseMessage matchers1And2GetResponse = new HttpResponseMessage() 44 | { 45 | StatusCode = HttpStatusCode.OK, 46 | Content = new StringContent(JsonConvert.SerializeObject(new [] { matcher1, matcher2 })), 47 | }; 48 | private static Models.UserDefinedFunction udf1 = new Models.UserDefinedFunction() 49 | { 50 | Id = udfGuid1.ToString(), 51 | Name = "User Defined Function 1", 52 | }; 53 | private static HttpResponseMessage udf1GetResponse = new HttpResponseMessage() 54 | { 55 | StatusCode = HttpStatusCode.OK, 56 | Content = new StringContent(JsonConvert.SerializeObject(new [] { udf1 })), 57 | }; 58 | [Fact] 59 | public async Task GetProvisionSampleCreatesDescriptions() 60 | { 61 | var yaml = @" 62 | - name: Test1 63 | userdefinedfunctions: 64 | - name: Function 1 65 | matcherNames: 66 | - matcher 1 67 | - matcher 2 68 | script: some1/path1 69 | - name: Function 2 70 | matcherNames: 71 | - matcher 3 72 | script: some2/path2 73 | "; 74 | var expectedDescriptions = new [] { new SpaceDescription() 75 | { 76 | name = "Test1", 77 | userdefinedfunctions = new [] { 78 | new UserDefinedFunctionDescription() 79 | { 80 | name = "Function 1", 81 | matcherNames = new [] { "matcher 1", "matcher 2" }, 82 | script = "some1/path1", 83 | }, 84 | new UserDefinedFunctionDescription() 85 | { 86 | name = "Function 2", 87 | matcherNames = new [] { "matcher 3" }, 88 | script = "some2/path2", 89 | }, 90 | }, 91 | }}; 92 | var actualDescriptions = await Actions.GetProvisionSampleTopology(new StringReader(yaml)); 93 | Assert.Equal(yamlSerializer.Serialize(expectedDescriptions), yamlSerializer.Serialize(actualDescriptions)); 94 | } 95 | 96 | [Fact] 97 | public async Task CreateTwoUserDefinedFunctions() 98 | { 99 | (var httpClient, var httpHandler) = FakeDigitalTwinsHttpClient.CreateWithSpace( 100 | postResponseGuids: new [] { udfGuid1, udfGuid2 }, 101 | getResponses: new [] { matchers1And2GetResponse, Responses.NotFound, matcher1GetResponse, Responses.NotFound } 102 | ); 103 | 104 | var descriptions = new [] { new SpaceDescription() 105 | { 106 | name = FakeDigitalTwinsHttpClient.Space.Name, 107 | userdefinedfunctions = new [] { 108 | new UserDefinedFunctionDescription() 109 | { 110 | name = "Function 1", 111 | matcherNames = new [] { "Matcher1", "Matcher2" }, 112 | script = "userDefinedFunctions/function1.js", 113 | }, 114 | new UserDefinedFunctionDescription() 115 | { 116 | name = "Function 2", 117 | matcherNames = new [] { "Matcher1" }, 118 | script = "userDefinedFunctions/function2.js", 119 | }}, 120 | }}; 121 | 122 | await Actions.CreateSpaces(httpClient, Loggers.SilentLogger, descriptions, Guid.Empty); 123 | Assert.Equal(2, httpHandler.PostRequests["userdefinedfunctions"].Count); 124 | Assert.Equal(2, httpHandler.GetRequests["matchers"].Count); 125 | Assert.Equal( 126 | "http://bing.com/matchers?names=Matcher1,Matcher2&spaceIds=90000000-0000-0000-0000-000000000001", 127 | httpHandler.GetRequests["matchers"][0].RequestUri.ToString()); 128 | Assert.Equal( 129 | "http://bing.com/matchers?names=Matcher1&spaceIds=90000000-0000-0000-0000-000000000001", 130 | httpHandler.GetRequests["matchers"][1].RequestUri.ToString()); 131 | Assert.Equal(2, httpHandler.GetRequests["userdefinedfunctions"].Count); 132 | } 133 | 134 | [Fact] 135 | public async Task UpdateUserDefinedFunction() 136 | { 137 | (var httpClient, var httpHandler) = FakeDigitalTwinsHttpClient.CreateWithSpace( 138 | postResponseGuids: new [] { udfGuid1, udfGuid2 }, 139 | getResponses: new [] { matcher1GetResponse, udf1GetResponse }, 140 | patchResponses: new [] { Responses.OK } 141 | ); 142 | 143 | var descriptions = new [] { new SpaceDescription() 144 | { 145 | name = FakeDigitalTwinsHttpClient.Space.Name, 146 | userdefinedfunctions = new [] { 147 | new UserDefinedFunctionDescription() 148 | { 149 | name = "Function 1", 150 | matcherNames = new [] { "Matcher1" }, 151 | script = "userDefinedFunctions/function1.js", 152 | }}, 153 | }}; 154 | 155 | await Actions.CreateSpaces(httpClient, Loggers.SilentLogger, descriptions, Guid.Empty); 156 | Assert.False(httpHandler.PostRequests.ContainsKey("userdefinedfunctions")); 157 | Assert.Equal(1, httpHandler.PatchRequests["userdefinedfunctions"].Count); 158 | Assert.Equal(1, httpHandler.GetRequests["matchers"].Count); 159 | Assert.Equal(1, httpHandler.GetRequests["userdefinedfunctions"].Count); 160 | } 161 | } 162 | } -------------------------------------------------------------------------------- /occupancy-quickstart/tests/responses.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System.Net; 5 | using System.Net.Http; 6 | 7 | namespace Microsoft.Azure.DigitalTwins.Samples.Tests 8 | { 9 | public static class Responses 10 | { 11 | public static HttpResponseMessage NotFound = new HttpResponseMessage() 12 | { 13 | StatusCode = HttpStatusCode.NotFound, 14 | }; 15 | 16 | public static HttpResponseMessage OK = new HttpResponseMessage() 17 | { 18 | StatusCode = HttpStatusCode.OK, 19 | }; 20 | } 21 | } -------------------------------------------------------------------------------- /occupancy-quickstart/tests/userDefinedFunctions/function1.js: -------------------------------------------------------------------------------- 1 | function process(sensor, sensorReading) { 2 | log(`Processing function for ${sensor} with reading: ${sensorReading}.`); 3 | } -------------------------------------------------------------------------------- /occupancy-quickstart/tests/userDefinedFunctions/function2.js: -------------------------------------------------------------------------------- 1 | function process(sensor, sensorReading) { 2 | log(`Processing function for ${sensor} with reading: ${sensorReading}.`); 3 | } --------------------------------------------------------------------------------