This commit is contained in:
Vasiya Krishnan
2020-12-18 14:02:06 -08:00
parent 2668836182
commit 46f242adae
316 changed files with 39624 additions and 766 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
![](./media/solutions-microsoft-logo-small.png)
![](./media/solutions-microsoft-logo-small.png)
# Azure Data SQL Samples Repository
This GitHub repository contains code samples that demonstrate how to use Microsoft's Azure Data products including SQL Server, Azure SQL Database, Azure Synapse, and Azure SQL Edge. Each sample includes a README file that explains how to run and use the sample.
@@ -1,139 +0,0 @@
-- Demonstrate WorldWideImporters Polybase connections
-- Requires PolyBase to be installed.
USE WideWorldImportersDW;
GO
-- WideWorldImporters have customers in a variety of cities but feel they are likely missing
-- other important cities. They have decided to try to find other cities have a growth rate of more
-- than 20% over the last 3 years, and where they do not have existing customers.
-- They have obtained census data (a CSV file) and have loaded it into an Azure storage account.
-- They want to combine that data with other data in their main OLTP database to work out where
-- they should try to find new customers.
-- First, let's apply Polybase connectivity and set up an external table to point to the data
-- in the Azure storage account.
EXEC [Application].Configuration_ApplyPolybase;
GO
-- In Object Explorer, refresh the WideWorldImporters database, then expand the Tables node.
-- Note that SQL Server 2016 added a new entry here for External Tables. Expand that node.
-- Expand the dbo.CityPopulationStatistics table, expand the list of columns and note the
-- values that are contained. Let's look at the data:
SELECT CityID, StateProvinceCode, CityName, YearNumber, LatestRecordedPopulation FROM dbo.CityPopulationStatistics;
GO
-- How did that work? First the procedure created an external data source like this:
/*
CREATE EXTERNAL DATA SOURCE AzureStorage
WITH
(
TYPE=HADOOP, LOCATION = 'wasbs://data@sqldwdatasets.blob.core.windows.net'
);
*/
-- This shows how to connect to AzureStorage. Next the procedure created an
-- external file format to describe the layout of the CSV file:
/*
CREATE EXTERNAL FILE FORMAT CommaDelimitedTextFileFormat
WITH
(
FORMAT_TYPE = DELIMITEDTEXT,
FORMAT_OPTIONS
(
FIELD_TERMINATOR = ','
)
);
*/
-- Finally the external table was defined like this:
/*
CREATE EXTERNAL TABLE dbo.CityPopulationStatistics
(
CityID int NOT NULL,
StateProvinceCode nvarchar(5) NOT NULL,
CityName nvarchar(50) NOT NULL,
YearNumber int NOT NULL,
LatestRecordedPopulation bigint NULL
)
WITH
(
LOCATION = '/',
DATA_SOURCE = AzureStorage,
FILE_FORMAT = CommaDelimitedTextFileFormat,
REJECT_TYPE = VALUE,
REJECT_VALUE = 4 -- skipping 1 header row per file
);
*/
-- From that point onwards, the external table can be used like a local table. Let's run that
-- query that they wanted to use to find out which cities they should be finding new customers
-- in. We'll start building the query by grouping the cities from the external table
-- and finding those with more than a 20% growth rate for the period:
WITH PotentialCities
AS
(
SELECT cps.CityName,
cps.StateProvinceCode,
MAX(cps.LatestRecordedPopulation) AS PopulationIn2016,
(MAX(cps.LatestRecordedPopulation) - MIN(cps.LatestRecordedPopulation)) * 100.0
/ MIN(cps.LatestRecordedPopulation) AS GrowthRate
FROM dbo.CityPopulationStatistics AS cps
WHERE cps.LatestRecordedPopulation IS NOT NULL
AND cps.LatestRecordedPopulation <> 0
GROUP BY cps.CityName, cps.StateProvinceCode
)
SELECT CityName, StateProvinceCode, PopulationIn2016, GrowthRate
FROM PotentialCities
WHERE GrowthRate > 2.0;
GO
-- Now let's combine that with our local city and sales data to exclude those where we already
-- have customers. We'll find the 100 most interesting cities based upon population.
WITH PotentialCities
AS
(
SELECT cps.CityName,
cps.StateProvinceCode,
MAX(cps.LatestRecordedPopulation) AS PopulationIn2016,
(MAX(cps.LatestRecordedPopulation) - MIN(cps.LatestRecordedPopulation)) * 100.0
/ MIN(cps.LatestRecordedPopulation) AS GrowthRate
FROM dbo.CityPopulationStatistics AS cps
WHERE cps.LatestRecordedPopulation IS NOT NULL
AND cps.LatestRecordedPopulation <> 0
GROUP BY cps.CityName, cps.StateProvinceCode
),
InterestingCities
AS
(
SELECT DISTINCT pc.CityName,
pc.StateProvinceCode,
pc.PopulationIn2016,
FLOOR(pc.GrowthRate) AS GrowthRate
FROM PotentialCities AS pc
INNER JOIN Dimension.City AS c
ON pc.CityName = c.City
WHERE GrowthRate > 2.0
AND NOT EXISTS (SELECT 1 FROM Fact.Sale AS s WHERE s.[City Key] = c.[City Key])
)
SELECT TOP(100) CityName, StateProvinceCode, PopulationIn2016, GrowthRate
FROM InterestingCities
ORDER BY PopulationIn2016 DESC;
GO
-- Clean up if required
/*
DROP EXTERNAL TABLE dbo.CityPopulationStatistics;
GO
DROP EXTERNAL FILE FORMAT CommaDelimitedTextFileFormat;
GO
DROP EXTERNAL DATA SOURCE AzureStorage;
GO
*/
@@ -1,75 +0,0 @@
# Sample Querying of External Data Source in WideWorldImportersDW
This script demonstrates the use of PolyBase to query an external data source.
Demographics data is available in Azure blob storage. This data is joined with sales data recorded in the local database to determine which would be good candidates for future expansion of the business.
### Contents
[About this sample](#about-this-sample)<br/>
[Before you begin](#before-you-begin)<br/>
[Running the sample](#run-this-sample)<br/>
[Sample details](#sample-details)<br/>
[Disclaimers](#disclaimers)<br/>
[Related links](#related-links)<br/>
<a name=about-this-sample></a>
## About this sample
<!-- Delete the ones that don't apply -->
1. **Applies to:** SQL Server 2016 (or higher), Azure SQL Database
1. **Key features:** PolyBase
1. **Workload:** Analytics
1. **Programming Language:** T-SQL
1. **Authors:** Greg Low, Jos de Bruijn
1. **Update history:** 26 May 2016 - initial revision
<a name=before-you-begin></a>
## Before you begin
To run this sample, you need the following prerequisites.
**Software prerequisites:**
<!-- Examples -->
1. SQL Server 2016 (or higher) with PolyBase, connected to the internet.
2. SQL Server Management Studio
3. The WideWorldImportersDW database (Full version).
<a name=run-this-sample></a>
## Running the sample
1. Execute the sample script.
2. Inspect external tables in the database.
3. Review query results.
## Sample details
The sample script performs a configuration and runs three queries:
1. An external table `dbo.CitePopulationStatistics` is created in the database, pointing to a data set in Azure blob storage.
2. The data in Azure storage is queried through Transact-SQL, showing all the data in the data source.
3. Cities with a significant growth rate (>= 20%) are identified.
4. Top cities for potential expansion are identified based on external data as well as sales data in the local database.
<a name=disclaimers></a>
## Disclaimers
The code included in this sample is not intended to be used for production purposes.
<a name=related-links></a>
## Related Links
<!-- Links to more articles. Remember to delete "en-us" from the link path. -->
For more information, see these articles:
- [Get started with PolyBase](https://msdn.microsoft.com/library/mt163689.aspx)
- [PolyBase: Gaining insights from HDFS and relational data in SQL Server 2016 (video)](https://channel9.msdn.com/Events/DataDriven/SQLServer2016/PolyBase)
@@ -0,0 +1,10 @@
* text eol=lf
*.gif -text
*.jpg -text
*.png -text
*.dll -text
*.exe -text
*.nupkg -text
ml/data/*.parquet binary
*.zip binary
*.dacpac binary
@@ -0,0 +1,447 @@
.DS_Store
temp/
# Created by https://www.gitignore.io/api/node,visualstudio,visualstudiocode
# Edit at https://www.gitignore.io/?templates=node,visualstudio,visualstudiocode
### Node ###
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# TypeScript v1 declaration files
typings/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env.test
# parcel-bundler cache (https://parceljs.org/)
.cache
# next.js build output
.next
# nuxt.js build output
.nuxt
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
### VisualStudioCode ###
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
### VisualStudioCode Patch ###
# Ignore all local history of files
.history
### VisualStudio ###
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
##
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
# User-specific files
*.rsuser
*.suo
*.user
*.userosscache
*.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Mono auto generated files
mono_crash.*
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/
# Visual Studio 2015/2017 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
# Visual Studio 2017 auto generated files
Generated\ Files/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUNIT
*.VisualState.xml
TestResult.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
# Benchmark Results
BenchmarkDotNet.Artifacts/
# .NET Core
project.lock.json
project.fragment.lock.json
artifacts/
# StyleCop
StyleCopReport.xml
# Files built by Visual Studio
*_i.c
*_p.c
*_h.h
*.ilk
*.meta
*.obj
*.iobj
*.pch
*.pdb
*.ipdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*_wpftmp.csproj
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb
# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap
# Visual Studio Trace Files
*.e2e
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# JustCode is a .NET coding add-in
.JustCode
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# AxoCover is a Code Coverage Tool
.axoCover/*
!.axoCover/settings.json
# Visual Studio code coverage results
*.coverage
*.coveragexml
# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# Note: Comment the next line if you want to checkin your web deploy settings,
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj
# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these scripts will be unencrypted
PublishScripts/
# NuGet Packages
*.nupkg
# The packages folder can be ignored because of Package Restore
**/[Pp]ackages/*
# except build/, which is used as an MSBuild target.
!**/[Pp]ackages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/[Pp]ackages/repositories.config
# NuGet v3's project.json files produces more ignorable files
*.nuget.props
*.nuget.targets
# Microsoft Azure Build Output
csx/
*.build.csdef
# Microsoft Azure Emulator
ecf/
rcf/
# Windows Store app package directories and files
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
*.appx
*.appxbundle
*.appxupload
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!?*.[Cc]ache/
# Others
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.jfm
*.pfx
*.publishsettings
orleans.codegen.cs
# Including strong name files can present a security risk
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
#*.snk
# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
ServiceFabricBackup/
*.rptproj.bak
# SQL Server files
*.mdf
*.ldf
*.ndf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
*.rptproj.rsuser
*- Backup*.rdl
# Microsoft Fakes
FakesAssemblies/
# GhostDoc plugin setting file
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions
# Paket dependency manager
.paket/paket.exe
paket-files/
# FAKE - F# Make
.fake/
# CodeRush personal settings
.cr/personal
# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc
# Cake - Uncomment if you are using it
# tools/**
# !tools/packages.config
# Tabs Studio
*.tss
# Telerik's JustMock configuration file
*.jmconfig
# BizTalk build output
*.btp.cs
*.btm.cs
*.odx.cs
*.xsd.cs
# OpenCover UI analysis results
OpenCover/
# Azure Stream Analytics local run output
ASALocalRun/
# MSBuild Binary and Structured Log
*.binlog
# NVidia Nsight GPU debugger configuration file
*.nvuser
# MFractors (Xamarin productivity tool) working folder
.mfractor/
# Local History for Visual Studio
.localhistory/
# BeatPulse healthcheck temp database
healthchecksdb
# Backup folder for Package Reference Convert tool in Visual Studio 2017
MigrationBackup/
# End of https://www.gitignore.io/api/node,visualstudio,visualstudiocode
@@ -0,0 +1,475 @@
# SQL DB Edge Demo
## Overview
The SQL DB Edge demo is based on a Contoso Renewable Energy, a wind turbine farm that leverages SQL DB Edge for data processing onboard the generator.
The demo will walk you through resolving an alert being raised due to wind turbulence being detected at the device. You will train a model and deploy it to SQL DB Edge that will correct the detected wind wake and ultimately optimize power output.
We will also look at some of the security features available with SQL DB Edge.
## Wind Turbine Data Explanation for the Wake Detection model
The data stored in the database table represents the following:
* **RecordId:** _Unique identifier for the entry._
* **TurbineId:** _Unique identifier for the turbine in scope._
* **GearboxOilLevel:** _Oil level recorded for the turbine gear box at the time of the reading._
* **GearboxOilTemp:** _Oil temperature recorded for the turbine gear box at the time of the reading._
* **GeneratorActivePower:** _Active Power recorded by the turbine generator._
* **GeneratorSpeed:** _Speed recorded by the turbine generator._
* **GeneratorTemp:** _Temperature recorded by the turbine generator._
* **GeneratorTorque:** _Torque recorded by the turbine generator._
* **GridFrequency:** _Frequency recorded in the grid for the specific wind turbine._
* **GridVoltage:** _Voltage recorded in the grid for the specific wind turbine._
* **HydraulicOilPressure:** _Current pressure of the hydraulic oil for the wind turbine._
* **NacelleAngle:** _Angle of the nacelle at the time of the reading (the housing that contains all the generating components)._
* **PitchAngle:** _Pitch angle of the blades against the oncoming air stream to obtain the optimal amount of energy._
* **Vibration:** _Vibration of the wind turbine at the time of the reading._
* **WindSpeedAverage:** _Average wind speed calculated from the last X records._
* **Precipitation:** _Flag to represent if rain was present at the time of the reading._
* **WindTempAverage:** _Average wind temperature calculated from the last X records._
* **OverallWindDirection:** _Overall wind direction recorded at the time of the reading._
* **TurbineWindDirection:** _Turbine wind direction recorded at the time of the reading._
* **TurbineSpeedAverage:** _Average turbine speed calculated from the last X records._
* **WindSpeedStdDev:** _Standard Deviation of the last X WindSpeedAverage records._
* **TurbineSpeedStdDev:** _Standard Deviation of the last X TurbineSpeedAverage records._
The above dataset definition contains trends that will enable us to detect the existence of wake in a wind turbine. There are two main conditions that influence the presence of wind wake:
1. Overall wind farm and turbine wind direction are both between 40° - 45° degrees.
1. TurbineSpeedStdDev and WindSpeedStdDev have been too far apart for greater than a minute.
The wind turbine will experience wake when the turbine wind direction is between 40° - 45° degrees and the values of TurbineSpeedStdDev and WindSpeedStdDev are not similar. For example:
* Wake Present:
* TurbineWindDirection = 43.5°
* TurbineSpeedStdDev = 8.231
* WindSpeedStdDev = 0.23
* Wake Not Present:
* TurbineWindDirection = 23.5°
* TurbineSpeedStdDev = 0.921
* WindSpeedStdDev = 0.213
## Clone the SQL DB Edge Demo Repository
Git will be used to copy all the files for the demo to your local computer.
1. Install Git from [here](https://git-scm.com/download)
1. Open a command prompt and navigate to a folder where the repo should be downloaded<br>
1. Issue the command `https://github.com/SQLSourabh/DemoFiles.git`
## Azure Resource Deployment
An Azure Resource Manager (ARM) template will be used to deploy all the required resources in the solution. Click on the link below to start the deployment.
[![homepage](https://raw.githubusercontent.com/Azure/azure-quickstart-templates/master/1-CONTRIBUTION-GUIDE/images/deploytoazure.png)](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fzarmada.blob.core.windows.net%2Farm-deployments-public%2Farm-template-dbedge.json "Deploy template")
TODO ^^ Need to update the ARM location
### Deployment of resources
Follow the steps to deploy the required Azure resources:
**BASICS**
- **Subscription**: Select the Subscription.
- **Resource group**: Click on 'Create new' and provide a unique name for the Resource Group
- **Location**: Select the Region where to deploy the resources. Keep in mind that all resources will be deployed to this region so make sure it supports all of the required services. The template has been confirmed to work in West US 2.
1. Read and accept the `TERMS AND CONDITIONS` by checking the box.
1. Click the `Purchase` button and wait for the deployment to finish.
## Post Deployment Configuration
Some resources require some extra configuration.
#### Upload SQL DACPAC
The Edge Module will require access the DACPAC package in order to setup the database.
1. In the [Azure portal](https://portal.azure.com/) select the **Resource Group** you created earlier.
1. Select the **Storage account** resource from the list.
1. Click the **Containers** option in the left menu under **Blob service**.
1. Click the **dacpac** container.
1. Click the **Upload** button.
1. Click the **Select a file** input and select the file under the project folder: `sql/turbine-sensor-db-dacpac.zip`.
1. Click the **Upload** button.
1. Once the file is uploaded, click on it.
1. Click **Generate SAS** tab.
1. Update the **Expiry** year to 2050.
1. Click **Generate SAS token and URL**
1. Copy the value in **Blob SAS URL** and save it for later in the setup.
##### SQL Security Setup Information
As security settings were deployed as part of the DACPAC package, below is a **review** of the security setup within the database.
1. Create users without a login for simpler testing:
```sql
/* Create users using the logins created */
CREATE USER OperatorUser WITHOUT LOGIN;
CREATE USER DataScientistUser WITHOUT LOGIN;
CREATE USER SecurityUser WITHOUT LOGIN;
CREATE USER TurbineUser WITHOUT LOGIN;
```
1. Assigned permissions for each user:
```sql
/* Grant permissions to users */
GRANT SELECT ON RealtimeSensorRecord TO OperatorUser;
GRANT SELECT ON RealtimeSensorRecord TO DataScientistUser;
GRANT SELECT ON RealtimeSensorRecord TO SecurityUser;
GRANT SELECT, INSERT ON RealtimeSensorRecord TO TurbineUser;
```
> **Note**: All users can SELECT, however the TurbineUser can also INSERT to the table.
1. For privacy reasons, mask the last 4 digits of the SensorId for the Data Scientist user:
```sql
/*Mask the last four digits of the serial number (Sensor ID) for the Data Scientist User*/
ALTER TABLE RealtimeSensorRecord
ALTER COLUMN SensorId varchar(50) MASKED WITH (FUNCTION = 'partial(34,"XXXX",0)');
DENY UNMASK TO DataScientistUser;
GO
```
1. Add a policy using a filter predicate and a function to manage access to data events:
* We updated the SensorType column as it is required in our function then created a new schema to store it.
```sql
/**
* Operator: Can see all events
* Data Scientist: Can see everything BUT Hatch Sensor events
* Security: Can ONLY see Hatch Sensor events
*/
ALTER TABLE RealtimeSensorRecord
ALTER COLUMN SensorType sysname
GO
CREATE SCHEMA Security;
GO
```
* Add the function that will ensure each query is authorized based on Sensor Type/User.
```sql
/**
* Operator: Can see all events
* Data Scientist: Can see everything BUT Hatch Sensor events
* Security: Can ONLY see Hatch Sensor events
*/
CREATE FUNCTION Security.fn_securitypredicate(@SensorType AS sysname)
RETURNS TABLE
WITH SCHEMABINDING
AS
RETURN SELECT 1 AS fn_securitypredicate_result
WHERE
USER_NAME() = 'OperatorUser' OR USER_NAME() = 'dbo' OR
(USER_NAME() = 'DataScientistUser' AND @SensorType <> 'HatchSensor') OR
(USER_NAME() = 'SecurityUser' AND @SensorType = 'HatchSensor');
```
* Add a filter to to use the function.
```sql
CREATE SECURITY POLICY SensorsDataFilter
ADD FILTER PREDICATE Security.fn_securitypredicate(SensorType)
ON dbo.RealtimeSensorRecord
WITH (STATE = ON);
```
#### Notebook Setup
In this section, we will setup our notebook with the required files for the generation of the wind adapt model.
##### Upload training data file:
1. In the [Azure portal](https://portal.azure.com/) select the **Resource Group** you created earlier.
1. Select the **Storage account** resource.
1. Click the **Containers** option in the left menu under **Blob service**.
1. Click the **azureml-blobstore-GUID** container.
1. Click the **Upload** button in the top.
1. Click the **Select a file** input and select the `ml\data\TrainingDataset.parquet` from your repo.
1. Click the **Upload** button and wait for the upload to finish.
##### Notebook files upload
1. Select **Azure Active Directory** option from the main navigation in the Azure Portal:
![Azure Active Directory Option](./images/azure-active-directory-option.png)
1. Copy the **Tenant Id** value from the overview as you will need this value later.
1. Go back to the **Resource Group** you created earlier.
1. Select the **Machine Learning** resource.
1. Take note of the following values to be used later in the deployment
* Resource Group
* Workspace Name
* Subscription ID
![Machine Learning Resource](./images/machine-learning-resource.png)
1. Click the **Launch now** button to open the Machine Learning workspace.
1. Click the **Notebooks** option in the left menu under **Author**.
1. Click the **Create new folder** button at the top of the navigation panel.
1. Enter the name `scripts` for as the folder name and click the **Create** button.
1. Click the **Upload files** button at the top of the navigation panel.
1. Select the 2 files inside the `ml\scripts` folder:
* ml\scripts\train.py
* ml\scripts\utils.py
1. Select the newly created `scripts` folder from the target directory list.
1. Click the **Upload** button and wait for the upload to finish.
1. Click the **Upload files** button again.
1. Select the following 2 files inside the `ml` folder:
* ml\utils.py
* ml\wind-turbine-scikit.ipynb
> **Note**: The `utils.py` is a different file from the previous step.
1. Select your username folder from the target directory list.
1. Click the **Upload** button.
##### Notebook configuration
We need configure values within the notebook before being able to execute it:
1. Click the `wind-turbine-scikit.ipynb` in the **My files** navigation:
1. Click the **New Compute** button.
1. Enter the name `compute-{your-initials}`.
1. Select **CPU (Central Processing Unit)** from the **Virtual machine type** dropdown.
1. Select the virtual machine size **Standard_D12_v2**.
1. Click the **Create** button and wait for the compute to be created.
> **Note**: This process can take several minutes; wait until status of **compute** is `Running`.
1. Click the **Edit** dropdown and select the **Edit in Jupyter** option.
> **Note**: If required, login with your Azure credentials.
1. Replace the values within the **Setup Azure ML** cell with the values you obtained in the **Notebook files upload** section:
```
interactive_auth = InteractiveLoginAuthentication(tenant_id="<tenant_id>")
# Get instance of the Workspace and write it to config file
ws = Workspace(
subscription_id = '<subscription_id>',
resource_group = '<resource_group>',
workspace_name = '<workspace_name>',
auth = interactive_auth)
```
1. Click **File** > **Save and Checkpoint** from the menu.
1. Select the **Install requirements** cell and click **Run** from the menu, wait for the script to execute before continuing.
1. Select the **Setup Azure ML** cell and click **Run** from the menu.
> **IMPORTANT**: Observe the output to **authenticate** via the URL provided (https://microsoft.com/devicelogin).
1. From here, **Run** the remaining cells sequentially until you have executed the notebook.
> **IMPORTANT**: Remember to wait for each cell to execute before continuing.
1. Go back to the azure resource group and click the **Storage Account** resource.
1. Click the **Containers** option in the left menu.
1. Click the container in the list with a name like: `azureml-blobstore-{guid}`.
1. A new file with the name `windturbinewake.model.onnx` will be in the container.
1. Click the `windturbinewake.model.onnx` file
1. Click the **Generate SAS** tab option.
1. Change the **Expiry** Year to 2050.
1. Click the **Generate SAS token and URL** button and wait for the SAS to be generated.
1. Copy the **Blob SAS URL** value for later in the demo usage section.
> **IMPORTANT**: As this process does take some time, once you have saved your model to blob storage, you will not be required to execute this every time you run through the demo. Showing the notebook flow may be adequate for demo purposes. You will just need the blob SAS for the **SQL DB Edge Demo Usage** section later in the document.
#### Device Setup
In this section, we will set up an Edge device within our IoT Hub instance.
##### Create a new Edge device
1. In the [Azure portal](https://portal.azure.com/) select the **Resource Group** you created earlier.
1. Select the **IoT Hub** resource.
1. Click on **IoT Edge** from the left navigation.
1. Click **+ Add an IoT Edge Device**.
1. Enter a **Device ID** and leave all other fields as default.
1. Click **Save**.
1. Once the device has been created, select the device and copy the **Primary Connection String** for later in this setup.
##### Setup Edge device as a VM
1. Click on the link below to start the deploy to Azure:
[![homepage](https://raw.githubusercontent.com/Azure/azure-quickstart-templates/master/1-CONTRIBUTION-GUIDE/images/deploytoazure.png)](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2Fazure%2Fiotedge-vm-deploy%2Fmaster%2FedgeDeploy.json "Deploy device")
1. On the newly launched window, fill in the available form fields:
* **Subscription**: Your subscription.
* **Resource group**: Select the resource group you created earlier.
* **DNS Label Prefix**: Your initials and birth year.
* **Admin Username**: Enter `microsoft` as default.
* **Device Connection String**: The device connection string that you got from previous section.
* **VM Size**: The size of the virtual machine to be deployed.
* **Ubuntu OS Version**: The version of the Ubuntu OS to be installed on the base virtual machine.
* **Location**: The geographic region to deploy the virtual machine into, this value defaults to the location of the selected Resource Group.
* **Authentication Type**: Choose the **password** option.
* **Admin Password or Key**: Enter `M1cr0s0ft2020`.
1. Accept the **Terms and Conditions**.
1. Select **Purchase** to begin the deployment.
##### SSH into the VM - Optional
1. Once the deployment is complete, go back to the **Resource Group** you created earlier.
1. Select the **Virtual Machine** resource.
> **Note**: Take note of the machine name, this should be in the format vm-0000000000000. Also, take note of the associated DNS Name, which should be in the format `<dnsLabelPrefix>.<location>.cloudapp.azure.com`.
The DNS Name can be obtained from the Overview section of the newly deployed virtual machine within the Azure portal.
![VM DNS Name](./images/iotedge-vm-dns-name.png)
1. If you want to SSH into this VM after setup, use the associated DNS Name with the command: `ssh <adminUsername>@<DNS_Name>`. You can use the password you created in the previous step.
> **IMPORTANT**: There is an optional section at the end of this document showing some example commands.
##### Setup Visual Studio Code Development Environment
1. Install [Visual Studio Code](https://code.visualstudio.com/Download) (VS Code).
1. Install [Docker Community Edition (CE)](https://docs.docker.com/install/#supported-platforms). Don't sign in to Docker Desktop after Docker CE is installed.
1. Install the following extensions for VS Code:
* [Azure Machine Learning](https://marketplace.visualstudio.com/items?itemName=ms-toolsai.vscode-ai) ([Azure Account](https://marketplace.visualstudio.com/items?itemName=ms-vscode.azure-account) will be automatically installed)
* [Azure IoT Hub Toolkit](https://marketplace.visualstudio.com/items?itemName=vsciot-vscode.azure-iot-toolkit)
* [Azure IoT Edge](https://marketplace.visualstudio.com/items?itemName=vsciot-vscode.azure-iot-edge)
* [Docker Extension](https://marketplace.visualstudio.com/items?itemName=ms-azuretools.vscode-docker)
1. Restart VS Code.
1. Select **[View > Command Palette…]** to open the command palette box, then enter **[Python: Select Interpreter]** command in the command palette box to select your Python interpreter.
1. Enter **[Azure: Sign In]** command in the command palette box to sign in Azure account and select your subscription.
##### Build and deploy container image to device
1. Launch Visual Studio Code, and select File > Open Workspace... command to open the `edge\sensor-solution.code-workspace`.
1. Update the .env file with the values for your container registry.
- In the [Azure portal](https://portal.azure.com/) select the **Resource Group** you created earlier.
- Select the **Container Registry** resource.
- Select **Access Keys** from the left navigation.
- Update the following in `edge/SensorSolution/.env` with the following values from **Access Keys** within the Container Registry:
CONTAINER_REGISTRY_NAME=`<Login Server>` (Ensure this is the login server and NOT the Registry Name)
CONTAINER_REGISTRY_USER_NAME=`<Username>`
CONTAINER_REGISTRY_PASSWORD=`<Password>`
SQL_PACKAGE=`<SQL Package Blob URL>` (the one you obtained earlier in the setup)
- Save the file.
1. Sign in to your Azure Container Registry by entering the following command in the Visual Studio Code integrated terminal (replace <REGISTRY_USER_NAME>, <REGISTRY_PASSWORD>, and <REGISTRY_NAME> with your container registry values set in the .env file IN THE PREVIOUS STEP).
`docker login -u <CONTAINER_REGISTRY_USER_NAME> -p <CONTAINER_REGISTRY_PASSWORD> <CONTAINER_REGISTRY_NAME>`
> **IMPORTANT**: Ensure you have `amd64` selected as the architecture in the bottom navigation bar of VS Code.
1. Right-click on `edge/SensorSolution/deployment.debug.template.json` and select the **Build and Push IoT Edge Solution** command to generate a new `deployment.debug.amd64.json` file in the config folder, build a module image, and push the image to the specified ACR repository.
> **IMPORTANT:** If you have amended code in your module, you will need to increment the version number in `module.json` so the new version will get deployed to the device in the next steps.
> **Note**: Some red warnings "/usr/bin/find: '/proc/XXX': No such file or directory" and "debconf: delaying package configuration, since apt-utils is not installed" displayed during the building process can be ignored.
1. Ensure you have the correct Iot Hub selected in VS Code.
- In the Azure IoT Hub extension, click **Select IoT Hub** from the hamburger menu. (Alternatively, select `Azure IoT Hub: Select IoT Hub` from the **Command Palette**)
- Select your **Subscription**.
- Select the **IoT Hub** you created earlier in the setup.
1. Right-click `config\deployment.debug.amd64.json` and select **Create Deployment for a Single Device**.
1. Select the device you created earlier.
1. Wait for deployment to be completed.
#### Web App Settings
Follow the next steps to setup the required module twin connection string property.
1. In the [Azure portal](https://portal.azure.com/) select the **Resource Group** you created earlier.
1. Select the **IoT Hub** resource.
1. Click the **IoT Edge** option in the left menu under **Automatic Device Management**.
1. Click the **device** you created earlier.
1. Click the **SensorModule** from the modules list.
1. Copy the **Connection string (primary key)** value and save for the next step.
1. Go back to your **Resource Group**.
1. Select the **App Service** resource.
1. Click the **Configuration** option in the left menu.
1. Under the **Application settings** find the `IoTHub:ModuleConnectionString` and click it.
1. Paste the module connection string that you got before to the `value` input field.
1. Click the **OK** button.
1. Click the **Save** button on the top to apply the change.
## SQL DB Edge Demo Usage
Open the Web App.
1. In the [Azure portal](https://portal.azure.com/) select the **Resource Group** you created earlier.
1. Select the **App Service** resource.
1. Click **Browse** to go the application on a desktop machine.
Investigate Turbine Issue
1. Click **view** on the alert. A query is ran against the SQL DB Edge instance.
1. Notice the Operator can't see the Security Alert due to as the permissions we set earlier.
1. You can notice a drop in the **Power Generated** chart.
1. Click the **Environmental** button.
1. You can notice the **Wind Speed and Direction** at the turbine is a lot more turbulent than the rest of the Wind Farm. This could indicate wind wake.
Now we need to run our notebook in order to generate the Onnx model that we will use to resolve the alert.
>**Important**: As mentioned earlier in the document, you can choose to run through executing the notebook cells in the `Notebook Setup` section again to obtain the model. Or you can use your Blob URL you created during the initial setup.
Now we have our wind adapt model, lets update the module to correct the turbine.
1. Go back to the azure resource group and click the **IoT Hub** resource.
1. Click the **Iot Edge** option in the left menu.
1. Click the created device from previous steps.
1. Click the **SensorModule** from the modules list.
1. Click the **Module Identity Twin** option in the top menu.
1. Find the `properties` section in the json.
1. Find the `desired` section in the json.
1. Find the `OnnxModelUrl` property and update the value with the model Blob URL from the previous section.
1. Click the **Save** button.
1. Go back to the **Web App**.
1. You will notice a notification indicating the alert has been resolved.
1. Click on the **Resfresh** button.
1. Notice the turbine **Wind Speed and Direction** has stabilized.
1. If you go back to the dashboard view. You will notice Unit 34 no longer has an alert.
#### Restart the demo
This steps allows you to restart the demo.
1. Go back to the azure resource group and click the **IoT Hub** resource.
1. Click the **Iot Edge** option in the left menu.
1. Click the created device from previous steps.
1. Click the **SensorModule** from the modules list.
1. Click the **Module Identity Twin** option in the top menu.
1. Find the `properties` section in the json.
1. Find the `desired` section in the json.
1. Find the `OnnxModelUrl` property set the value as empty.
> **Note**: Since the `Alert` property value was already in `start` we don't need to updated it but the module will set the reported property with this value.
1. Click the **Save** button.
1. Go back to the **Web App** and refresh.
1. After a short time the alert will appear again.
# Optional Steps
This section describe steps that allow us to see extra features of the resources as a reference only.
## Device VM access
Here we will see how to run commands into to the device virtual machine from the terminal using SSH connection.
1. In the [Azure portal](https://portal.azure.com/) select the **Resource Group** you created earlier.
2. Select the **Virtual machine** resource.
3. Copy the **DNS name** to use it for the connection.
4. In a terminal run the following command replacing the **DNS name**:` ssh microsoft@<DNS_Name>`
> **Note**: The above command is assuming that you use the default Admin username when deploying the VM.
5. Enter the password to connect.
> **Note**: Default password is: `M1cr0s0ft2020`
6. Run the following command to see the list of modules running: `sudo iotedge list`
> **Note**: You can should be able to see the `AzureSQLDatabaseEdge` and `SensorModule` we deployed earlier.
7. Run the following command to see the logs of the **Sensor Module**: `sudo iotedge logs SensorModule`
* Following we will connect with the edge sql server to run a simple query by doing:
* Get the list of containers running with docker: `sudo docker container list`.
* Get the container ID of the `AzureSQLDatabaseEdge` docker image running.
* Connect to the container using the id that you got and the command: `sudo docker exec -it CONTAINERID /bin/sh`.
* Connect to the container using: `/opt/mssql-tools/bin/sqlcmd -S localhost -U SA -P 'Microsoft2020$'`.
* Connect to the database using: `USE [turbine-sensor-db]` and then `go`.
* Query the number of records in the table using: `select count(*) from RealtimeSensorRecord` and `go`.
# Troubleshooting
### Error when deploying ARM Template
We've seen issues with different subscription types: MSDN, AIRS, etc... not being able to deploy certain resources to certain regions. We've found that deploying to West US 2 works consistently. If you have a deployment error, try deploying to West US 2. The resources inherit their deployment region from the Resource Group location.
@@ -0,0 +1,383 @@
{
"$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"parameters": {
},
"variables": {
"prefix": "sqldbedge",
"uniquePrefix": "[concat(variables('prefix'), substring(uniqueString(resourceGroup().id), 0, 6))]",
"registry": {
"name": "[concat(variables('uniquePrefix'), 'registry')]",
"id": "[concat(resourceGroup().id,'/providers/','Microsoft.Storage/storageAccounts/', concat(variables('uniquePrefix'), 'registry'))]",
"vault": {
"name": "[concat(variables('uniquePrefix'), 'vault')]",
"id": "[concat(resourceGroup().id,'/providers/','Microsoft.Storage/storageAccounts/', concat(variables('uniquePrefix'), 'vault'))]"
}
},
"storage": {
"name": "[concat(variables('uniquePrefix'), 'storage')]",
"id": "[concat(resourceGroup().id,'/providers/','Microsoft.Storage/storageAccounts/', concat(variables('uniquePrefix'), 'storage'))]"
},
"ml": {
"name": "[concat(variables('uniquePrefix'), 'ml')]",
"id": "[concat(resourceGroup().id,'/providers/','Microsoft.Storage/storageAccounts/', concat(variables('uniquePrefix'), 'ml'))]"
},
"insights": {
"component": {
"name": "[concat(variables('uniquePrefix'), 'insightscomp')]",
"id": "[concat(resourceGroup().id,'/providers/','Microsoft.Storage/storageAccounts/', concat(variables('uniquePrefix'), 'insightscomp'))]"
}
},
"sqldbedgehub": {
"name": "[concat(variables('uniquePrefix'), 'sqldbedgehub')]",
"id": "[concat(resourceGroup().id,'/providers/','Microsoft.Storage/storageAccounts/', concat(variables('uniquePrefix'), 'sqldbedgehub'))]"
},
"website": {
"name": "[concat(variables('uniquePrefix'), 'app')]",
"serverfarms": {
"name": "[concat(variables('uniquePrefix'), 'serverfarms')]"
}
}
},
"resources": [
{
"type": "Microsoft.ContainerRegistry/registries",
"apiVersion": "2019-12-01-preview",
"name": "[variables('registry').name]",
"location": "[resourceGroup().location]",
"sku": {
"name": "Basic",
"tier": "Basic"
},
"properties": {
"adminUserEnabled": true,
"policies": {
"quarantinePolicy": {
"status": "disabled"
},
"trustPolicy": {
"type": "Notary",
"status": "disabled"
},
"retentionPolicy": {
"days": 7,
"status": "disabled"
}
},
"encryption": {
"status": "disabled"
},
"dataEndpointEnabled": false
}
},
{
"type": "microsoft.insights/components",
"apiVersion": "2015-05-01",
"name": "[variables('insights').component.name]",
"location": "[resourceGroup().location]",
"kind": "web",
"properties": {
"Application_Type": "web"
}
},
{
"type": "Microsoft.Storage/storageAccounts",
"apiVersion": "2019-06-01",
"name": "[variables('storage').name]",
"location": "[resourceGroup().location]",
"sku": {
"name": "Standard_LRS",
"tier": "Standard"
},
"kind": "StorageV2",
"properties": {
"networkAcls": {
"bypass": "AzureServices",
"virtualNetworkRules": [],
"ipRules": [],
"defaultAction": "Allow"
},
"supportsHttpsTrafficOnly": false,
"encryption": {
"services": {
"file": {
"keyType": "Account",
"enabled": true
},
"blob": {
"keyType": "Account",
"enabled": true
}
},
"keySource": "Microsoft.Storage"
}
}
},
{
"type": "Microsoft.Storage/storageAccounts/blobServices",
"apiVersion": "2019-06-01",
"name": "[concat(variables('storage').name, '/default')]",
"dependsOn": [
"[resourceId('Microsoft.Storage/storageAccounts', variables('storage').name)]"
],
"sku": {
"name": "Standard_RAGRS",
"tier": "Standard"
},
"properties": {
"cors": {
"corsRules": []
},
"deleteRetentionPolicy": {
"enabled": false
}
}
},
{
"type": "Microsoft.Storage/storageAccounts/fileServices",
"apiVersion": "2019-06-01",
"name": "[concat(variables('storage').name, '/default')]",
"dependsOn": [
"[resourceId('Microsoft.Storage/storageAccounts', variables('storage').name)]"
],
"sku": {
"name": "Standard_RAGRS",
"tier": "Standard"
},
"properties": {
"cors": {
"corsRules": []
}
}
},
{
"type": "Microsoft.Storage/storageAccounts/blobServices/containers",
"apiVersion": "2019-06-01",
"name": "[concat(variables('storage').name, '/default/dacpac')]",
"dependsOn": [
"[resourceId('Microsoft.Storage/storageAccounts/blobServices', variables('storage').name, 'default')]",
"[resourceId('Microsoft.Storage/storageAccounts', variables('storage').name)]"
],
"properties": {
"publicAccess": "None"
}
},
{
"type": "Microsoft.KeyVault/vaults",
"apiVersion": "2018-02-14",
"name": "[variables('registry').vault.name]",
"location": "[resourceGroup().location]",
"properties": {
"tenantId": "[subscription().tenantId]",
"enabledForDeployment": true,
"enabledForDiskEncryption": true,
"enabledForTemplateDeployment": true,
"sku": {
"name": "standard",
"family": "A"
},
"networkAcls": {
"defaultAction": "Allow",
"bypass": "AzureServices"
},
"accessPolicies": []
}
},
{
"type": "Microsoft.MachineLearningServices/workspaces",
"apiVersion": "2018-11-19",
"name": "[variables('ml').name]",
"location": "[resourceGroup().location]",
"dependsOn": [
"[resourceId('Microsoft.Storage/storageAccounts', variables('storage').name)]",
"[resourceId('Microsoft.ContainerRegistry/registries', variables('registry').name)]",
"[resourceId('Microsoft.KeyVault/vaults', variables('registry').vault.name)]",
"[resourceId('microsoft.insights/components', variables('insights').component.name)]"
],
"sku": {
"name": "Basic",
"tier": "Basic"
},
"identity": {
"type": "SystemAssigned"
},
"properties": {
"storageAccount": "[resourceId('Microsoft.Storage/storageAccounts', variables('storage').name)]",
"containerRegistry": "[resourceId('Microsoft.ContainerRegistry/registries', variables('registry').name)]",
"keyVault": "[resourceId('Microsoft.KeyVault/vaults', variables('registry').vault.name)]",
"applicationInsights": "[resourceId('microsoft.insights/components', variables('insights').component.name)]",
"discoveryUrl": "[concat('https://', resourceGroup().location, '.experiments.azureml.net/discovery')]"
}
},
{
"type": "Microsoft.Devices/IotHubs",
"apiVersion": "2019-11-04",
"name": "[variables('sqldbedgehub').name]",
"location": "[resourceGroup().location]",
"sku": {
"name": "S1",
"tier": "Standard",
"capacity": 1
},
"properties": {
"ipFilterRules": [],
"eventHubEndpoints": {
"events": {
"retentionTimeInDays": 1,
"partitionCount": 4
}
},
"routing": {
"endpoints": {
"serviceBusQueues": [],
"serviceBusTopics": [],
"eventHubs": [],
"storageContainers": []
},
"routes": [],
"fallbackRoute": {
"name": "$fallback",
"source": "DeviceMessages",
"condition": "true",
"endpointNames": [
"events"
],
"isEnabled": true
}
},
"storageEndpoints": {},
"messagingEndpoints": {
"fileNotifications": {
"lockDurationAsIso8601": "PT1M",
"ttlAsIso8601": "PT1H",
"maxDeliveryCount": 10
}
},
"enableFileUploadNotifications": false,
"cloudToDevice": {
"maxDeliveryCount": 10,
"defaultTtlAsIso8601": "PT1H",
"feedback": {
"lockDurationAsIso8601": "PT1M",
"ttlAsIso8601": "PT1H",
"maxDeliveryCount": 10
}
},
"features": "None"
}
},
{
"type": "Microsoft.Web/serverfarms",
"apiVersion": "2018-02-01",
"name": "[variables('website').serverfarms.name]",
"location": "[resourceGroup().location]",
"dependsOn": [
],
"sku": {
"name": "D1",
"tier": "Shared",
"size": "D1",
"family": "D",
"capacity": 0
},
"kind": "app",
"properties": {
"perSiteScaling": false,
"maximumElasticWorkerCount": 1,
"isSpot": false,
"reserved": false,
"isXenon": false,
"hyperV": false,
"targetWorkerCount": 0,
"targetWorkerSizeId": 0
}
},
{
"apiVersion": "2018-02-01",
"name": "[variables('website').name]",
"type": "Microsoft.Web/sites",
"location": "[resourceGroup().location]",
"dependsOn": [
"[resourceId('Microsoft.Web/serverfarms', variables('website').serverfarms.name)]"
],
"tags": null,
"properties": {
"name": "[variables('website').name]",
"enabled": true,
"hostNameSslStates": [{
"name": "[concat(variables('website').name, '.azurewebsites.net')]",
"sslState": "Disabled",
"hostType": "Standard"
},
{
"name": "[concat(variables('website').name, '.scm.azurewebsites.net')]",
"sslState": "Disabled",
"hostType": "Repository"
}
],
"serverFarmId": "[resourceId('Microsoft.Web/serverfarms', variables('website').serverfarms.name)]",
"siteConfig": {
"appSettings": [{
"name": "ApplicationInsightsAgent_EXTENSION_VERSION",
"value": "~2"
},
{
"name": "XDT_MicrosoftApplicationInsights_Mode",
"value": "default"
},
{
"name": "DiagnosticServices_EXTENSION_VERSION",
"value": "disabled"
},
{
"name": "APPINSIGHTS_PROFILERFEATURE_VERSION",
"value": "disabled"
},
{
"name": "APPINSIGHTS_SNAPSHOTFEATURE_VERSION",
"value": "disabled"
},
{
"name": "InstrumentationEngine_EXTENSION_VERSION",
"value": "disabled"
},
{
"name": "SnapshotDebugger_EXTENSION_VERSION",
"value": "disabled"
},
{
"name": "XDT_MicrosoftApplicationInsights_BaseExtensions",
"value": "disabled"
},
{
"name": "IoTHub:ModuleConnectionString",
"value": ""
}
],
"metadata": [{
"name": "CURRENT_STACK",
"value": "dotnetcore"
}]
},
"clientAffinityEnabled": true
},
"resources": [{
"name": "MSDeploy",
"type": "extensions",
"location": "[resourceGroup().location]",
"apiVersion": "2015-08-01",
"dependsOn": [
"[concat('Microsoft.Web/sites/', variables('website').name)]"
],
"tags": {
"displayName": "webDeploy"
},
"properties": {
"packageUri": "https://zarmada.blob.core.windows.net/sqldbedge-shared/sqldbedgedemo.zip?sp=r&st=2020-04-21T03:07:20Z&se=2050-04-21T11:07:20Z&spr=https&sv=2019-02-02&sr=b&sig=veWrTqH9RqjDeceBszIBtg3ueQlyBxmLd4ZPVrpTXpg%3D",
"dbType": "None",
"connectionString": ""
}
}]
}
]
}
@@ -0,0 +1,4 @@
CONTAINER_REGISTRY_NAME=
CONTAINER_REGISTRY_USER_NAME=
CONTAINER_REGISTRY_PASSWORD=
SQL_PACKAGE=
@@ -0,0 +1,43 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "SensorModule Remote Debug (.NET Core)",
"type": "coreclr",
"request": "attach",
"processId": "${command:pickRemoteProcess}",
"pipeTransport": {
"pipeProgram": "docker",
"pipeArgs": [
"exec",
"-i",
"SensorModule",
"sh",
"-c"
],
"debuggerPath": "~/vsdbg/vsdbg",
"pipeCwd": "${workspaceFolder}",
"quoteArgs": true
},
"sourceFileMap": {
"/app": "${workspaceFolder}/modules/SensorModule"
},
"justMyCode": true
},
{
"name": "SensorModule Local Debug (.NET Core)",
"type": "coreclr",
"request": "launch",
"program": "${workspaceRoot}/modules/SensorModule/bin/Debug/netcoreapp2.1/SensorModule.dll",
"args": [],
"cwd": "${workspaceRoot}/modules/SensorModule",
"internalConsoleOptions": "openOnSessionStart",
"stopAtEntry": false,
"console": "internalConsole",
"env": {
"EdgeHubConnectionString": "${config:azure-iot-edge.EdgeHubConnectionString}",
"EdgeModuleCACertificateFile": "${config:azure-iot-edge.EdgeModuleCACertificateFile}"
}
}
]
}
@@ -0,0 +1,6 @@
{
"azure-iot-edge.defaultPlatform": {
"platform": "amd64",
"alias": null
}
}
@@ -0,0 +1,122 @@
{
"$schema-template": "2.0.0",
"modulesContent": {
"$edgeAgent": {
"properties.desired": {
"schemaVersion": "1.0",
"runtime": {
"type": "docker",
"settings": {
"minDockerVersion": "v1.25",
"loggingOptions": "",
"registryCredentials": {
"sqldbedgecr": {
"username": "$CONTAINER_REGISTRY_USER_NAME",
"password": "$CONTAINER_REGISTRY_PASSWORD",
"address": "$CONTAINER_REGISTRY_NAME"
}
}
}
},
"systemModules": {
"edgeAgent": {
"type": "docker",
"settings": {
"image": "mcr.microsoft.com/azureiotedge-agent:1.0",
"createOptions": {}
}
},
"edgeHub": {
"type": "docker",
"status": "running",
"restartPolicy": "always",
"settings": {
"image": "mcr.microsoft.com/azureiotedge-hub:1.0",
"createOptions": {
"HostConfig": {
"PortBindings": {
"5671/tcp": [
{
"HostPort": "5671"
}
],
"8883/tcp": [
{
"HostPort": "8883"
}
],
"443/tcp": [
{
"HostPort": "443"
}
]
}
}
}
}
}
},
"modules": {
"SensorModule": {
"version": "1.0",
"type": "docker",
"status": "running",
"restartPolicy": "always",
"settings": {
"image": "${MODULES.SensorModule.debug}",
"createOptions": {}
}
},
"AzureSQLDatabaseEdge": {
"settings": {
"image": "marketplace.azurecr.io/microsoftsqledge-preview/azure-sql-database-edge",
"createOptions": "{\"HostConfig\":{\"CapAdd\":[\"SYS_PTRACE\"],\"Binds\":[\"sqlvolume:/sqlvolume\"],\"PortBindings\":{\"1433/tcp\":[{\"HostPort\":\"1433\"}]},\"Mounts\":[{\"Type\":\"volume\",\"Source\":\"sqlvolume\",\"Target\":\"/var/opt/mssql\"}]},\"User\":\"0:0\",\"Env\":[\"MSSQL_AGENT_ENABLED=TRUE\",\"ClientTransportType=AMQP_TCP_Only\",\"MSSQL_PID=Developer\"]}"
},
"type": "docker",
"version": "1.0",
"env": {
"ACCEPT_EULA": {
"value": "Y"
},
"SA_PASSWORD": {
"value": "Microsoft2020$"
},
"MSSQL_LCID": {
"value": "1033"
},
"MSSQL_COLLATION": {
"value": "SQL_Latin1_General_CP1_CI_AS"
}
},
"status": "running",
"restartPolicy": "always"
}
}
}
},
"$edgeHub": {
"properties.desired": {
"schemaVersion": "1.0",
"routes": {
"SensorModuleToIoTHub": "FROM /messages/modules/SensorModule/outputs/* INTO $upstream"
},
"storeAndForwardConfiguration": {
"timeToLiveSecs": 7200
}
}
},
"SensorModule": {
"properties.desired": {
"SqlConnnectionString": "Server=tcp:AzureSQLDatabaseEdge,1433;Initial Catalog=turbine-sensor-db;Persist Security Info=False;User ID=sa;Password=Microsoft2020$;MultipleActiveResultSets=False;Connection Timeout=30;",
"PushTimeInterval": 5000,
"Alert": "start",
"OnnxModelUrl": ""
}
},
"AzureSQLDatabaseEdge": {
"properties.desired": {
"SqlPackage": "$SQL_PACKAGE"
}
}
}
}
@@ -0,0 +1,34 @@
# .NET Core
project.lock.json
project.fragment.lock.json
artifacts/
**/Properties/launchSettings.json
*_i.c
*_p.c
*_i.h
*.ilk
*.meta
*.obj
*.pch
*.pdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*.log
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
.vs
[Bb]in/
[Oo]bj/
@@ -0,0 +1,56 @@
using System;
using System.Collections.Generic;
using SensorModule.Models;
namespace SensorModule
{
public static class Constants
{
public static class Sensors
{
public const string WindSpeedStdDev = "WindSpeedStdDev";
public const string TurbineSpeedStdDev = "TurbineSpeedStdDev";
public const string OverallWindDirection = "OverallWindDirection";
public const string TurbineWindDirection = "TurbineWindDirection";
public const string WindSpeedAverage = "WindSpeedAverage";
public const string WindTempAverage = "WindTempAverage";
public const string GearboxOilLevel = "GearboxOilLevel";
public const string GearboxOilTemp = "GearboxOilTemp";
public const string GeneratorActivePower = "GeneratorActivePower";
public const string GeneratorSpeed = "GeneratorSpeed";
public const string GeneratorTemp = "GeneratorTemp";
public const string GeneratorTorque = "GeneratorTorque";
public const string GridFrequency = "GridFrequency";
public const string GridVoltage = "GridVoltage";
public const string HydraulicOilPressure = "HydraulicOilPressure";
public const string NacelleAngle = "NacelleAngle";
public const string PitchAngle = "PitchAngle";
public const string Vibration = "Vibration";
public const string TurbineSpeedAverage = "TurbineSpeedAverage";
public const string HatchSensor = "HatchSensor";
}
public static List<Sensor> SensorsList = new List<Sensor>{
new Sensor {Id = Guid.NewGuid(), Type = Sensors.WindSpeedStdDev},
new Sensor {Id = Guid.NewGuid(), Type = Sensors.TurbineSpeedStdDev},
new Sensor {Id = Guid.NewGuid(), Type = Sensors.OverallWindDirection},
new Sensor {Id = Guid.NewGuid(), Type = Sensors.TurbineWindDirection},
new Sensor {Id = Guid.NewGuid(), Type = Sensors.WindSpeedAverage},
new Sensor {Id = Guid.NewGuid(), Type = Sensors.WindTempAverage},
new Sensor {Id = Guid.NewGuid(), Type = Sensors.GearboxOilLevel},
new Sensor {Id = Guid.NewGuid(), Type = Sensors.GearboxOilTemp},
new Sensor {Id = Guid.NewGuid(), Type = Sensors.GeneratorActivePower},
new Sensor {Id = Guid.NewGuid(), Type = Sensors.GeneratorSpeed},
new Sensor {Id = Guid.NewGuid(), Type = Sensors.GeneratorTemp},
new Sensor {Id = Guid.NewGuid(), Type = Sensors.GeneratorTorque},
new Sensor {Id = Guid.NewGuid(), Type = Sensors.GridFrequency},
new Sensor {Id = Guid.NewGuid(), Type = Sensors.GridVoltage},
new Sensor {Id = Guid.NewGuid(), Type = Sensors.HydraulicOilPressure},
new Sensor {Id = Guid.NewGuid(), Type = Sensors.NacelleAngle},
new Sensor {Id = Guid.NewGuid(), Type = Sensors.PitchAngle},
new Sensor {Id = Guid.NewGuid(), Type = Sensors.Vibration},
new Sensor {Id = Guid.NewGuid(), Type = Sensors.TurbineSpeedAverage},
new Sensor {Id = Guid.NewGuid(), Type = Sensors.HatchSensor}
};
}
}
@@ -0,0 +1,29 @@
using SensorModule;
using SensorModule.Models;
using Microsoft.EntityFrameworkCore;
namespace SensorModule.DataStore
{
public class DatabaseContext : DbContext
{
private string _sqlConnectionString = string.Empty;
public DatabaseContext(string sqlConnectionString)
{
this._sqlConnectionString = sqlConnectionString;
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseSqlServer(_sqlConnectionString);
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<OnnxModel>().ToTable("models");
}
public DbSet<RealtimeWindTurbineRecord> RealtimeWindTurbineRecord { get; set; }
public DbSet<RealtimeSensorRecord> RealtimeSensorRecord { get; set; }
}
}
@@ -0,0 +1,360 @@
using System;
using System.Collections;
using System.Collections.Generic;
namespace SensorModule.DataStructures
{
/// <inheritdoc/>
/// <summary>
/// Ring buffer.
///
/// When writing to a full buffer:
/// PushBack -> removes this[0] / Front()
/// PushFront -> removes this[Size-1] / Back()
///
/// this implementation is inspired by
/// http://www.boost.org/doc/libs/1_53_0/libs/Ring_buffer/doc/Ring_buffer.html
/// because I liked their interface.
/// </summary>
public class RingBuffer<T> : IEnumerable<T>
{
private readonly T[] _buffer;
/// <summary>
/// The _start. Index of the first element in buffer.
/// </summary>
private int _start;
/// <summary>
/// The _end. Index after the last element in the buffer.
/// </summary>
private int _end;
/// <summary>
/// The _size. Buffer size.
/// </summary>
private int _size;
public RingBuffer(int capacity)
: this(capacity, new T[] { })
{
}
/// <summary>
/// Initializes a new instance of the <see cref="RingBuffer{T}"/> class.
///
/// </summary>
/// <param name='capacity'>
/// Buffer capacity. Must be positive.
/// </param>
/// <param name='items'>
/// Items to fill buffer with. Items length must be less than capacity.
/// Suggestion: use Skip(x).Take(y).ToArray() to build this argument from
/// any enumerable.
/// </param>
public RingBuffer(int capacity, T[] items)
{
if (capacity < 1)
{
throw new ArgumentException(
"Ring buffer cannot have negative or zero capacity.", nameof(capacity));
}
if (items == null)
{
throw new ArgumentNullException(nameof(items));
}
if (items.Length > capacity)
{
throw new ArgumentException(
"Too many items to fit Ring buffer", nameof(items));
}
_buffer = new T[capacity];
Array.Copy(items, _buffer, items.Length);
_size = items.Length;
_start = 0;
_end = _size == capacity ? 0 : _size;
}
/// <summary>
/// Maximum capacity of the buffer. Elements pushed into the buffer after
/// maximum capacity is reached (IsFull = true), will remove an element.
/// </summary>
public int Capacity { get { return _buffer.Length; } }
public bool IsFull
{
get
{
return Size == Capacity;
}
}
public bool IsEmpty
{
get
{
return Size == 0;
}
}
/// <summary>
/// Current buffer size (the number of elements that the buffer has).
/// </summary>
public int Size { get { return _size; } }
/// <summary>
/// Element at the front of the buffer - this[0].
/// </summary>
/// <returns>The value of the element of type T at the front of the buffer.</returns>
public T Front()
{
ThrowIfEmpty();
return _buffer[_start];
}
/// <summary>
/// Element at the back of the buffer - this[Size - 1].
/// </summary>
/// <returns>The value of the element of type T at the back of the buffer.</returns>
public T Back()
{
ThrowIfEmpty();
return _buffer[(_end != 0 ? _end : Capacity) - 1];
}
public T this[int index]
{
get
{
if (IsEmpty)
{
throw new IndexOutOfRangeException(string.Format("Cannot access index {0}. Buffer is empty", index));
}
if (index >= _size)
{
throw new IndexOutOfRangeException(string.Format("Cannot access index {0}. Buffer size is {1}", index, _size));
}
int actualIndex = InternalIndex(index);
return _buffer[actualIndex];
}
set
{
if (IsEmpty)
{
throw new IndexOutOfRangeException(string.Format("Cannot access index {0}. Buffer is empty", index));
}
if (index >= _size)
{
throw new IndexOutOfRangeException(string.Format("Cannot access index {0}. Buffer size is {1}", index, _size));
}
int actualIndex = InternalIndex(index);
_buffer[actualIndex] = value;
}
}
/// <summary>
/// Pushes a new element to the back of the buffer. Back()/this[Size-1]
/// will now return this element.
///
/// When the buffer is full, the element at Front()/this[0] will be
/// popped to allow for this new element to fit.
/// </summary>
/// <param name="item">Item to push to the back of the buffer</param>
public void PushBack(T item)
{
if (IsFull)
{
_buffer[_end] = item;
Increment(ref _end);
_start = _end;
}
else
{
_buffer[_end] = item;
Increment(ref _end);
++_size;
}
}
/// <summary>
/// Pushes a new element to the front of the buffer. Front()/this[0]
/// will now return this element.
///
/// When the buffer is full, the element at Back()/this[Size-1] will be
/// popped to allow for this new element to fit.
/// </summary>
/// <param name="item">Item to push to the front of the buffer</param>
public void PushFront(T item)
{
if (IsFull)
{
Decrement(ref _start);
_end = _start;
_buffer[_start] = item;
}
else
{
Decrement(ref _start);
_buffer[_start] = item;
++_size;
}
}
/// <summary>
/// Removes the element at the back of the buffer. Decreasing the
/// Buffer size by 1.
/// </summary>
public void PopBack()
{
ThrowIfEmpty("Cannot take elements from an empty buffer.");
Decrement(ref _end);
_buffer[_end] = default(T);
--_size;
}
/// <summary>
/// Removes the element at the front of the buffer. Decreasing the
/// Buffer size by 1.
/// </summary>
public void PopFront()
{
ThrowIfEmpty("Cannot take elements from an empty buffer.");
_buffer[_start] = default(T);
Increment(ref _start);
--_size;
}
/// <summary>
/// Copies the buffer contents to an array, according to the logical
/// contents of the buffer (i.e. independent of the internal
/// order/contents)
/// </summary>
/// <returns>A new array with a copy of the buffer contents.</returns>
public T[] ToArray()
{
T[] newArray = new T[Size];
int newArrayOffset = 0;
var segments = new ArraySegment<T>[2] { ArrayOne(), ArrayTwo() };
foreach (ArraySegment<T> segment in segments)
{
Array.Copy(segment.Array, segment.Offset, newArray, newArrayOffset, segment.Count);
newArrayOffset += segment.Count;
}
return newArray;
}
#region IEnumerable<T> implementation
public IEnumerator<T> GetEnumerator()
{
var segments = new ArraySegment<T>[2] { ArrayOne(), ArrayTwo() };
foreach (ArraySegment<T> segment in segments)
{
for (int i = 0; i < segment.Count; i++)
{
yield return segment.Array[segment.Offset + i];
}
}
}
#endregion
#region IEnumerable implementation
IEnumerator IEnumerable.GetEnumerator()
{
return (IEnumerator)GetEnumerator();
}
#endregion
private void ThrowIfEmpty(string message = "Cannot access an empty buffer.")
{
if (IsEmpty)
{
throw new InvalidOperationException(message);
}
}
/// <summary>
/// Increments the provided index variable by one, wrapping
/// around if necessary.
/// </summary>
/// <param name="index"></param>
private void Increment(ref int index)
{
if (++index == Capacity)
{
index = 0;
}
}
/// <summary>
/// Decrements the provided index variable by one, wrapping
/// around if necessary.
/// </summary>
/// <param name="index"></param>
private void Decrement(ref int index)
{
if (index == 0)
{
index = Capacity;
}
index--;
}
/// <summary>
/// Converts the index in the argument to an index in <code>_buffer</code>
/// </summary>
/// <returns>
/// The transformed index.
/// </returns>
/// <param name='index'>
/// External index.
/// </param>
private int InternalIndex(int index)
{
return _start + (index < (Capacity - _start) ? index : index - Capacity);
}
// doing ArrayOne and ArrayTwo methods returning ArraySegment<T> as seen here:
// http://www.boost.org/doc/libs/1_37_0/libs/Ring_buffer/doc/Ring_buffer.html#classboost_1_1Ring__buffer_1957cccdcb0c4ef7d80a34a990065818d
// http://www.boost.org/doc/libs/1_37_0/libs/Ring_buffer/doc/Ring_buffer.html#classboost_1_1Ring__buffer_1f5081a54afbc2dfc1a7fb20329df7d5b
// should help a lot with the code.
#region Array items easy access.
// The array is composed by at most two non-contiguous segments,
// the next two methods allow easy access to those.
private ArraySegment<T> ArrayOne()
{
if (IsEmpty)
{
return new ArraySegment<T>(new T[0]);
}
else if (_start < _end)
{
return new ArraySegment<T>(_buffer, _start, _end - _start);
}
else
{
return new ArraySegment<T>(_buffer, _start, _buffer.Length - _start);
}
}
private ArraySegment<T> ArrayTwo()
{
if (IsEmpty)
{
return new ArraySegment<T>(new T[0]);
}
else if (_start < _end)
{
return new ArraySegment<T>(_buffer, _end, 0);
}
else
{
return new ArraySegment<T>(_buffer, 0, _end);
}
}
#endregion
}
}
@@ -0,0 +1,17 @@
FROM mcr.microsoft.com/dotnet/core/sdk:3.1 AS build-env
WORKDIR /app
COPY *.csproj ./
RUN dotnet restore
COPY . ./
RUN dotnet publish -c Release -o out
FROM mcr.microsoft.com/dotnet/core/runtime:3.1-buster-slim
WORKDIR /app
COPY --from=build-env /app/out ./
RUN useradd -ms /bin/bash moduleuser
USER moduleuser
ENTRYPOINT ["dotnet", "SensorModule.dll"]
@@ -0,0 +1,24 @@
FROM mcr.microsoft.com/dotnet/core/runtime:3.1-buster-slim AS base
RUN apt-get update && \
apt-get install -y --no-install-recommends unzip procps && \
rm -rf /var/lib/apt/lists/*
RUN useradd -ms /bin/bash moduleuser
USER moduleuser
RUN curl -sSL https://aka.ms/getvsdbgsh | bash /dev/stdin -v latest -l ~/vsdbg
FROM mcr.microsoft.com/dotnet/core/sdk:3.1 AS build-env
WORKDIR /app
COPY *.csproj ./
RUN dotnet restore
COPY . ./
RUN dotnet publish -c Debug -o out
FROM base
WORKDIR /app
COPY --from=build-env /app/out ./
ENTRYPOINT ["dotnet", "SensorModule.dll"]
@@ -0,0 +1,17 @@
FROM mcr.microsoft.com/dotnet/core/sdk:3.1 AS build-env
WORKDIR /app
COPY *.csproj ./
RUN dotnet restore
COPY . ./
RUN dotnet publish -c Release -o out
FROM mcr.microsoft.com/dotnet/core/runtime:3.1-buster-slim-arm32v7
WORKDIR /app
COPY --from=build-env /app/out ./
RUN useradd -ms /bin/bash moduleuser
USER moduleuser
ENTRYPOINT ["dotnet", "SensorModule.dll"]
@@ -0,0 +1,24 @@
FROM mcr.microsoft.com/dotnet/core/runtime:3.1-buster-slim-arm32v7 AS base
RUN apt-get update && \
apt-get install -y --no-install-recommends unzip procps && \
rm -rf /var/lib/apt/lists/*
RUN useradd -ms /bin/bash moduleuser
USER moduleuser
RUN curl -sSL https://aka.ms/getvsdbgsh | bash /dev/stdin -v latest -l ~/vsdbg
FROM mcr.microsoft.com/dotnet/core/sdk:3.1 AS build-env
WORKDIR /app
COPY *.csproj ./
RUN dotnet restore
COPY . ./
RUN dotnet publish -c Debug -o out
FROM base
WORKDIR /app
COPY --from=build-env /app/out ./
ENTRYPOINT ["dotnet", "SensorModule.dll"]
@@ -0,0 +1,13 @@
FROM mcr.microsoft.com/dotnet/core/sdk:3.1 AS build-env
WORKDIR /app
COPY *.csproj ./
RUN dotnet restore
COPY . ./
RUN dotnet publish -c Release -o out
FROM mcr.microsoft.com/dotnet/core/runtime:3.1-nanoserver-1909
WORKDIR /app
COPY --from=build-env /app/out ./
ENTRYPOINT ["dotnet", "SensorModule.dll"]
@@ -0,0 +1,316 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace SensorModule.Helpers
{
public class GeneratorHelper
{
private Random _random = new Random();
public double GetGearBoxOilLevel()
{
int trendSign = _random.Next(-1, 1);
var offset = GetRandomNumber(0, 5);
offset = trendSign < 0 ? offset * -1 : offset;
return 40 + offset;
}
public double GetGearBoxTemp()
{
var offset = GetRandomNumber(0, 3);
int trendSign = _random.Next(-2, 1);
var toReturn = 53.0 + offset;
if (toReturn > 40)
{
offset = trendSign < 0 ? offset * -1 : offset;
}
else if (toReturn > 61)
{
offset *= -1;
}
toReturn += offset;
return toReturn > 61 ? GetRandomNumber(60, 61) : toReturn;
}
public double GetGeneratorActivePower(double windSpeed)
{
Dictionary<int, double> Values = new Dictionary<int, double>()
{
{ 0, -1.165047329 },
{ 1, -1.561275004 },
{ 2, -1.523817212 },
{ 3, 0.258328264 },
{ 4, 39.48032308 },
{ 5, 129.0223307 },
{ 6, 296.3115464 },
{ 7, 533.6013549 },
{ 8, 784.2038201 },
{ 9, 1033.222044 },
{ 10, 1282.494906 },
{ 11, 1522.07693 },
{ 12, 1710.51213 },
{ 13, 1847.754821 },
{ 14, 1909.292275 },
{ 15, 1929.077495 },
};
var ws = Convert.ToInt32(Math.Round(windSpeed));
ws = ws > 15 ? 15 : ws;
var apAvg = Values[ws];
int trendSign = _random.Next(-2, 1);
double offset;
if (ws <= 3)
{
offset = GetRandomNumber(-0.04, 0.01);
}
else if (ws <= 7)
{
offset = GetRandomNumber(0.01, 0.15);
}
else if (ws <= 12)
{
offset = GetRandomNumber(0.01, 0.63);
}
else
{
offset = GetRandomNumber(0.01, 0.94);
}
offset = trendSign < 0 ? offset * -1 : offset;
var toReturn = (apAvg * offset) + apAvg;
return toReturn > 2100 ? 2100 : toReturn;
}
public double GetGeneratorSpeed(double windSpeed)
{
Dictionary<int, double> Values = new Dictionary<int, double>()
{
{ 0, 29.5339 },
{ 1, 93.4983 },
{ 2, 123.626 },
{ 3, 270.0063 },
{ 4, 850.3199 },
{ 5, 1059.4094 },
{ 6, 1317.4786 },
{ 7, 1548.3753 },
{ 8, 1635.4728 },
{ 9, 1660.1312 },
{ 10, 1685.2159 },
{ 11, 1709.3631 },
{ 12, 1722.729 },
{ 13, 1743.2982 },
{ 14, 1739.6748 },
{ 15, 1737.1417 }
};
var ws = Convert.ToInt32(Math.Round(windSpeed));
ws = ws > 15 ? 15 : ws;
var gsAvg = Values[ws];
double rate;
int trendSign = _random.Next(-3, 1);
if (ws <= 5)
{
rate = GetRandomNumber(0, 0.7);
}
else if (ws <= 8)
{
rate = GetRandomNumber(0.001, 0.055);
}
else if (ws <= 12)
{
rate = GetRandomNumber(0.001, 0.04362);
trendSign = _random.Next(-1, 2);
}
else
{
rate = GetRandomNumber(0.001, 0.374);
trendSign = _random.Next(-1, 3);
}
rate = trendSign < 0 ? rate * -1 : rate;
var toReturn = (gsAvg * rate) + gsAvg;
return toReturn > 1800.100 + 5 ? 1800.100 : toReturn;
}
public double GetGeneratorStatorTemp(double generatorSpeed)
{
double[] avgTempValues = { 15.0, 45.5, 55.3, 72.5 };
var gs = Convert.ToInt32(Math.Round(generatorSpeed));
var offset = GetRandomNumber(0, 10);
int trendSign = _random.Next(-2, 2);
offset = trendSign < 0 ? offset * -1 : offset;
double toReturn;
if (gs < 450)
{
toReturn = avgTempValues[0] + offset;
}
else if (gs < 900)
{
toReturn = avgTempValues[1] + offset;
}
else if (gs < 1350)
{
toReturn = avgTempValues[2] + offset;
}
else
{
offset = GetRandomNumber(0, 5);
toReturn = avgTempValues[3] + offset;
}
return toReturn > 83 ? GetRandomNumber(82, 84) : toReturn;
}
public double GetGeneratorTorque(double generatorSpeed, double activePower)
{
var toReturn = activePower == 0 || generatorSpeed == 0 ? 0 : (activePower / generatorSpeed) * (30 / 3.1416);
return toReturn > 10 ? GetRandomNumber(9, 10) : toReturn;
}
public double GetGridFrequency(double activePower)
{
if (activePower == 0)
{
return 0;
}
else
{
var offset = GetRandomNumber(0, 0.5);
int trendSign = _random.Next(-1, 1);
offset = trendSign < 0 ? offset * -1 : offset;
return 50 + offset;
}
}
public double GetVoltage(double activePower)
{
var offset = GetRandomNumber(0, 20);
int trendSign = _random.Next(-1, 4);
offset = trendSign < 0 ? GetRandomNumber(0, 5) * -1 : offset;
return activePower != 0 ? 690 + offset : 0;
}
public double GetHydraulicOilPressure()
{
var offset = GetRandomNumber(0, 10);
int trendSign = _random.Next(-2, 1);
double toReturn = 250 + offset;
if (toReturn > 249)
{
offset = trendSign < 0 ? offset * -1 : offset;
}
else if (toReturn > 375)
{
offset *= -1;
}
toReturn += offset;
return toReturn > 380 ? GetRandomNumber(379, 380) : toReturn;
}
public double GetNacelleAngle(double windDirection)
{
var offset = GetRandomNumber(0, 10);
int trendSign = _random.Next(-2, 2);
offset = trendSign < 0 ? offset * -1 : offset;
var toReturn = windDirection + offset;
return toReturn > 360 ? GetRandomNumber(358, 360) : toReturn;
}
public double GetPitchAngle(double windSpeed)
{
Dictionary<int, PitchAngleReference> Values = new Dictionary<int, PitchAngleReference>()
{
{ 0, new PitchAngleReference { Ratio = 0.98, Avg = 54.24524239 } },
{ 1, new PitchAngleReference {Ratio = 1.014, Avg = 51.67482604 } },
{ 2, new PitchAngleReference {Ratio = 1.4, Avg = 50.15667612 } },
{ 3, new PitchAngleReference {Ratio = 3.75, Avg = 44.47110192 } },
{ 4, new PitchAngleReference {Ratio = 10.9, Avg = 10.16831867 } },
{ 5, new PitchAngleReference {Ratio = 8, Avg = 3.753638458 } },
{ 6, new PitchAngleReference {Ratio = 8.6, Avg = 3.143487107 } },
{ 7, new PitchAngleReference {Ratio = 8.1, Avg = 3.851029642 } },
{ 8, new PitchAngleReference {Ratio = 7, Avg = 5.51622539 } },
{ 9, new PitchAngleReference {Ratio = 12.3, Avg = 5.607653521 } },
{ 10, new PitchAngleReference {Ratio = 11.3, Avg = 4.906807873 } },
{ 11, new PitchAngleReference {Ratio = 12.5, Avg = 4.556013361 } },
{ 12, new PitchAngleReference {Ratio = 7.6, Avg = 5.461135239 } },
{ 13, new PitchAngleReference {Ratio = 4, Avg = 6.617167409 } },
{ 14, new PitchAngleReference {Ratio = 6.5, Avg = 9.174521933 } },
{ 15, new PitchAngleReference {Ratio = 7.1, Avg = 11.5855706 } },
};
var ws = Convert.ToInt32(Math.Round(windSpeed));
ws = ws > 15 ? 15 : ws;
var paAvg = Values[ws].Avg;
var rate = GetRandomNumber(0.001, Values[ws].Ratio);
int trendSign = _random.Next(-2, 2);
rate = trendSign < 0 ? rate * -1 : rate;
var toReturn = (paAvg * rate) + paAvg;
// This is needed as the avg for speeds greater than 5 are to low and the min value is to close to 0.
if (ws >= 5 && toReturn < 0)
{
toReturn = GetRandomNumber(-13, 1);
}
return toReturn > 262.6 + 5 ? 262.6 : toReturn;
}
public double GetVibration(double windSpeed)
{
if (windSpeed < 5)
{
return GetRandomNumber(0, 50);
}
else
{
return windSpeed * 13;
}
}
public double GetRandomNumber(double minimum, double maximum)
{
return _random.NextDouble() * (maximum - minimum) + minimum;
}
public bool GetRandomBool(int truePercentage = 50)
{
return _random.NextDouble() < truePercentage / 100.0;
}
public double CalculateStandardDeviation(IEnumerable<double> values)
{
double standardDeviation = 0;
if (values.Any())
{
// Compute the average.
double avg = values.Average();
// Perform the Sum of (value-avg)_2_2.
double sum = values.Sum(d => Math.Pow(d - avg, 2));
// Put it all together.
standardDeviation = Math.Sqrt((sum) / (values.Count() - 1));
}
return double.IsNaN(standardDeviation) ? 0 : standardDeviation;
}
}
class PitchAngleReference
{
public double Ratio { get; set; }
public double Avg { get; set; }
}
}
@@ -0,0 +1,27 @@
namespace SensorModule.Models.Interfaces
{
public interface IWindTurbineRecord
{
int TurbineId { get; set; }
double WindSpeedStdDev { get; set; }
double TurbineSpeedStdDev { get; set; }
double OverallWindDirection { get; set; }
double TurbineWindDirection { get; set; }
double WindSpeedAverage { get; set; }
double WindTempAverage { get; set; }
bool Precipitation { get; set; }
double GearboxOilLevel { get; set; }
double GearboxOilTemp { get; set; }
double GeneratorActivePower { get; set; }
double GeneratorSpeed { get; set; }
double GeneratorTemp { get; set; }
double GeneratorTorque { get; set; }
double GridFrequency { get; set; }
double GridVoltage { get; set; }
double HydraulicOilPressure { get; set; }
double NacelleAngle { get; set; }
double PitchAngle { get; set; }
double Vibration { get; set; }
double TurbineSpeedAverage { get; set; }
}
}
@@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Text;
namespace SensorModule.Models
{
public class OnnxModel
{
[Key]
public int Id { get; set; }
public string Description { get; set; }
public byte[] Data { get; set; }
}
}
@@ -0,0 +1,16 @@
using System;
using System.ComponentModel.DataAnnotations;
namespace SensorModule.Models
{
public class RealtimeSensorRecord
{
[Key]
public Guid RecordId { get; set; }
public Guid TurbineId { get; set; }
public Guid SensorId { get; set; }
public string SensorType { get; set; }
public double SensorValue { get; set; }
public DateTime Timestamp { get; set; }
}
}
@@ -0,0 +1,35 @@
using SensorModule.Models.Interfaces;
using System;
using System.ComponentModel.DataAnnotations;
namespace SensorModule.Models
{
public class RealtimeWindTurbineRecord : IWindTurbineRecord
{
[Key]
public Guid RecordId { get; set; }
public int TurbineId { get; set; }
public Guid TurbineGuid { get; set; }
public double WindSpeedStdDev { get; set; }
public double TurbineSpeedStdDev { get; set; }
public double OverallWindDirection { get; set; }
public double TurbineWindDirection { get; set; }
public double WindSpeedAverage { get; set; }
public double WindTempAverage { get; set; }
public bool Precipitation { get; set; }
public double GearboxOilLevel { get; set; }
public double GearboxOilTemp { get; set; }
public double GeneratorActivePower { get; set; }
public double GeneratorSpeed { get; set; }
public double GeneratorTemp { get; set; }
public double GeneratorTorque { get; set; }
public double GridFrequency { get; set; }
public double GridVoltage { get; set; }
public double HydraulicOilPressure { get; set; }
public double NacelleAngle { get; set; }
public double PitchAngle { get; set; }
public double Vibration { get; set; }
public double TurbineSpeedAverage { get; set; }
public DateTime Timestamp { get; set; }
}
}
@@ -0,0 +1,11 @@
using System;
namespace SensorModule.Models
{
public class Sensor
{
public Guid Id { get; set; } = Guid.NewGuid();
public string Type { get; set; }
}
}
@@ -0,0 +1,30 @@
using SensorModule.Models.Interfaces;
namespace SensorModule.Models
{
public class TrainingWindTurbineRecord : IWindTurbineRecord
{
public int TurbineId { get; set; }
public double WindSpeedStdDev { get; set; }
public double TurbineSpeedStdDev { get; set; }
public double OverallWindDirection { get; set; }
public double TurbineWindDirection { get; set; }
public double WindSpeedAverage { get; set; }
public double WindTempAverage { get; set; }
public bool Precipitation { get; set; }
public double GearboxOilLevel { get; set; }
public double GearboxOilTemp { get; set; }
public double GeneratorActivePower { get; set; }
public double GeneratorSpeed { get; set; }
public double GeneratorTemp { get; set; }
public double GeneratorTorque { get; set; }
public double GridFrequency { get; set; }
public double GridVoltage { get; set; }
public double HydraulicOilPressure { get; set; }
public double NacelleAngle { get; set; }
public double PitchAngle { get; set; }
public double Vibration { get; set; }
public double TurbineSpeedAverage { get; set; }
public bool AlterBlades { get; set; }
}
}
@@ -0,0 +1,197 @@
namespace SensorModule
{
using System;
using System.IO;
using System.Runtime.InteropServices;
using System.Runtime.Loader;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Azure.Devices.Client;
using Microsoft.Azure.Devices.Client.Transport.Mqtt;
using Microsoft.Azure.Devices.Shared;
using Newtonsoft.Json;
using SensorModule.Services;
using SensorModule.Services.Interfaces;
class Program
{
private static IDataGeneratorService _datageneratorservice;
private static IDataStoreService _datastoreservice;
private static ModuleClient _ioTHubModuleClient;
static int PushTimeInterval { get; set; } = 5000;
static bool StopWake { get; set; } = false;
static bool BlockedDataGeneration { get; set; } = false;
static async Task Main(string[] args)
{
_datageneratorservice = new DataGeneratorService();
_datastoreservice = new DataStoreService();
await Init();
try
{
Console.WriteLine("Truncating sensors table before start pushing data");
_datastoreservice.TruncateRealtimeSensorRecordTable();
}
catch(Exception e)
{
Console.WriteLine("Error truncating table: " + e.Message);
}
Console.WriteLine($"Starting writing realtime records with {PushTimeInterval} millisecond interval to database...");
while (true)
{
if (!BlockedDataGeneration)
{
var records = _datageneratorservice.GenerateRealTimeSensorRecords(StopWake);
Console.WriteLine($"[{DateTime.Now}] Writing records to db.");
_datastoreservice.WriteSensorRecordsToDB(records);
}
await Task.Delay(PushTimeInterval);
}
}
/// <summary>
/// Handles cleanup operations when app is cancelled or unloads
/// </summary>
public static Task WhenCancelled(CancellationToken cancellationToken)
{
var tcs = new TaskCompletionSource<bool>();
cancellationToken.Register(s => ((TaskCompletionSource<bool>)s).SetResult(true), tcs);
return tcs.Task;
}
/// <summary>
/// Initializes the ModuleClient and sets up the callback to receive
/// messages containing temperature information
/// </summary>
static async Task Init()
{
_ioTHubModuleClient = await ModuleClient.CreateFromEnvironmentAsync();
_ioTHubModuleClient.SetDesiredPropertyUpdateCallbackAsync(OnDesiredPropertyChanged, null).Wait();
await SetAlertStatus("start");
Console.WriteLine("IoT Hub module client initialized.");
// Read from the module twin's desired properties
var moduleTwin = await _ioTHubModuleClient.GetTwinAsync();
await OnDesiredPropertyChanged(moduleTwin.Properties.Desired, _ioTHubModuleClient);
}
static async Task SetAlertStatus(string status)
{
Console.WriteLine($"Sending alert as reported property with value `{status}`.");
TwinCollection reportedProperties = new TwinCollection
{
["alert"] = status
};
await _ioTHubModuleClient.UpdateReportedPropertiesAsync(reportedProperties);
}
static async Task OnDesiredPropertyChanged(TwinCollection desiredProperties, object userContext)
{
try
{
Console.WriteLine("Desired property change:");
Console.WriteLine(JsonConvert.SerializeObject(desiredProperties));
if (desiredProperties["SqlConnnectionString"]!=null)
{
var connectionString = desiredProperties["SqlConnnectionString"];
Console.WriteLine($"Updating SqlConnectionString: {connectionString}");
_datastoreservice.SetSqlConnectionString($"{connectionString}");
}
if (desiredProperties["PushTimeInterval"]!=null)
{
var pushTimeInterval = desiredProperties["PushTimeInterval"];
Console.WriteLine($"Updating PushTimeInterval to: {pushTimeInterval}");
PushTimeInterval = desiredProperties["PushTimeInterval"];
}
if (desiredProperties["Alert"]!=null)
{
var alertStatus = desiredProperties["Alert"];
Console.WriteLine($"Will update alert from property to: {alertStatus}");
await SetAlertStatus($"{alertStatus}");
}
if (desiredProperties["OnnxModelUrl"]!=null)
{
var onnxModelUrl = $"{desiredProperties["OnnxModelUrl"]}";
if (!string.IsNullOrEmpty(onnxModelUrl))
{
// Blocking data generation while we run the model
BlockedDataGeneration = true;
Console.WriteLine($"Updating OnnxModelUrl: {onnxModelUrl}");
// Re-Create the Model Table in DB to keep only one record of the model
_datastoreservice.DropAndCreateModelTable();
// Insert the model into the created table
_datastoreservice.InsertModelFromUrl($"{onnxModelUrl}");
// Get model prediction of top rows
var modelResult = _datastoreservice.GetModelResult();
Console.WriteLine($"Model prediction got a value of: {modelResult}");
// Stabilize the data if result is > 0
StopWake = modelResult > 0;
if (StopWake)
{
Console.WriteLine($"Stabilizing the data.");
// Clear the buffer to accelarate stabilization of the data
ClearBuffer();
}
// If result is > 0 we need to stop the alert
var alertStatus = modelResult > 0 ? "stop":"start";
Console.WriteLine($"Setting Alert to: {alertStatus}");
// Update the twin module property to get new value in client
await SetAlertStatus(alertStatus);
// Unblock the data generation after running the model
BlockedDataGeneration = false;
}
else
{
BlockedDataGeneration = true;
StopWake = false;
Console.WriteLine($"Is data stabilized? {StopWake}");
_datastoreservice.TruncateRealtimeSensorRecordTable();
ClearBuffer();
BlockedDataGeneration = false;
}
}
else
{
BlockedDataGeneration = true;
StopWake = false;
Console.WriteLine($"Is data stabilized? {StopWake}");
_datastoreservice.TruncateRealtimeSensorRecordTable();
ClearBuffer();
BlockedDataGeneration = false;
}
}
catch (AggregateException ex)
{
foreach (Exception exception in ex.InnerExceptions)
{
Console.WriteLine();
Console.WriteLine("Error when receiving desired property: {0}", exception);
}
}
catch (Exception ex)
{
Console.WriteLine();
Console.WriteLine("Error when receiving desired property: {0}", ex);
}
}
static void ClearBuffer()
{
_datageneratorservice.ClearBuffer();
// Add new records as a reference for the next record generation
_datageneratorservice.GenerateRealTimeSensorRecords(StopWake);
_datageneratorservice.GenerateRealTimeSensorRecords(StopWake);
}
}
}
@@ -0,0 +1,35 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp3.1</TargetFramework>
<LangVersion>latest</LangVersion>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|netcoreapp3.1|AnyCPU'">
<TreatWarningsAsErrors>True</TreatWarningsAsErrors>
<TreatSpecificWarningsAsErrors />
</PropertyGroup>
<ItemGroup>
<ProjectCapability Include="AzureIoTEdgeModule"/>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Azure.Devices.Client" Version="1.*" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="3.1.2" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.2" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="3.1.2" />
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="3.1.2" />
<PackageReference Include="Microsoft.Extensions.Configuration.FileExtensions" Version="3.1.2" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="2.0.0" />
<PackageReference Include="System.Runtime.Loader" Version="4.3.0" />
<PackageReference Include="DevExpress.Xpo" Version="18.2.10" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="3.1.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer.Design" Version="1.1.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="3.1.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Parquet.Net" Version="3.6.0" />
</ItemGroup>
</Project>
@@ -0,0 +1,144 @@
using SensorModule.Models;
using SensorModule.Services.Interfaces;
using Parquet;
using Parquet.Data;
using System.Collections.Generic;
using System.IO;
using System.Linq;
namespace SensorModule.Services
{
public class DataExporterService : IDataExporterService
{
public void SaveParquet(IEnumerable<TrainingWindTurbineRecord> records, string outputFile)
{
//create data columns with schema metadata and the data you need
var turbineId = new DataColumn(
new DataField<int>("TurbineId"),
records.Select(x => x.TurbineId).ToArray());
var gearboxOilLevel = new DataColumn(
new DataField<double>("GearboxOilLevel"),
records.Select(x => x.GearboxOilLevel).ToArray());
var gearboxOilTemp = new DataColumn(
new DataField<double>("GearboxOilTemp"),
records.Select(x => x.GearboxOilTemp).ToArray());
var generatorActivePower = new DataColumn(
new DataField<double>("GeneratorActivePower"),
records.Select(x => x.GeneratorActivePower).ToArray());
var generatorSpeed = new DataColumn(
new DataField<double>("GeneratorSpeed"),
records.Select(x => x.GeneratorSpeed).ToArray());
var generatorTemp = new DataColumn(
new DataField<double>("GeneratorTemp"),
records.Select(x => x.GeneratorTemp).ToArray());
var generatorTorque = new DataColumn(
new DataField<double>("GeneratorTorque"),
records.Select(x => x.GeneratorTorque).ToArray());
var gridFrequency = new DataColumn(
new DataField<double>("GridFrequency"),
records.Select(x => x.GridFrequency).ToArray());
var gridVoltage = new DataColumn(
new DataField<double>("GridVoltage"),
records.Select(x => x.GridVoltage).ToArray());
var hydraulicOilPressure = new DataColumn(
new DataField<double>("HydraulicOilPressure"),
records.Select(x => x.HydraulicOilPressure).ToArray());
var nacelleAngle = new DataColumn(
new DataField<double>("NacelleAngle"),
records.Select(x => x.NacelleAngle).ToArray());
var overallWindDirection = new DataColumn(
new DataField<double>("OverallWindDirection"),
records.Select(x => x.OverallWindDirection).ToArray());
var overalWindSpeedStdDev = new DataColumn(
new DataField<double>("WindSpeedStdDev"),
records.Select(x => x.WindSpeedStdDev).ToArray());
var precipitation = new DataColumn(
new DataField<bool>("Precipitation"),
records.Select(x => x.Precipitation).ToArray());
var turbineWindDirection = new DataColumn(
new DataField<double>("TurbineWindDirection"),
records.Select(x => x.TurbineWindDirection).ToArray());
var turbineSpeedStdDev = new DataColumn(
new DataField<double>("TurbineSpeedStdDev"),
records.Select(x => x.TurbineSpeedStdDev).ToArray());
var windSpeedAverage = new DataColumn(
new DataField<double>("WindSpeedAverage"),
records.Select(x => x.WindSpeedAverage).ToArray());
var windTempAverage = new DataColumn(
new DataField<double>("WindTempAverage"),
records.Select(x => x.WindTempAverage).ToArray());
var alterBlades = new DataColumn(
new DataField<bool>("AlterBlades"),
records.Select(x => x.AlterBlades).ToArray());
var pitchAngle = new DataColumn(
new DataField<double>("PitchAngle"),
records.Select(x => x.PitchAngle).ToArray());
var vibration = new DataColumn(
new DataField<double>("Vibration"),
records.Select(x => x.Vibration).ToArray());
var turbineSpeedAverage = new DataColumn(
new DataField<double>("TurbineSpeedAverage"),
records.Select(x => x.TurbineSpeedAverage).ToArray());
// create file schema
var schema = new Schema(turbineId.Field, gearboxOilLevel.Field, gearboxOilTemp.Field, generatorActivePower.Field, generatorSpeed.Field, generatorTemp.Field, generatorTorque.Field, gridFrequency.Field, gridVoltage.Field, hydraulicOilPressure.Field,
nacelleAngle.Field, overallWindDirection.Field, overalWindSpeedStdDev.Field, precipitation.Field, turbineWindDirection.Field, turbineSpeedStdDev.Field, windSpeedAverage.Field, windTempAverage.Field, pitchAngle.Field, vibration.Field, turbineSpeedAverage.Field, alterBlades.Field);
var outputPath = Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location);
using (Stream fileStream = File.OpenWrite(Path.Combine(outputPath, $"{outputFile}.parquet")))
{
using (var parquetWriter = new ParquetWriter(schema, fileStream))
{
// create a new row group in the file
using (ParquetRowGroupWriter groupWriter = parquetWriter.CreateRowGroup())
{
groupWriter.WriteColumn(turbineId);
groupWriter.WriteColumn(gearboxOilLevel);
groupWriter.WriteColumn(gearboxOilTemp);
groupWriter.WriteColumn(generatorActivePower);
groupWriter.WriteColumn(generatorSpeed);
groupWriter.WriteColumn(generatorTemp);
groupWriter.WriteColumn(generatorTorque);
groupWriter.WriteColumn(gridFrequency);
groupWriter.WriteColumn(gridVoltage);
groupWriter.WriteColumn(hydraulicOilPressure);
groupWriter.WriteColumn(nacelleAngle);
groupWriter.WriteColumn(overallWindDirection);
groupWriter.WriteColumn(overalWindSpeedStdDev);
groupWriter.WriteColumn(precipitation);
groupWriter.WriteColumn(turbineWindDirection);
groupWriter.WriteColumn(turbineSpeedStdDev);
groupWriter.WriteColumn(windSpeedAverage);
groupWriter.WriteColumn(windTempAverage);
groupWriter.WriteColumn(pitchAngle);
groupWriter.WriteColumn(vibration);
groupWriter.WriteColumn(turbineSpeedAverage);
groupWriter.WriteColumn(alterBlades);
}
}
}
}
}
}
@@ -0,0 +1,215 @@
using System;
using System.Collections.Generic;
using System.Linq;
using SensorModule.DataStructures;
using SensorModule.Helpers;
using SensorModule.Models;
using SensorModule.Services.Interfaces;
namespace SensorModule.Services
{
public class DataGeneratorService : IDataGeneratorService
{
private RingBuffer<RealtimeWindTurbineRecord> ringBuffer = new RingBuffer<RealtimeWindTurbineRecord>(100);
private GeneratorHelper gen = new GeneratorHelper();
private readonly Guid TurbineGuid = Guid.NewGuid();
private RealtimeWindTurbineRecord GenerateFirstRealtimeRecord()
{
var windSpeed = gen.GetRandomNumber(15.0, 25.0);
var generatorSpeed = gen.GetGeneratorSpeed(windSpeed);
var activePower = gen.GetGeneratorActivePower(windSpeed);
var windDirection = gen.GetRandomNumber(41.0, 44.0);
var toReturn = new RealtimeWindTurbineRecord
{
RecordId = Guid.NewGuid(),
TurbineId = 15443,
TurbineGuid = TurbineGuid,
GearboxOilLevel = gen.GetGearBoxOilLevel(),
GearboxOilTemp = gen.GetGearBoxTemp(),
GeneratorActivePower = gen.GetGeneratorActivePower(windSpeed),
GeneratorSpeed = generatorSpeed,
GeneratorTemp = gen.GetGeneratorStatorTemp(generatorSpeed),
GeneratorTorque = gen.GetGeneratorTorque(generatorSpeed, activePower),
GridFrequency = gen.GetGridFrequency(activePower),
GridVoltage = gen.GetVoltage(activePower),
HydraulicOilPressure = gen.GetHydraulicOilPressure(),
NacelleAngle = gen.GetNacelleAngle(windDirection),
PitchAngle = gen.GetPitchAngle(windSpeed),
Vibration = gen.GetVibration(windSpeed),
WindSpeedAverage = windSpeed,
Precipitation = gen.GetRandomBool(20),
WindTempAverage = gen.GetRandomNumber(-10.0, 30),
OverallWindDirection = windDirection,
TurbineWindDirection = windDirection * gen.GetRandomNumber(0.97, 1.03),
TurbineSpeedAverage = windSpeed * gen.GetRandomNumber(0.97f, 1.03f),
WindSpeedStdDev = 4.885422,
TurbineSpeedStdDev = 4.978619,
Timestamp = DateTime.Now
};
ringBuffer.PushFront(toReturn);
return toReturn;
}
public void ClearBuffer()
{
ringBuffer = new RingBuffer<RealtimeWindTurbineRecord>(100);
}
private RealtimeWindTurbineRecord GenerateRegularRealtimeRecord(bool stopWake)
{
var currentRecords = ringBuffer.ToList();
double windDirection = currentRecords.Select(x => x.OverallWindDirection).Average() * gen.GetRandomNumber(0.9, 1.1);
var windSpeed = currentRecords.Select(x => x.WindSpeedAverage).Average() * gen.GetRandomNumber(0.9, 1.1);
double turbineSpeed = windSpeed * gen.GetRandomNumber(0.25, 1.75);
if (stopWake)
{
turbineSpeed = windSpeed * gen.GetRandomNumber(0.97, 1.03);
windDirection = gen.GetRandomNumber(40.0, 45.0);
}
var generatorSpeed = gen.GetGeneratorSpeed(windSpeed);
var activePower = gen.GetGeneratorActivePower(windSpeed);
var toReturn = new RealtimeWindTurbineRecord
{
RecordId = Guid.NewGuid(),
TurbineId = 15443,
TurbineGuid = TurbineGuid,
GearboxOilLevel = currentRecords.Select(x => x.GearboxOilLevel).Average() * gen.GetRandomNumber(0.95, 1.05),
GearboxOilTemp = currentRecords.Select(x => x.GearboxOilTemp).Average() * gen.GetRandomNumber(0.95, 1.05),
GeneratorActivePower = gen.GetGeneratorActivePower(windSpeed),
GeneratorSpeed = generatorSpeed,
GeneratorTemp = gen.GetGeneratorStatorTemp(generatorSpeed),
GeneratorTorque = gen.GetGeneratorTorque(generatorSpeed, activePower),
GridFrequency = gen.GetGridFrequency(activePower),
GridVoltage = gen.GetVoltage(activePower),
HydraulicOilPressure = gen.GetHydraulicOilPressure(),
NacelleAngle = gen.GetNacelleAngle(windDirection),
PitchAngle = gen.GetPitchAngle(windSpeed),
Vibration = gen.GetVibration(windSpeed),
WindSpeedAverage = windSpeed,
Precipitation = gen.GetRandomBool(20),
WindTempAverage = gen.GetRandomNumber(-10.0, 30),
OverallWindDirection = windDirection,
TurbineWindDirection = windDirection * gen.GetRandomNumber(0.97, 1.03),
TurbineSpeedAverage = turbineSpeed,
WindSpeedStdDev = gen.CalculateStandardDeviation(ringBuffer.ToList().Select(x => x.WindSpeedAverage).TakeLast(10)),
TurbineSpeedStdDev = gen.CalculateStandardDeviation(ringBuffer.ToList().Select(x => x.TurbineSpeedAverage).TakeLast(10)),
Timestamp = DateTime.Now
};
ringBuffer.PushFront(toReturn);
return toReturn;
}
public RealtimeWindTurbineRecord GenerateRealTimeRecords(bool stopWake)
{
if (ringBuffer.IsEmpty)
{
return GenerateFirstRealtimeRecord();
}
else
{
return GenerateRegularRealtimeRecord(stopWake);
}
}
public IEnumerable<TrainingWindTurbineRecord> GenerateTrainingRecords(int amountOfRecords)
{
var toReturn = new List<TrainingWindTurbineRecord>();
for (var i = 0; i < amountOfRecords; i++)
{
var windDirection = gen.GetRandomNumber(0.0, 360.0);
var windSpeed = gen.GetRandomNumber(10.0, 25.0);
var turbineSpeed = windSpeed * gen.GetRandomNumber(0.97, 1.03);
if (((double)i / amountOfRecords) >= 0.8)
{
turbineSpeed = windSpeed * gen.GetRandomNumber(0.25, 1.75);
windDirection = gen.GetRandomNumber(40.0, 45.0);
}
var generatorSpeed = gen.GetGeneratorSpeed(windSpeed);
var activePower = gen.GetGeneratorActivePower(windSpeed);
toReturn.Add(new TrainingWindTurbineRecord
{
TurbineId = i,
GearboxOilLevel = gen.GetGearBoxOilLevel(),
GearboxOilTemp = gen.GetGearBoxTemp(),
GeneratorActivePower = gen.GetGeneratorActivePower(windSpeed),
GeneratorSpeed = generatorSpeed,
GeneratorTemp = gen.GetGeneratorStatorTemp(generatorSpeed),
GeneratorTorque = gen.GetGeneratorTorque(generatorSpeed, activePower),
GridFrequency = gen.GetGridFrequency(activePower),
GridVoltage = gen.GetVoltage(activePower),
HydraulicOilPressure = gen.GetHydraulicOilPressure(),
NacelleAngle = gen.GetNacelleAngle(windDirection),
PitchAngle = gen.GetPitchAngle(windSpeed),
Vibration = gen.GetVibration(windSpeed),
WindSpeedAverage = windSpeed,
Precipitation = gen.GetRandomBool(20),
WindTempAverage = gen.GetRandomNumber(-10.0, 30),
OverallWindDirection = windDirection,
TurbineWindDirection = windDirection * gen.GetRandomNumber(0.97, 1.03),
TurbineSpeedAverage = turbineSpeed,
WindSpeedStdDev = gen.CalculateStandardDeviation(toReturn.Select(x => x.WindSpeedAverage).TakeLast(10)),
TurbineSpeedStdDev = gen.CalculateStandardDeviation(toReturn.Select(x => x.TurbineSpeedAverage).TakeLast(10)),
AlterBlades = (gen.CalculateStandardDeviation(toReturn.Select(x => x.TurbineSpeedAverage).TakeLast(10)) - gen.CalculateStandardDeviation(toReturn.Select(x => x.WindSpeedAverage).TakeLast(10))) > 1.0
});
}
return toReturn.OrderBy(a => Guid.NewGuid()).ToList();
}
public List<RealtimeSensorRecord> GenerateRealTimeSensorRecords(bool stopWake)
{
RealtimeWindTurbineRecord windTurbineRecord;
if (ringBuffer.IsEmpty)
{
windTurbineRecord = GenerateFirstRealtimeRecord();
}
else
{
windTurbineRecord = GenerateRegularRealtimeRecord(stopWake);
}
var records = Constants.SensorsList.Select(sensor =>
{
double sensorValue = sensor.Type switch
{
Constants.Sensors.WindSpeedStdDev => windTurbineRecord.WindSpeedStdDev,
Constants.Sensors.TurbineSpeedStdDev => windTurbineRecord.TurbineSpeedStdDev,
Constants.Sensors.OverallWindDirection => windTurbineRecord.OverallWindDirection,
Constants.Sensors.TurbineWindDirection => windTurbineRecord.TurbineWindDirection,
Constants.Sensors.WindSpeedAverage => windTurbineRecord.WindSpeedAverage,
Constants.Sensors.WindTempAverage => windTurbineRecord.WindTempAverage,
Constants.Sensors.GearboxOilLevel => windTurbineRecord.GearboxOilLevel,
Constants.Sensors.GearboxOilTemp => windTurbineRecord.GearboxOilTemp,
Constants.Sensors.GeneratorActivePower => windTurbineRecord.GeneratorActivePower,
Constants.Sensors.GeneratorSpeed => windTurbineRecord.GeneratorSpeed,
Constants.Sensors.GeneratorTemp => windTurbineRecord.GeneratorTemp,
Constants.Sensors.GeneratorTorque => windTurbineRecord.GeneratorTorque,
Constants.Sensors.GridFrequency => windTurbineRecord.GridFrequency,
Constants.Sensors.GridVoltage => windTurbineRecord.GridVoltage,
Constants.Sensors.HydraulicOilPressure => windTurbineRecord.HydraulicOilPressure,
Constants.Sensors.NacelleAngle => windTurbineRecord.NacelleAngle,
Constants.Sensors.PitchAngle => windTurbineRecord.PitchAngle,
Constants.Sensors.Vibration => windTurbineRecord.Vibration,
Constants.Sensors.TurbineSpeedAverage => windTurbineRecord.TurbineSpeedAverage,
Constants.Sensors.HatchSensor => 0,
_ => 0,
};
return new RealtimeSensorRecord
{
RecordId = Guid.NewGuid(),
TurbineId = windTurbineRecord.TurbineGuid,
SensorId = sensor.Id,
SensorType = sensor.Type,
SensorValue = sensorValue,
Timestamp = windTurbineRecord.Timestamp
};
});
return records.ToList();
}
}
}
@@ -0,0 +1,117 @@
using SensorModule.DataStore;
using SensorModule.Models;
using SensorModule.Services.Interfaces;
using Microsoft.Data.SqlClient;
using Microsoft.EntityFrameworkCore;
using System;
using System.Data;
using System.Net;
using System.Collections.Generic;
namespace SensorModule.Services
{
public class DataStoreService : IDataStoreService
{
private const string STOREDPROCNAME = "ModelResponse";
private const string RUNMODELSTOREDPROCNAME = "RunModel";
private const string MODELTABLENAME = "models";
private const string TRUNCATETABLESTOREDPROCNAME = "TruncateRealtimeWindTurbineRecord";
private const string TRUNCATEREALTIMESENSORTABLESTOREDPROCNAME = "TruncateRealtimeSensorRecords";
private string _sqlConnectionString;
public DataStoreService()
{
}
public void SetSqlConnectionString(string sqlConnectionString)
{
_sqlConnectionString = sqlConnectionString;
}
public bool ModelResponse()
{
SqlParameter[] @params =
{
new SqlParameter("@returnVal", SqlDbType.Bit) {Direction = ParameterDirection.Output}
};
using (var dbContext = new DatabaseContext(_sqlConnectionString))
{
dbContext.Database.ExecuteSqlRaw($"exec @returnVal=" + STOREDPROCNAME, @params);
}
return (bool)@params[0].Value;
}
public void TruncateRealtimeWindTurbineRecordTable()
{
using (var dbContext = new DatabaseContext(_sqlConnectionString))
{
dbContext.Database.ExecuteSqlRaw($"exec " + TRUNCATETABLESTOREDPROCNAME);
}
}
public void WriteToDB(RealtimeWindTurbineRecord record)
{
using (var db = new DatabaseContext(_sqlConnectionString))
{
record.Timestamp = DateTime.Now;
db.Add(record);
db.SaveChanges();
}
}
public void DropAndCreateModelTable()
{
using var dbContext = new DatabaseContext(_sqlConnectionString);
dbContext.Database.ExecuteSqlRaw($"drop table if exists {MODELTABLENAME}");
dbContext.Database.ExecuteSqlRaw($"create table {MODELTABLENAME} ([id] [int] IDENTITY(1,1) NOT NULL, [data] [varbinary](max) NULL, [description] varchar(1000))");
}
public void InsertModelFromUrl(string url)
{
using var webClient = new WebClient();
byte[] modelBytes = webClient.DownloadData(url);
using var dbContext = new DatabaseContext(_sqlConnectionString);
var query = $"insert into {MODELTABLENAME} ([description], [data]) values ('Onnx Model',?)";
var model = new OnnxModel { Description = "Onnx Model", Data = modelBytes };
dbContext.Add<OnnxModel>(model);
dbContext.SaveChanges();
}
public int GetModelResult()
{
SqlParameter[] @params =
{
new SqlParameter("@Result", SqlDbType.Int) {Direction = ParameterDirection.Output}
};
using (var dbContext = new DatabaseContext(_sqlConnectionString))
{
dbContext.Database.ExecuteSqlRaw($"EXEC " + RUNMODELSTOREDPROCNAME + " @Result OUTPUT", @params);
}
return (int)@params[0].Value;
}
public void TruncateRealtimeSensorRecordTable()
{
using (var dbContext = new DatabaseContext(_sqlConnectionString))
{
dbContext.Database.ExecuteSqlRaw($"exec " + TRUNCATEREALTIMESENSORTABLESTOREDPROCNAME);
}
}
public void WriteSensorRecordsToDB(List<RealtimeSensorRecord> records)
{
using (var dbContext = new DatabaseContext(_sqlConnectionString))
{
records.ForEach(record =>
{
dbContext.Add(record);
});
dbContext.SaveChanges();
}
}
}
}
@@ -0,0 +1,10 @@
using SensorModule.Models;
using System.Collections.Generic;
namespace SensorModule.Services.Interfaces
{
public interface IDataExporterService
{
void SaveParquet(IEnumerable<TrainingWindTurbineRecord> records, string outputFile);
}
}
@@ -0,0 +1,15 @@
using System.Collections.Generic;
using SensorModule.Models;
namespace SensorModule.Services.Interfaces
{
public interface IDataGeneratorService
{
IEnumerable<TrainingWindTurbineRecord> GenerateTrainingRecords(int amountOfRecords);
RealtimeWindTurbineRecord GenerateRealTimeRecords(bool stopWake);
List<RealtimeSensorRecord> GenerateRealTimeSensorRecords(bool stopWake);
void ClearBuffer();
}
}
@@ -0,0 +1,18 @@
using System.Collections.Generic;
using SensorModule.Models;
namespace SensorModule.Services.Interfaces
{
public interface IDataStoreService
{
void WriteToDB(RealtimeWindTurbineRecord record);
bool ModelResponse();
void TruncateRealtimeWindTurbineRecordTable();
void SetSqlConnectionString(string sqlConnectionString);
void DropAndCreateModelTable();
void InsertModelFromUrl(string url);
int GetModelResult();
void TruncateRealtimeSensorRecordTable();
void WriteSensorRecordsToDB(List<RealtimeSensorRecord> records);
}
}
@@ -0,0 +1,20 @@
{
"$schema-version": "0.0.1",
"description": "",
"image": {
"repository": "$CONTAINER_REGISTRY_NAME/sensormodule",
"tag": {
"version": "0.0.24",
"platforms": {
"amd64": "./Dockerfile.amd64",
"amd64.debug": "./Dockerfile.amd64.debug",
"arm32v7": "./Dockerfile.arm32v7",
"arm32v7.debug": "./Dockerfile.arm32v7.debug",
"windows-amd64": "./Dockerfile.windows-amd64"
}
},
"buildOptions": [],
"contextPath": "./"
},
"language": "csharp"
}
@@ -0,0 +1,8 @@
{
"folders": [
{
"path": "."
}
],
"settings": {}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

@@ -0,0 +1,33 @@
from azureml.core import Run
from sklearn.compose import ColumnTransformer
from sklearn.pipeline import Pipeline
from sklearn.linear_model import LogisticRegression
from sklearn.preprocessing import StandardScaler
from utils import *
# Fetch current run
run = Run.get_context()
# Fetch dataset from the run by name
dataset = run.input_datasets['training']
# Convert dataset to Pandas data frame
X_train, X_test, y_train, y_test = split_dataset(dataset)
# Setup scikit-learn pipeline
numeric_transformer = Pipeline(steps=[('scaler', StandardScaler())])
preprocessor = ColumnTransformer(transformers=[('num', numeric_transformer, list(X_train.columns.values))])
clf = Pipeline(steps=[('preprocessor', preprocessor),
('classifier', LogisticRegression())])
model = clf.fit(X_train, y_train)
# Analyze model performance
analyze_model(clf, X_test, y_test)
# Save model
model_id = save_model(clf)
@@ -0,0 +1,91 @@
from azureml.core import Dataset, Model, Run
from azureml.contrib.interpret.explanation.explanation_client import ExplanationClient
from interpret.ext.blackbox import TabularExplainer
from sklearn.preprocessing import LabelEncoder
from sklearn.metrics import accuracy_score, confusion_matrix, f1_score, precision_score, recall_score, roc_auc_score, roc_curve
from sklearn.model_selection import train_test_split
import joblib
import matplotlib.pyplot as plt
import numpy as np
import os
import pandas as pd
import seaborn as sns
import sklearn
OUTPUT_DIR = './outputs/'
def split_dataset(dataset):
os.makedirs(OUTPUT_DIR, exist_ok=True)
train_data = dataset.to_pandas_dataframe()
train_data = train_data.drop(columns=['TurbineId','Precipitation'])
le = LabelEncoder()
train_data['AlterBlades'] = le.fit_transform(train_data['AlterBlades'])
for x in train_data:
train_data[x] = pd.to_numeric(train_data[x])
y = train_data['AlterBlades'].values.flatten()
X = train_data.drop(['AlterBlades'], axis=1)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2)
return X_train, X_test, y_train, y_test
def analyze_model(clf, X_test, y_test):
run = Run.get_context()
preds = clf.predict(X_test)
accuracy = accuracy_score(y_test, preds)
run.log('Accuracy', np.float(accuracy))
precision = precision_score(y_test, preds, average="macro")
run.log('Precision', np.float(precision))
recall = recall_score(y_test, preds, average="macro")
run.log('Recall', np.float(recall))
f1score = f1_score(y_test, preds, average="macro")
run.log('F1 Score', np.float(f1score))
class_names = clf.classes_
fig, ax = plt.subplots()
tick_marks = np.arange(len(class_names))
plt.xticks(tick_marks, class_names)
plt.yticks(tick_marks, class_names)
sns.heatmap(pd.DataFrame(confusion_matrix(y_test, preds)), annot=True, cmap='YlGnBu', fmt='g')
ax.xaxis.set_label_position('top')
plt.tight_layout()
plt.title('Confusion Matrix', y=1.1)
plt.ylabel('Actual label')
plt.xlabel('Predicted label')
run.log_image('Confusion Matrix', plot=plt)
plt.close()
preds_proba = clf.predict_proba(X_test)[::,1]
fpr, tpr, _ = roc_curve(y_test, preds_proba, pos_label = clf.classes_[1])
auc = roc_auc_score(y_test, preds_proba)
plt.plot(fpr, tpr, label="data 1, auc=" + str(auc))
plt.legend(loc=4)
run.log_image('ROC Curve', plot=plt)
plt.close()
def save_model(clf):
run = Run.get_context()
dataset = run.input_datasets['training']
# Save model in the outputs folder
model_file_name = 'model.joblib'
joblib.dump(value=clf, filename=os.path.join(OUTPUT_DIR, model_file_name))
run.upload_file(model_file_name, os.path.join(OUTPUT_DIR, model_file_name))
# Register the model
registered_model = run.register_model(model_name='wind_turbine_model',
model_path=model_file_name,
model_framework=Model.Framework.SCIKITLEARN,
model_framework_version=sklearn.__version__,
datasets=[(Dataset.Scenario.TRAINING, dataset)])
return registered_model.id
@@ -0,0 +1,26 @@
from skl2onnx.common.data_types import FloatTensorType, Int64TensorType, DoubleTensorType
import joblib
import pandas as pd
def download_model(run):
run.download_file('model.joblib')
model = joblib.load('model.joblib')
return model
def convert_dataframe_schema(df, drop=None, batch_axis=False):
inputs = []
nrows = None if batch_axis else 1
for k, v in zip(df.columns, df.dtypes):
if drop is not None and k in drop:
continue
if v == 'int64':
t = Int64TensorType([nrows, 1])
elif v == 'float32':
t = FloatTensorType([nrows, 1])
elif v == 'float64':
t = FloatTensorType([nrows, 1])
else:
raise Exception("Bad type")
inputs.append((k, t))
return inputs
@@ -0,0 +1,616 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Wind Turbine: Azure ML with scikit-learn\n",
"\n",
"In this notebook, we'll build and analyze a new model to predict wind turbine wake winds.\n",
"\n",
"It is important to consider the two main conditions that influence the presence of wind wake:\n",
"1. Overall wind farm direction and turbine wind direction are both are between 40° - 45° degrees.\n",
"1. High difference that's greater than one minute between `TurbineSpeedStdDev` and `WindSpeedStdDev`.\n",
"\n",
"The above conditions are well known features to predict when `Wind Wake` is affecting the wind turbine."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Instructions\n",
"\n",
"Before you begin with this lab, please make sure to follow the steps below:\n",
"1. Locate the default datastore for the workspace, this can be done by authenticating against the workspace (cell #2) and execute the following command: `ws.get_default_datastore()`\n",
"1. Locate the dataset parquet file in the lab materials: `TrainingDataset.parquet`\n",
"1. Upload the dataset for this lab to the default datastore for the workspace. You can do this via Azure Portal or via Microsoft Azure Storage Explorer.\n",
"1. Ensure you have the correct version of `scikit-learn` and `joblib` installed. To install these dependencies, you can execute the cell below, skip this step if the dependencies are already installed.\n",
"1. Restart your kernel"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"!pip install scikit-learn==0.22.1 joblib==0.14.1"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Setup Azure ML\n",
"\n",
"In the next cell, we will create a new Workspace config object using the `<subscription_id>`, `<resource_group_name>`, and `<workspace_name>`. This will fetch the matching Workspace and prompt you for authentication. Please click on the link and input the provided details.\n",
"\n",
"For more information on **Workspace**, please visit: [Microsoft Workspace Documentation](https://docs.microsoft.com/en-us/python/api/azureml-core/azureml.core.workspace.workspace?view=azure-ml-py)\n",
"\n",
"`<subscription_id>` = You can get this ID from the landing page of your Resource Group.\n",
"\n",
"`<resource_group_name>` = This is the name of your Resource Group.\n",
"\n",
"`<workspace_name>` = This is the name of your Workspace."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"from azureml.core.workspace import Workspace\n",
"from azureml.core.authentication import InteractiveLoginAuthentication\n",
"project_folder = './scripts'\n",
"\n",
"try: \n",
" interactive_auth = InteractiveLoginAuthentication(tenant_id=\"<tenant_id>\")\n",
" # Get instance of the Workspace and write it to config file\n",
" ws = Workspace(\n",
" subscription_id = '<subscription_id>', \n",
" resource_group = '<resource_group>', \n",
" workspace_name = '<workspace_name>',\n",
" auth = interactive_auth)\n",
"\n",
" # Writes workspace config file\n",
" ws.write_config()\n",
" \n",
" print('Library configuration succeeded')\n",
"except Exception as e:\n",
" print(e)\n",
" print('Workspace not found')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Fetch our data\n",
"\n",
"Let's retrieve our dataset from the default workspace Datastore."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"from azureml.core import Dataset\n",
"from azureml.data.datapath import DataPath\n",
"from azureml.core import Datastore\n",
"\n",
"datastore = ws.get_default_datastore()\n",
"\n",
"datastore_path = [DataPath(datastore, '*.parquet')]\n",
"\n",
"tabular = Dataset.Tabular.from_parquet_files(path=datastore_path)\n",
"tabular = tabular.register(workspace=ws, \n",
" name='wind_turbine_training', \n",
" create_new_version=True)\n",
"tabular = Dataset.get_by_name(ws, name='wind_turbine_training')\n",
"print(tabular.version)\n",
"data = tabular.to_pandas_dataframe()\n",
"data.head(5)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Next, we'll take a subset of our data and then proceed to visualize it to better understand any patterns and trends that might exist to drive good ML models."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"subset = tabular.take_sample(probability=0.4, seed=123).to_pandas_dataframe()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Dataset Description\n",
"\n",
"Describe our current dataset. The table below shows the different statistical values for our training subset."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"subset.describe()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Turbine Wind Direction\n",
"\n",
"Let's take a look at the Turbine Wind Direction distribution against the Wind Direction Angle. As we can see, we have a considerable alteration between 40° and 50° degrees."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import seaborn as sns\n",
"import matplotlib.pyplot as plt\n",
"from IPython.display import display\n",
"\n",
"hstyle={\"rwidth\":0.75,'edgecolor':'black'}\n",
"\n",
"# Analyze distribution of TurbineWindDirection in the dataset\n",
"fig, ax = plt.subplots()\n",
"sns.distplot(subset[['TurbineWindDirection']], ax=ax, \n",
" hist_kws=hstyle).set_title(\"Turbine Wind Direction Distribuition\")\n",
"ax.set_xlim(0,360)\n",
"ax.set(xlabel=\"Wind Direction Angle\")\n",
"plt.show()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Turbine Wind Direction vs Alter Blades\n",
"\n",
"Let's take a look at how our training dataset conducts for `Alter Blades` against the `Wind Direction Angle`. It is very clear that between 40° and 50° degrees we have a clear spike of `True` values for `Alter Blades`. Keep in mind, that the target column for our prediction is `Alter Blades`, this column will enable us to identify a wake condition."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"g = sns.FacetGrid(subset, col='AlterBlades')\n",
"g.map(sns.distplot, 'TurbineWindDirection', hist_kws=hstyle)\n",
"g.set(xlabel=\"Wind Direction Angle\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Turbine Speed\n",
"\n",
"Let's take a look at the Turbine Speed distribution. In the chart, we can observe the distribution has values between 10 and 25 km/h."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"fig, ax = plt.subplots()\n",
"sns.distplot(subset[['TurbineSpeedAverage']], ax=ax, \n",
" hist_kws=hstyle).set_title(\"Average Turbine Speed Distribuition\")\n",
"ax.set(xlabel=\"Average Turbine Speed\")\n",
"plt.show()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Turbine Speed Standard Deviation vs Alter Blades\n",
"\n",
"Let's take a look at how our training dataset behaves for `Alter Blades` against the `Turbine Speed Standard Deviation`. "
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Analyze how age influences whether customers have responded to insurance campaigns\n",
"g = sns.FacetGrid(subset, col='AlterBlades')\n",
"g.map(sns.distplot, 'TurbineSpeedStdDev', hist_kws=hstyle)\n",
"g.set(xlabel=\"Turbine Speed Std Dev\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Wind Speed\n",
"\n",
"Let's take a look at the Wind Speed distribution. In the chart, we can observe the distribution has values between 10 and 25 km/h."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"fig, ax = plt.subplots()\n",
"sns.distplot(subset[['WindSpeedAverage']], ax=ax, \n",
" hist_kws=hstyle).set_title(\"Average Wind Speed Distribuition\")\n",
"ax.set(xlabel=\"Average Wind Speed\")\n",
"plt.show()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Wind Speed Standard Deviation vs Alter Blades\n",
"\n",
"Let's take a look at how our training dataset behaves for `Alter Blades` against the `Turbine Speed Standard Deviation`."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Analyze how age influences whether customers have responded to insurance campaigns\n",
"g = sns.FacetGrid(subset, col='AlterBlades')\n",
"g.map(sns.distplot, 'WindSpeedStdDev', hist_kws=hstyle)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Isolate AlterBlades rows true values\n",
"\n",
"Let's create a Facet Grid to understand the trends that our `True` values from the `Alter Blades` column has against other features in the dataset such as:\n",
"1. Turbine Speed Standard Deviation\n",
"1. Turbine Wind Direction\n",
"1. Wind Speed Standard Deviation\n",
"\n",
"As we are able to see, when `Turbine Wind Direction` is around 40° to 45° degrees, it is a very good indication for an `Alter Blades: True` value. Also, we are able to see that high `Turbine Speed Standard Deviation` versus a low `Wind Speed Standard Deviation` are also key features to achieve a `True` value in the `Alter Blades` column"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"alterBlades = subset.loc[subset.AlterBlades]\n",
"g = sns.FacetGrid(alterBlades, col='AlterBlades')\n",
"g.map(plt.hist, 'TurbineSpeedStdDev')\n",
"g.set(xlabel=\"Turbine Speed Std Dev\")\n",
"\n",
"display(g)\n",
"\n",
"g = sns.FacetGrid(alterBlades, col='AlterBlades')\n",
"g.map(plt.hist, 'TurbineWindDirection')\n",
"g.set(xlabel=\"Turbine Wind Direction Angle\")\n",
"\n",
"display(g)\n",
"\n",
"g = sns.FacetGrid(alterBlades, col='AlterBlades')\n",
"g.map(plt.hist, 'WindSpeedStdDev')\n",
"g.set(xlabel=\"Wind Speed Std Dev\")\n",
"\n",
"display(g)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Pairplot Wind Speed Std Dev, Turbine Speed Std Dev and Alter Blades\n",
"\n",
"Let's place our key features in a Pair plot to analyze their trends."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Analyze how age and category gardening spend is influenced by age and region\n",
"sns.pairplot(subset, vars=['WindSpeedStdDev', 'TurbineSpeedStdDev'], hue='AlterBlades')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Create experiment\n",
"\n",
"In our script, there are three distinct sections:\n",
"1. Setting up the scikit-learn logistic regression model pipeline (including encoding our features).\n",
"1. Analyzing and logging the results of the model training.\n",
"1. Running the model explainer to understand the key model drivers."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"%%writefile $project_folder/train.py\n",
"\n",
"from azureml.core import Run\n",
"\n",
"from sklearn.compose import ColumnTransformer\n",
"from sklearn.pipeline import Pipeline\n",
"from sklearn.linear_model import LogisticRegression\n",
"from sklearn.preprocessing import StandardScaler\n",
"\n",
"from utils import *\n",
"\n",
"# Fetch current run\n",
"run = Run.get_context()\n",
" \n",
"# Fetch dataset from the run by name\n",
"dataset = run.input_datasets['training']\n",
"\n",
"# Convert dataset to Pandas data frame\n",
"X_train, X_test, y_train, y_test = split_dataset(dataset)\n",
"\n",
"# Setup scikit-learn pipeline\n",
"numeric_transformer = Pipeline(steps=[('scaler', StandardScaler())])\n",
"preprocessor = ColumnTransformer(transformers=[('num', numeric_transformer, list(X_train.columns.values))])\n",
"\n",
"clf = Pipeline(steps=[('preprocessor', preprocessor),\n",
" ('classifier', LogisticRegression())])\n",
"\n",
"model = clf.fit(X_train, y_train)\n",
"\n",
"# Analyze model performance\n",
"analyze_model(clf, X_test, y_test)\n",
"\n",
"# Save model\n",
"model_id = save_model(clf)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Create a Workspace Experiment\n",
"\n",
"The Experiment constructor allows to create an experiment instance. The constructor takes in the current workspace, which is fetched by calling `Workspace.from_config()` and an experiment name. \n",
"\n",
"For more information on **Experiment**, please visit: [Microsoft Experiment Documentation](https://docs.microsoft.com/en-us/python/api/azureml-core/azureml.core.experiment.experiment?view=azure-ml-py)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"from azureml.core.experiment import Experiment\n",
"\n",
"# Get an instance of the Workspace from the config file\n",
"ws = Workspace.from_config()\n",
"\n",
"experiment_name = 'wake-detection-experiment'\n",
"\n",
"# Create Experiment\n",
"experiment = Experiment(ws, experiment_name)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Create Automated ML Compute cluster\n",
"\n",
"Firstly, check for the existence of the cluster. If it already exists, we are able to reuse it. Checking for the existence of the cluster can be performed by calling the constructor `ComputeTarget()` with the current workspace and name of the cluster.\n",
"\n",
"In case the cluster does not exist, the next step will be to provide a configuration for the new AML cluster by calling the function `AmlCompute.provisioning_configuration()`. It takes as parameters the VM size and the max number of nodes that the cluster can scale up to. After the configuration has executed, `ComputeTarget.create()` should be called with the previously configuration object and the workspace object.\n",
"\n",
"For more information on **ComputeTarget**, please visit: [Microsoft get_data Documentation](https://docs.microsoft.com/en-us/python/api/azureml-core/azureml.core.compute.computetarget?view=azure-ml-py)\n",
"\n",
"For more information on **AmlCompute**, please visit: [Microsoft get_data Documentation](https://docs.microsoft.com/en-us/python/api/azureml-core/azureml.core.compute.akscompute?view=azure-ml-py)\n",
"\n",
"\n",
"**Note:** Please wait for the execution of the cell to finish before moving forward."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"from azureml.core.compute import ComputeTarget, AmlCompute\n",
"from azureml.core.compute_target import ComputeTargetException\n",
"\n",
"# Create AML CPU Compute Cluster\n",
"try:\n",
" compute_target = ComputeTarget(workspace=ws, name='cpucluster')\n",
" print('Found existing compute target.')\n",
"except ComputeTargetException:\n",
" print('Creating a new compute target...')\n",
" compute_config = AmlCompute.provisioning_configuration(vm_size='Standard_DS12_v2',\n",
" max_nodes=4)\n",
"\n",
" compute_target = ComputeTarget.create(ws, 'cpucluster', compute_config)\n",
" compute_target.wait_for_completion(show_output=True)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Submit Experiment\n",
"\n",
"We'll use remote compute for this job. We need to install a couple of extra libraries, including those required for model interpretability.\n",
"\n",
"The `experiment.submit()` function is called to send the experiment for execution. The only parameter received by this function is the `Estimator` object."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"from azureml.train.sklearn import SKLearn\n",
"\n",
"estimator = SKLearn(source_directory=project_folder,\n",
" compute_target=compute_target,\n",
" entry_script='train.py',\n",
" inputs=[tabular.as_named_input('training')],\n",
" pip_packages=['azureml-dataprep[fuse,pandas]','joblib==0.14.1','azureml-interpret','azureml-contrib-interpret','matplotlib','scikit-learn==0.22.1','seaborn'])\n",
"\n",
"run = experiment.submit(estimator)\n",
"run"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Monitor Experiment\n",
"\n",
"The creation of an object of type `Run` will enable us to observe the experiments progress and results. The object is created by calling the constructor `Run()`. It takes, as arguments, the experiment and the identifier of the run to fetch. After the object has been instantiated, the `RunDetails()` function will retrieve the progress, metrics, and tasks for the specified run. They will be displayed by calling the function `show()` over the mentioned object.\n",
"\n",
"**Note:** Please wait for the execution of the cell to finish before moving forward. (Status should be **Completed**)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"from azureml.core import Run\n",
"from azureml.widgets import RunDetails\n",
"\n",
"run = Run(experiment, run.id)\n",
"RunDetails(run).show()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Encode dataset and download trained model\n",
"\n",
"First step is to encode our training data to take the shape expected by the Onnx converter. Next, download the model obtained from the best run. In order to download the model, the function `download_model()` should be called. This will take care of downloading the model obtained from the best run."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"from utils import *\n",
"from scripts.utils import *\n",
"\n",
"# Convert dataset to Pandas data frame\n",
"X_train, X_test, y_train, y_test = split_dataset(tabular)\n",
"model = download_model(run)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Convert model to Onnx format\n",
"\n",
"Export the Sklearn model to Onnx format by using `skl2onnx`. This step will output an Onnx model that we will be able to publish to the Azure SQL Edge Database Instance to use along with our `PREDICT` statement. "
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import skl2onnx\n",
"import onnxmltools\n",
"\n",
"# Convert the scikit model to onnx format\n",
"onnx_model = skl2onnx.convert_sklearn(model, 'Wind Turbine Dataset', convert_dataframe_schema(X_train))\n",
"# Save the onnx model locally\n",
"onnx_model_path = 'windturbinewake.model.onnx'\n",
"onnxmltools.utils.save_model(onnx_model, onnx_model_path)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Save model to Azure Blob Storage\n",
"\n",
"Let's save our Onnx model to the default workspace Datastore."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"datastore.upload_files(files=[onnx_model_path],\n",
" overwrite=True,\n",
" show_progress=True)"
]
}
],
"metadata": {
"kernel_info": {
"name": "python3"
},
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.6.9"
},
"nteract": {
"version": "nteract-front-end@1.0.0"
}
},
"nbformat": 4,
"nbformat_minor": 2
}
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<RuleSet Name="SmartRetail" Description=" " ToolsVersion="10.0">
<Rules AnalyzerId="StyleCop.Analyzers" RuleNamespace="StyleCop.Analyzers">
<Rule Id="SA1101" Action="None" />
<Rule Id="SA1133" Action="None" />
<Rule Id="SA1200" Action="None" />
<Rule Id="SA1309" Action="None" />
<Rule Id="SA1402" Action="None" />
<Rule Id="SA1611" Action="None" />
<Rule Id="SA1615" Action="None" />
<Rule Id="SA1633" Action="None" />
<Rule Id="SA1649" Action="None" />
<Rule Id="SA1652" Action="None" />
<Rule Id="SA1600" Action="None" />
<Rule Id="SA0001" Action="None" />
<Rule Id="SA1413" Action="None" />
</Rules>
<Rules AnalyzerId="Microsoft.Usage" RuleNamespace="Microsoft.Usage">
<Rule Id="CA2235" Action="None" />
<Rule Id="CA2007" Action="None" />
<Rule Id="CA1031" Action="None" />
</Rules>
</RuleSet>
@@ -0,0 +1,232 @@
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
# User-specific files
*.suo
*.user
*.userosscache
*.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
build/
bld/
bin/
Bin/
obj/
Obj/
# Visual Studio 2015 cache/options directory
.vs/
/wwwroot/dist/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUNIT
*.VisualState.xml
TestResult.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
*_i.c
*_p.c
*_i.h
*.ilk
*.meta
*.obj
*.pch
*.pdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*.log
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# JustCode is a .NET coding add-in
.JustCode
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# TODO: Comment the next line if you want to checkin your web deploy settings
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj
# NuGet Packages
*.nupkg
# The packages folder can be ignored because of Package Restore
**/packages/*
# except build/, which is used as an MSBuild target.
!**/packages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/packages/repositories.config
# Microsoft Azure Build Output
csx/
*.build.csdef
# Microsoft Azure Emulator
ecf/
rcf/
# Microsoft Azure ApplicationInsights config file
ApplicationInsights.config
# Windows Store app package directory
AppPackages/
BundleArtifacts/
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!*.[Cc]ache/
# Others
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.pfx
*.publishsettings
orleans.codegen.cs
/node_modules
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
# SQL Server files
*.mdf
*.ldf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
# Microsoft Fakes
FakesAssemblies/
# GhostDoc plugin setting file
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions
# Paket dependency manager
.paket/paket.exe
# FAKE - F# Make
.fake/
@@ -0,0 +1,23 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
@@ -0,0 +1,68 @@
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
## Available Scripts
In the project directory, you can run:
### `npm start`
Runs the app in the development mode.<br />
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
The page will reload if you make edits.<br />
You will also see any lint errors in the console.
### `npm test`
Launches the test runner in the interactive watch mode.<br />
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
### `npm run build`
Builds the app for production to the `build` folder.<br />
It correctly bundles React in production mode and optimizes the build for the best performance.
The build is minified and the filenames include the hashes.<br />
Your app is ready to be deployed!
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
### `npm run eject`
**Note: this is a one-way operation. Once you `eject`, you cant go back!**
If you arent satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point youre on your own.
You dont have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldnt feel obligated to use this feature. However we understand that this tool wouldnt be useful if you couldnt customize it when you are ready for it.
## Learn More
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
To learn React, check out the [React documentation](https://reactjs.org/).
### Code Splitting
This section has moved here: https://facebook.github.io/create-react-app/docs/code-splitting
### Analyzing the Bundle Size
This section has moved here: https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size
### Making a Progressive Web App
This section has moved here: https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app
### Advanced Configuration
This section has moved here: https://facebook.github.io/create-react-app/docs/advanced-configuration
### Deployment
This section has moved here: https://facebook.github.io/create-react-app/docs/deployment
### `npm run build` fails to minify
This section has moved here: https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify
@@ -0,0 +1,35 @@
{
"name": "azure-maps-demo",
"version": "0.1.0",
"private": true,
"dependencies": {
"node-sass": "^4.13.0",
"normalize.css": "^8.0.1",
"react": "^16.11.0",
"react-dom": "^16.11.0",
"react-inlinesvg": "^1.2.0",
"react-router-dom": "^5.1.2",
"react-scripts": "3.2.0"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": "react-app"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}
@@ -0,0 +1,43 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<link rel="apple-touch-icon" href="logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>Contoso Renewable Energy</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>
@@ -0,0 +1,25 @@
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}
@@ -0,0 +1,2 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
@@ -0,0 +1,56 @@
// node_modules
import React, { Component } from 'react'
import { BrowserRouter as Router, Route, Redirect } from "react-router-dom";
import "normalize.css";
import "./styles/global.scss";
// local components
import { Layout } from "./components/Layout";
import { Home } from "./pages/Home";
import Alerts from './pages/Alerts';
export default class App extends Component {
state = {
selected: 'dashboard',
showAlert: false,
showToast: false
}
componentDidMount() {
this.pollForAlert();
}
componentWillUnmount() {
clearInterval(this.interval);
}
pollForAlert = () => {
this.interval = setInterval(()=> {
fetch('/api/device').then(res => res.json()).then(json => {
const showToast = (this.state.showAlert && json.alert === false) || (this.state.showToast && json.alert === false);
this.setState({ showAlert: json.alert ? json.alert : false, showToast });
}).catch();
}, 1000);
}
onSelect = (selected) => {
this.setState({ selected });
}
setShowToast = (showToast) => {
this.setState({ showToast });
}
render() {
const { selected, showAlert, showToast } = this.state;
return (
<Router>
<Layout selected={selected} showToast={showToast} setShowToast={this.setShowToast}>
<Route path="/" exact render={() => <Redirect to="/dashboard" />}/>
<Route path="/dashboard" exact render={() => <Home onSelect={this.onSelect} showAlert={showAlert} />} />
<Route path="/alerts" exact render={() => <Alerts onSelect={this.onSelect} />} />
</Layout>
</Router>
)
}
}
@@ -0,0 +1,4 @@
export const getForecasts = async () => {
const forecastsResponse = await fetch("api/SampleData/WeatherForecasts");
return await forecastsResponse.json();
};
@@ -0,0 +1,3 @@
# Shared Assets folder
This folder is for shared assets that are imported into components.
@@ -0,0 +1,82 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="402.5" height="181" viewBox="0 0 402.5 181">
<defs>
<clipPath id="a">
<rect width="384" height="146" style="fill:none"/>
</clipPath>
</defs>
<g transform="translate(-1326.5 -483)">
<path d="M3346,487.094h358" transform="translate(-1975 75)" style="stroke-width:2px;stroke:#707070;fill:none"/>
<path d="M1-121.863H31.379l30.379-12,37.024,13,59.808,7.5,20.269-8.5,42.387,16,26.581-7.5,29.429,7.5,41.967-18.1L357-117.934V-10H1V-120.5H1" transform="translate(1372 655)" style="fill-rule:evenodd;fill:none"/>
<path d="M-1.909,24.285H58.424l30.892,3.687,28.752-3.687,31.782,2.564,33.14,1.123,27.939-2.934,32.381-.753,30.276,1.229,27.758-3,30.73,1.771,23.479,1" transform="translate(1373 531)" style="stroke:#00c3ff;stroke-linejoin:round;stroke-width:2px;fill:none"/>
<g transform="translate(900 244)">
<text class="e" transform="translate(466 385)">
<tspan x="1.895" y="11">-5min</tspan>
</text>
<text class="e" transform="translate(533 385)">
<tspan x="1.785" y="11">-4min</tspan>
</text>
<text class="e" transform="translate(599 385)">
<tspan x="1.895" y="11">-3min</tspan>
</text>
<text class="e" transform="translate(666 385)">
<tspan x="1.895" y="11">-2min</tspan>
</text>
<text class="e" transform="translate(732 385)">
<tspan x="2.654" y="11">-1min</tspan>
</text>
<text class="e" transform="translate(799 385)">
<tspan x="1.895" y="11">-0min</tspan>
</text>
</g>
<text class="f" transform="translate(1515 650)">
<tspan x="21.719" y="11">Time</tspan>
</text>
<text class="f" transform="translate(1326.5 590.5) rotate(-90)">
<tspan x="13.313" y="11">Temp C</tspan>
</text>
<g transform="translate(1345 483)" style="clip-path:url(#a)">
<g transform="translate(-445 -370)">
<rect class="h" width="358" height="1" transform="translate(471 377)"/>
<text class="e" transform="translate(445 370)">
<tspan x="5.313" y="11">150</tspan>
</text>
</g>
<g transform="translate(-445 -348)">
<rect class="h" width="358" height="1" transform="translate(471 377)"/>
<text class="e" transform="translate(445 370)">
<tspan x="5.313" y="11">125</tspan>
</text>
</g>
<g transform="translate(-445 -326)">
<rect class="h" width="358" height="1" transform="translate(471 377)"/>
<text class="e" transform="translate(445 370)">
<tspan x="5.313" y="11">100</tspan>
</text>
</g>
<g transform="translate(-445 -304)">
<rect class="h" width="358" height="1" transform="translate(471 377)"/>
<text class="e" transform="translate(445 370)">
<tspan x="9.483" y="11">75</tspan>
</text>
</g>
<g transform="translate(-445 -282)">
<rect class="h" width="358" height="1" transform="translate(471 377)"/>
<text class="e" transform="translate(445 370)">
<tspan x="9.263" y="11">50</tspan>
</text>
</g>
<g transform="translate(-445 -260)">
<rect class="h" width="358" height="1" transform="translate(471 377)"/>
<text class="e" transform="translate(445 370)">
<tspan x="9.263" y="11">25</tspan>
</text>
</g>
<g transform="translate(-445 -238)">
<rect class="h" width="358" height="1" transform="translate(471 377)"/>
<text class="e" transform="translate(445 370)">
<tspan x="14.731" y="11">0</tspan>
</text>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.1 KiB

@@ -0,0 +1,74 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="402.5" height="181" viewBox="0 0 402.5 181">
<defs>
<clipPath id="a">
<rect width="384" height="145" style="fill:none"/>
</clipPath>
</defs>
<g transform="translate(-426.5 -239)">
<path d="M3346,487.094h358" transform="translate(-2875 -215)" style="stroke-width:2px;stroke:#707070;fill:none"/>
<path d="M1-121.863H31.379l30.379-12,37.024,13,59.808,7.5,20.269-8.5,42.387,16,26.581-7.5,29.429,7.5,41.967-18.1L357-117.934V-10H1V-120.5H1" transform="translate(472 411)" style="fill-rule:evenodd;fill:none"/>
<path d="M-1.909,24.285,28.076,36.514,58.424,24.285l30.892,3.687,29.953,10.665,30.581-6.788,30.425,6.788,30.654-4.6,32.381-9.753,28.319,15.753,27.655-1.4,30.589,12.706,23.473,42.77" transform="translate(473 243)" style="stroke:#00c3ff;stroke-linejoin:round;stroke-width:2px;fill:none"/>
<text class="e" transform="translate(466 385)">
<tspan x="1.895" y="11">-5min</tspan>
</text>
<text class="e" transform="translate(533 385)">
<tspan x="1.785" y="11">-4min</tspan>
</text>
<text class="e" transform="translate(599 385)">
<tspan x="1.895" y="11">-3min</tspan>
</text>
<text class="e" transform="translate(666 385)">
<tspan x="1.895" y="11">-2min</tspan>
</text>
<text class="e" transform="translate(732 385)">
<tspan x="2.654" y="11">-1min</tspan>
</text>
<text class="e" transform="translate(799 385)">
<tspan x="1.895" y="11">-0min</tspan>
</text>
<text class="f" transform="translate(615 406)">
<tspan x="21.719" y="11">Time</tspan>
</text>
<text class="f" transform="translate(426.5 331.5) rotate(-90)">
<tspan x="3.494" y="11">KWH</tspan>
</text>
<g transform="translate(445 239)" style="clip-path:url(#a)">
<g transform="translate(-445 -370)">
<rect class="h" width="358" height="1" transform="translate(471 377)"/>
<text class="e" transform="translate(445 370)">
<tspan x="9.263" y="11">50</tspan>
</text>
</g>
<g transform="translate(-445 -344)">
<rect class="h" width="358" height="1" transform="translate(471 377)"/>
<text class="e" transform="translate(445 370)">
<tspan x="9.043" y="11">40</tspan>
</text>
</g>
<g transform="translate(-445 -318)">
<rect class="h" width="358" height="1" transform="translate(471 377)"/>
<text class="e" transform="translate(445 370)">
<tspan x="9.263" y="11">30</tspan>
</text>
</g>
<g transform="translate(-445 -292)">
<rect class="h" width="358" height="1" transform="translate(471 377)"/>
<text class="e" transform="translate(445 370)">
<tspan x="9.263" y="11">20</tspan>
</text>
</g>
<g transform="translate(-445 -266)">
<rect class="h" width="358" height="1" transform="translate(471 377)"/>
<text class="e" transform="translate(445 370)">
<tspan x="10.781" y="11">10</tspan>
</text>
</g>
<g transform="translate(-445 -240)">
<rect class="h" width="358" height="1" transform="translate(471 377)"/>
<text class="e" transform="translate(445 370)">
<tspan x="14.731" y="11">0</tspan>
</text>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.7 KiB

@@ -0,0 +1,76 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="402.5" height="181" viewBox="0 0 402.5 181">
<defs>
<clipPath id="a">
<rect width="384" height="143" style="fill:none"/>
</clipPath>
</defs>
<g transform="translate(-392.5 -626)">
<path d="M3346,487.094h358" transform="translate(-2909 250)" style="stroke-width:2px;stroke:#707070;fill:none"/>
<path d="M1-121.863H31.379l30.379-12,37.024,13,59.808,7.5,20.269-8.5,42.387,16,26.581-7.5,29.429,7.5,41.967-18.1L357-117.934V-10H1V-120.5H1" transform="translate(438 798)" style="fill-rule:evenodd;fill:none"/>
<path d="M-.1,17.95l30.578-3.436L57.715,18.2H86.426l34.365-3.687,31.782,3.436,30.418-.436,27.939,9.934L243.31,26.2l30.276-1.229,27.758,3,30.73-9.771,23.479-1" transform="translate(439 715)" style="stroke:#00c3ff;stroke-linejoin:round;stroke-width:2px;fill:none"/>
<g transform="translate(-34 387)">
<text class="e" transform="translate(466 385)">
<tspan x="1.895" y="11">-5min</tspan>
</text>
<text class="e" transform="translate(533 385)">
<tspan x="1.785" y="11">-4min</tspan>
</text>
<text class="e" transform="translate(599 385)">
<tspan x="1.895" y="11">-3min</tspan>
</text>
<text class="e" transform="translate(666 385)">
<tspan x="1.895" y="11">-2min</tspan>
</text>
<text class="e" transform="translate(732 385)">
<tspan x="2.654" y="11">-1min</tspan>
</text>
<text class="e" transform="translate(799 385)">
<tspan x="1.895" y="11">-0min</tspan>
</text>
</g>
<text class="f" transform="translate(581 793)">
<tspan x="21.719" y="11">Time</tspan>
</text>
<text class="f" transform="translate(392.5 733.5) rotate(-90)">
<tspan x="17.847" y="11">Hertz</tspan>
</text>
<g transform="translate(411 626)" style="clip-path:url(#a)">
<g transform="translate(-445 -370)">
<rect class="h" width="358" height="1" transform="translate(471 377)"/>
<text class="e" transform="translate(445 370)">
<tspan x="14.731" y="11">5</tspan>
</text>
</g>
<g transform="translate(-445 -344)">
<rect class="h" width="358" height="1" transform="translate(471 377)"/>
<text class="e" transform="translate(445 370)">
<tspan x="14.512" y="11">4</tspan>
</text>
</g>
<g transform="translate(-445 -318)">
<rect class="h" width="358" height="1" transform="translate(471 377)"/>
<text class="e" transform="translate(445 370)">
<tspan x="14.731" y="11">3</tspan>
</text>
</g>
<g transform="translate(-445 -292)">
<rect class="h" width="358" height="1" transform="translate(471 377)"/>
<text class="e" transform="translate(445 370)">
<tspan x="14.731" y="11">2</tspan>
</text>
</g>
<g transform="translate(-445 -266)">
<rect class="h" width="358" height="1" transform="translate(471 377)"/>
<text class="e" transform="translate(445 370)">
<tspan x="16.25" y="11">1</tspan>
</text>
</g>
<g transform="translate(-445 -240)">
<rect class="h" width="358" height="1" transform="translate(471 377)"/>
<text class="e" transform="translate(445 370)">
<tspan x="14.731" y="11">0</tspan>
</text>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.8 KiB

@@ -0,0 +1,76 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="402.799" height="181" viewBox="0 0 402.799 181">
<defs>
<clipPath id="a">
<rect width="384" height="149" style="fill:none"/>
</clipPath>
</defs>
<g transform="translate(-1391.5 -473)">
<path d="M1-121.863H31.379l30.379-12,37.024,13,59.808,7.5,20.269-8.5,42.387,16,26.581-7.5,29.429,7.5,41.967-18.1L357-117.934V-10H1V-120.5H1" transform="translate(1437 645)" style="fill-rule:evenodd;fill:none"/>
<g transform="translate(965 234)">
<text class="c" transform="translate(466 385)">
<tspan x="1.895" y="11">-5min</tspan>
</text>
<text class="c" transform="translate(533 385)">
<tspan x="1.785" y="11">-4min</tspan>
</text>
<text class="c" transform="translate(599 385)">
<tspan x="1.895" y="11">-3min</tspan>
</text>
<text class="c" transform="translate(666 385)">
<tspan x="1.895" y="11">-2min</tspan>
</text>
<text class="c" transform="translate(732 385)">
<tspan x="2.654" y="11">-1min</tspan>
</text>
<text class="c" transform="translate(799 385)">
<tspan x="1.895" y="11">-0min</tspan>
</text>
</g>
<text class="d" transform="translate(1580 640)">
<tspan x="21.719" y="11">Time</tspan>
</text>
<text class="d" transform="translate(1391.5 580.5) rotate(-90)">
<tspan x="9.859" y="11">Direction</tspan>
</text>
<g transform="translate(1410 473)" style="clip-path:url(#a)">
<g transform="translate(-445 -370)">
<rect class="f" width="358" height="1" transform="translate(471 377)"/>
<text class="c" transform="translate(445 370)">
<tspan x="3.794" y="11">360</tspan>
</text>
</g>
<g transform="translate(-445 -343)">
<rect class="f" width="358" height="1" transform="translate(471 377)"/>
<text class="c" transform="translate(445 370)">
<tspan x="3.794" y="11">300</tspan>
</text>
</g>
<g transform="translate(-445 -316)">
<rect class="f" width="358" height="1" transform="translate(471 377)"/>
<text class="c" transform="translate(445 370)">
<tspan x="3.575" y="11">240</tspan>
</text>
</g>
<g transform="translate(-445 -289)">
<rect class="f" width="358" height="1" transform="translate(471 377)"/>
<text class="c" transform="translate(445 370)">
<tspan x="5.313" y="11">180</tspan>
</text>
</g>
<g transform="translate(-445 -262)">
<rect class="f" width="358" height="1" transform="translate(471 377)"/>
<text class="c" transform="translate(445 370)">
<tspan x="9.263" y="11">60</tspan>
</text>
</g>
<g transform="translate(-445 -235)">
<rect class="f" width="358" height="1" transform="translate(471 377)"/>
<text class="c" transform="translate(445 370)">
<tspan x="14.731" y="11">0</tspan>
</text>
</g>
</g>
<path d="M207.314,40.768,178.208,57.593,140.667,29.928,117.739,46.593,84.378,30.437,58.024,40.768l-29.75-4.176L12.318,45.88-1.909,54.16" transform="translate(1438 544)" style="stroke-linejoin:round;stroke-width:2px;stroke:#ff6c00;fill:none"/>
<path d="M144.217,29.132,110.334,26.2,79.761,29.132,52.3,27.972,21.57,26.2-3.669,21.711" transform="translate(1649 563)" style="stroke:#00c3ff;stroke-linecap:round;stroke-linejoin:round;stroke-width:2px;fill:none"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.9 KiB

@@ -0,0 +1,75 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="402.5" height="181" viewBox="0 0 402.5 181">
<defs>
<clipPath id="a">
<rect width="384" height="149" style="fill:none"/>
</clipPath>
</defs>
<g transform="translate(-438.5 -473)">
<path d="M1-121.863H31.379l30.379-12,37.024,13,59.808,7.5,20.269-8.5,42.387,16,26.581-7.5,29.429,7.5,41.967-18.1L357-117.934V-10H1V-120.5H1" transform="translate(484 645)" style="fill-rule:evenodd;fill:none"/>
<path d="M355.553,26.2H295.22l-30.892-3.687L235.576,26.2l-31.782-2.564-33.14-1.123-27.939,2.934-32.381.753L80.058,24.972l-27.758,3L21.57,26.2l-23.479-1" transform="translate(485 572)" style="stroke:#00c3ff;stroke-linejoin:round;stroke-width:2px;fill:none"/>
<g transform="translate(12 234)">
<text class="d" transform="translate(466 385)">
<tspan x="1.895" y="11">-5min</tspan>
</text>
<text class="d" transform="translate(533 385)">
<tspan x="1.785" y="11">-4min</tspan>
</text>
<text class="d" transform="translate(599 385)">
<tspan x="1.895" y="11">-3min</tspan>
</text>
<text class="d" transform="translate(666 385)">
<tspan x="1.895" y="11">-2min</tspan>
</text>
<text class="d" transform="translate(732 385)">
<tspan x="2.654" y="11">-1min</tspan>
</text>
<text class="d" transform="translate(799 385)">
<tspan x="1.895" y="11">-0min</tspan>
</text>
</g>
<text class="e" transform="translate(627 640)">
<tspan x="21.719" y="11">Time</tspan>
</text>
<text class="e" transform="translate(438.5 580.5) rotate(-90)">
<tspan x="9.859" y="11">Direction</tspan>
</text>
<g transform="translate(457 473)" style="clip-path:url(#a)">
<g transform="translate(-445 -370)">
<rect class="g" width="358" height="1" transform="translate(471 377)"/>
<text class="d" transform="translate(445 370)">
<tspan x="3.794" y="11">360</tspan>
</text>
</g>
<g transform="translate(-445 -343)">
<rect class="g" width="358" height="1" transform="translate(471 377)"/>
<text class="d" transform="translate(445 370)">
<tspan x="3.794" y="11">300</tspan>
</text>
</g>
<g transform="translate(-445 -316)">
<rect class="g" width="358" height="1" transform="translate(471 377)"/>
<text class="d" transform="translate(445 370)">
<tspan x="3.575" y="11">240</tspan>
</text>
</g>
<g transform="translate(-445 -289)">
<rect class="g" width="358" height="1" transform="translate(471 377)"/>
<text class="d" transform="translate(445 370)">
<tspan x="5.313" y="11">180</tspan>
</text>
</g>
<g transform="translate(-445 -262)">
<rect class="g" width="358" height="1" transform="translate(471 377)"/>
<text class="d" transform="translate(445 370)">
<tspan x="9.263" y="11">60</tspan>
</text>
</g>
<g transform="translate(-445 -235)">
<rect class="g" width="358" height="1" transform="translate(471 377)"/>
<text class="d" transform="translate(445 370)">
<tspan x="14.731" y="11">0</tspan>
</text>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.7 KiB

@@ -0,0 +1,75 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="403.045" height="181" viewBox="0 0 403.045 181">
<defs>
<clipPath id="a">
<rect width="384" height="149" style="fill:none"/>
</clipPath>
</defs>
<g transform="translate(-1391.5 -473)">
<path d="M1-121.863H31.379l30.379-12,37.024,13,59.808,7.5,20.269-8.5,42.387,16,26.581-7.5,29.429,7.5,41.967-18.1L357-117.934V-10H1V-120.5H1" transform="translate(1437 645)" style="fill-rule:evenodd;fill:none"/>
<g transform="translate(965 234)">
<text class="c" transform="translate(466 385)">
<tspan x="1.895" y="11">-5min</tspan>
</text>
<text class="c" transform="translate(533 385)">
<tspan x="1.785" y="11">-4min</tspan>
</text>
<text class="c" transform="translate(599 385)">
<tspan x="1.895" y="11">-3min</tspan>
</text>
<text class="c" transform="translate(666 385)">
<tspan x="1.895" y="11">-2min</tspan>
</text>
<text class="c" transform="translate(732 385)">
<tspan x="2.654" y="11">-1min</tspan>
</text>
<text class="c" transform="translate(799 385)">
<tspan x="1.895" y="11">-0min</tspan>
</text>
</g>
<text class="d" transform="translate(1580 640)">
<tspan x="21.719" y="11">Time</tspan>
</text>
<text class="d" transform="translate(1391.5 580.5) rotate(-90)">
<tspan x="9.859" y="11">Direction</tspan>
</text>
<g transform="translate(1410 473)" style="clip-path:url(#a)">
<g transform="translate(-445 -370)">
<rect class="f" width="358" height="1" transform="translate(471 377)"/>
<text class="c" transform="translate(445 370)">
<tspan x="3.794" y="11">360</tspan>
</text>
</g>
<g transform="translate(-445 -343)">
<rect class="f" width="358" height="1" transform="translate(471 377)"/>
<text class="c" transform="translate(445 370)">
<tspan x="3.794" y="11">300</tspan>
</text>
</g>
<g transform="translate(-445 -316)">
<rect class="f" width="358" height="1" transform="translate(471 377)"/>
<text class="c" transform="translate(445 370)">
<tspan x="3.575" y="11">240</tspan>
</text>
</g>
<g transform="translate(-445 -289)">
<rect class="f" width="358" height="1" transform="translate(471 377)"/>
<text class="c" transform="translate(445 370)">
<tspan x="5.313" y="11">180</tspan>
</text>
</g>
<g transform="translate(-445 -262)">
<rect class="f" width="358" height="1" transform="translate(471 377)"/>
<text class="c" transform="translate(445 370)">
<tspan x="9.263" y="11">60</tspan>
</text>
</g>
<g transform="translate(-445 -235)">
<rect class="f" width="358" height="1" transform="translate(471 377)"/>
<text class="c" transform="translate(445 370)">
<tspan x="14.731" y="11">0</tspan>
</text>
</g>
</g>
<path d="M355.956,36.593l-27.383,20L278.956,45.88,268.115,29.928,238.508,56.593,207.552,35.437,178.208,57.593,140.667,29.928,117.739,46.593,84.378,30.437,58.024,40.768l-29.75-4.176L12.318,45.88-1.909,54.16" transform="translate(1438 544)" style="stroke:#ff6c00;stroke-linejoin:round;stroke-width:2px;fill:none"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.8 KiB

@@ -0,0 +1,94 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="403.597" height="181" viewBox="0 0 403.597 181">
<defs>
<clipPath id="a">
<rect width="384" height="152" style="fill:none"/>
</clipPath>
</defs>
<g transform="translate(-1391.5 -203)">
<path d="M1-121.863H31.379l30.379-12,37.024,13,59.808,7.5,20.269-8.5,42.387,16,26.581-7.5,29.429,7.5,41.967-18.1L357-117.934V-10H1V-120.5H1" transform="translate(1437 375)" style="fill-rule:evenodd;fill:none"/>
<g transform="translate(965 -36)">
<text class="c" transform="translate(466 385)">
<tspan x="1.895" y="11">-5min</tspan>
</text>
<text class="c" transform="translate(533 385)">
<tspan x="1.785" y="11">-4min</tspan>
</text>
<text class="c" transform="translate(599 385)">
<tspan x="1.895" y="11">-3min</tspan>
</text>
<text class="c" transform="translate(666 385)">
<tspan x="1.895" y="11">-2min</tspan>
</text>
<text class="c" transform="translate(732 385)">
<tspan x="2.654" y="11">-1min</tspan>
</text>
<text class="c" transform="translate(799 385)">
<tspan x="1.895" y="11">-0min</tspan>
</text>
</g>
<text class="d" transform="translate(1580 370)">
<tspan x="21.719" y="11">Time</tspan>
</text>
<text class="d" transform="translate(1391.5 310.5) rotate(-90)">
<tspan x="17.596" y="11">Knots</tspan>
</text>
<g transform="translate(1410 203)" style="clip-path:url(#a)">
<g transform="translate(-445 -370)">
<rect class="f" width="358" height="1" transform="translate(471 377)"/>
<text class="c" transform="translate(445 370)">
<tspan x="9.263" y="11">80</tspan>
</text>
</g>
<g transform="translate(-445 -353)">
<rect class="f" width="358" height="1" transform="translate(471 377)"/>
<text class="c" transform="translate(445 370)">
<tspan x="9.483" y="11">70</tspan>
</text>
</g>
<g transform="translate(-445 -336)">
<rect class="f" width="358" height="1" transform="translate(471 377)"/>
<text class="c" transform="translate(445 370)">
<tspan x="9.263" y="11">60</tspan>
</text>
</g>
<g transform="translate(-445 -319)">
<rect class="f" width="358" height="1" transform="translate(471 377)"/>
<text class="c" transform="translate(445 370)">
<tspan x="9.263" y="11">50</tspan>
</text>
</g>
<g transform="translate(-445 -302)">
<rect class="f" width="358" height="1" transform="translate(471 377)"/>
<text class="c" transform="translate(445 370)">
<tspan x="9.043" y="11">40</tspan>
</text>
</g>
<g transform="translate(-445 -285)">
<rect class="f" width="358" height="1" transform="translate(471 377)"/>
<text class="c" transform="translate(445 370)">
<tspan x="9.263" y="11">30</tspan>
</text>
</g>
<g transform="translate(-445 -268)">
<rect class="f" width="358" height="1" transform="translate(471 377)"/>
<text class="c" transform="translate(445 370)">
<tspan x="9.263" y="11">20</tspan>
</text>
</g>
<g transform="translate(-445 -251)">
<rect class="f" width="358" height="1" transform="translate(471 377)"/>
<text class="c" transform="translate(445 370)">
<tspan x="10.781" y="11">10</tspan>
</text>
</g>
<g transform="translate(-445 -234)">
<rect class="f" width="358" height="1" transform="translate(471 377)"/>
<text class="c" transform="translate(445 370)">
<tspan x="14.731" y="11">0</tspan>
</text>
</g>
</g>
<path d="M208.771,26.96,177.924.928l-30.251,34.6-30.123-24.6-29.779,8.6L57.929,36.752l-29.7-25.824L-1.909,33.36" transform="translate(1438 289)" style="stroke-linejoin:round;stroke-width:2px;stroke:#ff6c00;stroke-linecap:square;fill:none"/>
<path d="M-1.6,24.9l22.507,4.146,37.518-4.762,29.361-3.168,30.283,3.168,26.959-1.952" transform="translate(1649 292)" style="stroke:#00c3ff;stroke-linecap:round;stroke-linejoin:round;stroke-width:2px;fill:none"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.8 KiB

@@ -0,0 +1,93 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="402.5" height="181" viewBox="0 0 402.5 181">
<defs>
<clipPath id="a">
<rect width="384" height="152" style="fill:none"/>
</clipPath>
</defs>
<g transform="translate(-438.5 -203)">
<path d="M1-121.863H31.379l30.379-12,37.024,13,59.808,7.5,20.269-8.5,42.387,16,26.581-7.5,29.429,7.5,41.967-18.1L357-117.934V-10H1V-120.5H1" transform="translate(484 375)" style="fill-rule:evenodd;fill:none"/>
<path d="M-1.909,24.285H58.424l30.892,3.687,28.752-3.687,31.782,2.564,33.14,1.123,27.939-2.934,32.381-.753,30.276,1.229,27.758-3,30.73,1.771,23.479,1" transform="translate(485 279)" style="stroke:#00c3ff;stroke-linejoin:round;stroke-width:2px;fill:none"/>
<g transform="translate(12 -36)">
<text class="d" transform="translate(466 385)">
<tspan x="1.895" y="11">-5min</tspan>
</text>
<text class="d" transform="translate(533 385)">
<tspan x="1.785" y="11">-4min</tspan>
</text>
<text class="d" transform="translate(599 385)">
<tspan x="1.895" y="11">-3min</tspan>
</text>
<text class="d" transform="translate(666 385)">
<tspan x="1.895" y="11">-2min</tspan>
</text>
<text class="d" transform="translate(732 385)">
<tspan x="2.654" y="11">-1min</tspan>
</text>
<text class="d" transform="translate(799 385)">
<tspan x="1.895" y="11">-0min</tspan>
</text>
</g>
<text class="e" transform="translate(627 370)">
<tspan x="21.719" y="11">Time</tspan>
</text>
<text class="e" transform="translate(438.5 310.5) rotate(-90)">
<tspan x="17.596" y="11">Knots</tspan>
</text>
<g transform="translate(457 203)" style="clip-path:url(#a)">
<g transform="translate(-445 -370)">
<rect class="g" width="358" height="1" transform="translate(471 377)"/>
<text class="d" transform="translate(445 370)">
<tspan x="9.263" y="11">80</tspan>
</text>
</g>
<g transform="translate(-445 -353)">
<rect class="g" width="358" height="1" transform="translate(471 377)"/>
<text class="d" transform="translate(445 370)">
<tspan x="9.483" y="11">70</tspan>
</text>
</g>
<g transform="translate(-445 -336)">
<rect class="g" width="358" height="1" transform="translate(471 377)"/>
<text class="d" transform="translate(445 370)">
<tspan x="9.263" y="11">60</tspan>
</text>
</g>
<g transform="translate(-445 -319)">
<rect class="g" width="358" height="1" transform="translate(471 377)"/>
<text class="d" transform="translate(445 370)">
<tspan x="9.263" y="11">50</tspan>
</text>
</g>
<g transform="translate(-445 -302)">
<rect class="g" width="358" height="1" transform="translate(471 377)"/>
<text class="d" transform="translate(445 370)">
<tspan x="9.043" y="11">40</tspan>
</text>
</g>
<g transform="translate(-445 -285)">
<rect class="g" width="358" height="1" transform="translate(471 377)"/>
<text class="d" transform="translate(445 370)">
<tspan x="9.263" y="11">30</tspan>
</text>
</g>
<g transform="translate(-445 -268)">
<rect class="g" width="358" height="1" transform="translate(471 377)"/>
<text class="d" transform="translate(445 370)">
<tspan x="9.263" y="11">20</tspan>
</text>
</g>
<g transform="translate(-445 -251)">
<rect class="g" width="358" height="1" transform="translate(471 377)"/>
<text class="d" transform="translate(445 370)">
<tspan x="10.781" y="11">10</tspan>
</text>
</g>
<g transform="translate(-445 -234)">
<rect class="g" width="358" height="1" transform="translate(471 377)"/>
<text class="d" transform="translate(445 370)">
<tspan x="14.731" y="11">0</tspan>
</text>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.6 KiB

@@ -0,0 +1,93 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="403.238" height="181" viewBox="0 0 403.238 181">
<defs>
<clipPath id="a">
<rect width="384" height="152" style="fill:none"/>
</clipPath>
</defs>
<g transform="translate(-1391.5 -203)">
<path d="M1-121.863H31.379l30.379-12,37.024,13,59.808,7.5,20.269-8.5,42.387,16,26.581-7.5,29.429,7.5,41.967-18.1L357-117.934V-10H1V-120.5H1" transform="translate(1437 375)" style="fill-rule:evenodd;fill:none"/>
<g transform="translate(965 -36)">
<text class="c" transform="translate(466 385)">
<tspan x="1.895" y="11">-5min</tspan>
</text>
<text class="c" transform="translate(533 385)">
<tspan x="1.785" y="11">-4min</tspan>
</text>
<text class="c" transform="translate(599 385)">
<tspan x="1.895" y="11">-3min</tspan>
</text>
<text class="c" transform="translate(666 385)">
<tspan x="1.895" y="11">-2min</tspan>
</text>
<text class="c" transform="translate(732 385)">
<tspan x="2.654" y="11">-1min</tspan>
</text>
<text class="c" transform="translate(799 385)">
<tspan x="1.895" y="11">-0min</tspan>
</text>
</g>
<text class="d" transform="translate(1580 370)">
<tspan x="21.719" y="11">Time</tspan>
</text>
<text class="d" transform="translate(1391.5 310.5) rotate(-90)">
<tspan x="17.596" y="11">Knots</tspan>
</text>
<g transform="translate(1410 203)" style="clip-path:url(#a)">
<g transform="translate(-445 -370)">
<rect class="f" width="358" height="1" transform="translate(471 377)"/>
<text class="c" transform="translate(445 370)">
<tspan x="9.263" y="11">80</tspan>
</text>
</g>
<g transform="translate(-445 -353)">
<rect class="f" width="358" height="1" transform="translate(471 377)"/>
<text class="c" transform="translate(445 370)">
<tspan x="9.483" y="11">70</tspan>
</text>
</g>
<g transform="translate(-445 -336)">
<rect class="f" width="358" height="1" transform="translate(471 377)"/>
<text class="c" transform="translate(445 370)">
<tspan x="9.263" y="11">60</tspan>
</text>
</g>
<g transform="translate(-445 -319)">
<rect class="f" width="358" height="1" transform="translate(471 377)"/>
<text class="c" transform="translate(445 370)">
<tspan x="9.263" y="11">50</tspan>
</text>
</g>
<g transform="translate(-445 -302)">
<rect class="f" width="358" height="1" transform="translate(471 377)"/>
<text class="c" transform="translate(445 370)">
<tspan x="9.043" y="11">40</tspan>
</text>
</g>
<g transform="translate(-445 -285)">
<rect class="f" width="358" height="1" transform="translate(471 377)"/>
<text class="c" transform="translate(445 370)">
<tspan x="9.263" y="11">30</tspan>
</text>
</g>
<g transform="translate(-445 -268)">
<rect class="f" width="358" height="1" transform="translate(471 377)"/>
<text class="c" transform="translate(445 370)">
<tspan x="9.263" y="11">20</tspan>
</text>
</g>
<g transform="translate(-445 -251)">
<rect class="f" width="358" height="1" transform="translate(471 377)"/>
<text class="c" transform="translate(445 370)">
<tspan x="10.781" y="11">10</tspan>
</text>
</g>
<g transform="translate(-445 -234)">
<rect class="f" width="358" height="1" transform="translate(471 377)"/>
<text class="c" transform="translate(445 370)">
<tspan x="14.731" y="11">0</tspan>
</text>
</g>
</g>
<path d="M356,31.526,328.05.928l-30.708,29.11-29.654,1.555-29.56-20.665-30.45,19.3L177.924.928l-30.251,34.6-30.123-24.6-29.779,8.6L57.929,36.752l-29.7-25.824L-1.909,33.36" transform="translate(1438 289)" style="stroke:#ff6c00;stroke-linejoin:round;stroke-width:2px;fill:none"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.6 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.9 KiB

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 22 22">
<path d="M0,0H22V22H0Z" style="fill:none"/>
<path d="M1,19H21L11,2Zm10.909-2.684H10.091V14.526h1.818Zm0-3.579H10.091V9.158h1.818Z" class="icon-path"/>
</svg>

After

Width:  |  Height:  |  Size: 250 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14"><defs><style>.a{opacity:0.5;}.b{fill:none;}</style></defs><g class="a"><path d="M15,6.007,13.993,5,10,8.993,6.007,5,5,6.007,8.993,10,5,13.993,6.007,15,10,11.007,13.993,15,15,13.993,11.007,10Z" transform="translate(-2.294 -2.294)"/><path class="b" d="M0,0H14V14H0Z"/></g></svg>

After

Width:  |  Height:  |  Size: 359 B

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<path d="M54,207h7l0,8-7,0Zm-9,8,0-6h7v6Zm0-8,0-8,7,0v8Zm9-2v-6l7,0,0,6Z" transform="translate(-45 -198.999)" class="icon-path"/>
</svg>

After

Width:  |  Height:  |  Size: 225 B

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="14" viewBox="0 0 16 14">
<path d="M47.077,1003v-1.609h1.4v-1.446H45V989H61v10.945H57.526v1.446h1.4V1003Zm8.674-7.93v1.495h2.127v1.091H59.45v-1.091l.319.071v-1.273l-.319-.293v-3.552H58.375Zm-7.8,2.586H49.52v-1.091h.729V995.07H49.52v-3.552H48.444l-2.214,3.845v1.273l1.717-.071Zm4.266-6.165a2.142,2.142,0,0,0-.695.559,2.628,2.628,0,0,0-.459.844,3.529,3.529,0,0,0,0,2.137,2.619,2.619,0,0,0,.459.844,2.093,2.093,0,0,0,.695.554,2.024,2.024,0,0,0,1.746,0,2.1,2.1,0,0,0,.7-.554,2.646,2.646,0,0,0,.459-.844,3.529,3.529,0,0,0,0-2.137q-.028-.083-.06-.162.006.079.006.166a1.314,1.314,0,0,1-.138.646.4.4,0,0,1-.353.246.412.412,0,0,1-.366-.246,1.314,1.314,0,0,1-.138-.646,1.335,1.335,0,0,1,.138-.649.411.411,0,0,1,.366-.249l.045,0a2.136,2.136,0,0,0-.656-.512,1.991,1.991,0,0,0-1.746,0Zm5.78,2.279.618-.829v.829Z" transform="translate(-45 -988.999)" class="icon-path"/>
</svg>

After

Width:  |  Height:  |  Size: 925 B

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16.008" height="14.005" viewBox="0 0 16.008 14.005">
<path d="M54.846,1053H46.23c-.8,0-1.235-1.5-1.235-2.417v-11.174a.391.391,0,0,1,.364-.414H60.64a.391.391,0,0,1,.364.414v11.174c0,.913-.432,2.417-1.235,2.417ZM47,1051H59v-10H47Zm9.247-1.006c-.138,0-.249-.157-.249-.35v-6.306c0-.193.112-.351.249-.351h1.5c.138,0,.249.157.249.351v6.306c0,.193-.112.35-.249.35Zm-4.005,0c-.139,0-.253-.168-.253-.375v-5.246c0-.207.113-.374.253-.374h1.517c.139,0,.253.167.253.374v5.246c0,.207-.113.375-.253.375Zm-3.984,0c-.138,0-.249-.178-.249-.4v-3.187c0-.22.112-.4.249-.4h1.5c.138,0,.249.178.249.4v3.187c0,.219-.112.4-.249.4Z" transform="translate(-44.996 -1038.994)" class="icon-path"/>
</svg>

After

Width:  |  Height:  |  Size: 725 B

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="12.99" height="12.998" viewBox="0 0 12.99 12.998">
<path d="M15.091,5.909a6.5,6.5,0,1,0,1.69,6.215h-1.69a4.872,4.872,0,1,1-4.59-6.5,4.805,4.805,0,0,1,3.428,1.446L11.313,9.687H17V4Z" transform="translate(-4.01 -4)" style="fill:#888"/>
</svg>

After

Width:  |  Height:  |  Size: 292 B

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="15.99" viewBox="0 0 16 15.99">
<path d="M57,758v-2H45v13.509a2.484,2.484,0,0,0,2.481,2.481H58.508a2.484,2.484,0,0,0,2.481-2.481L61,758Zm-2,11H47v-2h8v2Zm0-4H47v-2h8v2Zm0-5v1H47v-3h8v2Zm4,10c0,.456-.035.336-.491.336S57,770.455,57,770V760h2Z" transform="translate(-45 -755.999)" class="icon-path"/>
</svg>

After

Width:  |  Height:  |  Size: 367 B

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="13" viewBox="0 0 16 13">
<path d="M53.667,1287v-4.756l1.609-1.743-1.609-1.743V1274h6A1.394,1.394,0,0,1,61,1275.444v10.111A1.394,1.394,0,0,1,59.666,1287Zm.666-3.611a.7.7,0,0,0,.667.722h2.667a.724.724,0,0,0,0-1.444H55A.7.7,0,0,0,54.333,1283.388Zm1.334-2.889a.7.7,0,0,0,.667.722h2a.724.724,0,0,0,0-1.444h-2A.7.7,0,0,0,55.667,1280.5Zm-1.334-2.889a.7.7,0,0,0,.667.722h2.667a.724.724,0,0,0,0-1.444H55A.7.7,0,0,0,54.333,1277.61Zm-8,9.389A1.393,1.393,0,0,1,45,1285.555v-10.111A1.393,1.393,0,0,1,46.333,1274h6v5.354l1.057,1.146-1.057,1.146V1287Zm.667-3.611a.7.7,0,0,0,.667.722h2.667a.724.724,0,0,0,0-1.444H47.667A.7.7,0,0,0,47,1283.388Zm.667-2.889a.7.7,0,0,0,.667.722H51a.724.724,0,0,0,0-1.444H48.334A.7.7,0,0,0,47.667,1280.5ZM47,1277.61a.7.7,0,0,0,.667.722h2.667a.724.724,0,0,0,0-1.444H47.667A.7.7,0,0,0,47,1277.61Z" transform="translate(-45 -1274)" class="icon-path"/>
</svg>

After

Width:  |  Height:  |  Size: 932 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18"><defs><style>.a,.d{fill:none;}.a,.b{stroke:#fff;}.a{stroke-width:1.5px;}.b{fill:#fff;}.c{stroke:none;}</style></defs><g transform="translate(-1651 -1025)"><g class="a" transform="translate(1651 1025)"><circle class="c" cx="9" cy="9" r="9"/><circle class="d" cx="9" cy="9" r="8.25"/></g><path class="b" d="M6.034,10.88,4.077,8.795l-.667.7,2.624,2.795,5.634-6-.662-.7Z" transform="translate(1651.998 1025.281)"/></g></svg>

After

Width:  |  Height:  |  Size: 503 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="80.829" height="51.019" viewBox="0 0 80.829 51.019"><defs><style>.a,.d{fill:none;}.a{stroke:#00999a;}.b{fill:#fff;stroke:#707070;}.c{stroke:none;}</style></defs><g transform="translate(-1154 -392.981)"><path class="a" d="M-42.24,138.732,32.068,93.355" transform="translate(1202.5 300.053)"/><g class="b" transform="translate(1154 434)"><circle class="c" cx="5" cy="5" r="5"/><circle class="d" cx="5" cy="5" r="4.5"/></g></g></svg>

After

Width:  |  Height:  |  Size: 477 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="203.721" height="50.475" viewBox="0 0 203.721 50.475"><defs><style>.a,.d{fill:none;}.a{stroke:#fff;stroke-width:2px;}.b{fill:#fff;stroke:#707070;}.c{stroke:none;}</style></defs><g transform="translate(-1117 -464)"><line class="a" x2="199" y2="45" transform="translate(1121.5 468.5)"/><g class="b" transform="translate(1117 464)"><circle class="c" cx="5" cy="5" r="5"/><circle class="d" cx="5" cy="5" r="4.5"/></g></g></svg>

After

Width:  |  Height:  |  Size: 470 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="211.968" height="115.384" viewBox="0 0 211.968 115.384"><defs><style>.a,.d{fill:none;}.a{stroke:#fff;stroke-width:2px;}.b{fill:#fff;stroke:#707070;}.c{stroke:none;}</style></defs><g transform="translate(-855.032 -265.616)"><line class="a" x2="206" y2="109" transform="translate(855.5 266.5)"/><g class="b" transform="translate(1057 371)"><circle class="c" cx="5" cy="5" r="5"/><circle class="d" cx="5" cy="5" r="4.5"/></g></g></svg>

After

Width:  |  Height:  |  Size: 479 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="286.713" height="67.477" viewBox="0 0 286.713 67.477"><defs><style>.a,.d{fill:none;}.a{stroke:#fff;stroke-width:2px;}.b{fill:#fff;stroke:#707070;}.c{stroke:none;}</style></defs><g transform="translate(-1129 -217.523)"><g transform="translate(95 -295)"><line class="a" y1="61" x2="280" transform="translate(1040.5 513.5)"/></g><g class="b" transform="translate(1129 275)"><circle class="c" cx="5" cy="5" r="5"/><circle class="d" cx="5" cy="5" r="4.5"/></g></g></svg>

After

Width:  |  Height:  |  Size: 512 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="272.85" height="105.437" viewBox="0 0 272.85 105.437"><defs><style>.a,.d{fill:none;}.a{stroke:#fff;stroke-width:2px;}.b{fill:#fff;stroke:#707070;}.c{stroke:none;}</style></defs><g transform="translate(-807.15 -601)"><line class="a" y1="99" x2="265" transform="translate(807.5 606.5)"/><g class="b" transform="translate(1070 601)"><circle class="c" cx="5" cy="5" r="5"/><circle class="d" cx="5" cy="5" r="4.5"/></g></g></svg>

After

Width:  |  Height:  |  Size: 471 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="212.076" height="151.317" viewBox="0 0 212.076 151.317"><defs><style>.a,.d{fill:none;}.a{stroke:#fff;stroke-width:2px;}.b{fill:#fff;stroke:#707070;}.c{stroke:none;}</style></defs><g transform="translate(-1171 -334)"><line class="a" x1="207" y1="146" transform="translate(1175.5 338.5)"/><g class="b" transform="translate(1171 334)"><circle class="c" cx="5" cy="5" r="5"/><circle class="d" cx="5" cy="5" r="4.5"/></g></g></svg>

After

Width:  |  Height:  |  Size: 473 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="164.91" height="78.412" viewBox="0 0 164.91 78.412"><defs><style>.a,.d{fill:none;}.a{stroke:#fff;stroke-width:2px;}.b{fill:#fff;stroke:#707070;}.c{stroke:none;}</style></defs><g transform="translate(-1219 -237.588)"><line class="a" x1="160" y2="72" transform="translate(1223.5 238.5)"/><g class="b" transform="translate(1219 306)"><circle class="c" cx="5" cy="5" r="5"/><circle class="d" cx="5" cy="5" r="4.5"/></g></g></svg>

After

Width:  |  Height:  |  Size: 472 B

@@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" width="165" height="43" viewBox="0 0 165 43">
<g transform="translate(-42 -35)">
<text transform="translate(42 64)" style="fill:#00999a;font-size:29px;font-family:Audiowide-Regular,Audiowide">
<tspan x="0" y="0">CONTOSO</tspan>
</text>
<text transform="translate(43 75)" style="fill:#4f4f4f;font-size:10px;font-family:Cousine;letter-spacing:.44em">
<tspan x="0" y="0">RENEWABLE ENERGY</tspan>
</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 517 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="405.79" height="872.801" viewBox="0 0 405.79 872.801"><defs><style>.a{fill:#e6e6e6;}.a,.b{stroke:#000;stroke-miterlimit:10;stroke-width:0.25px;}.b{fill:#f15a24;}</style></defs><g transform="translate(-763.136 -1362.321)"><path class="a" d="M956.294,2225.6c0,5.188-15.09,9.393-33.7,9.393s-33.7-4.205-33.7-9.393V2140.99l14.98-560.855h37.448l14.98,560.855Z"/><path class="b" d="M956.294,2140.99c0,5.188-15.09,9.393-33.7,9.393s-33.7-4.205-33.7-9.393V2225.6c0,5.188,15.09,9.393,33.7,9.393s33.7-4.205,33.7-9.393Z"/><path class="a" d="M933.123,1609.285l-46.755-23.9c-3.505,0-4.774-26.64-3.5-30.144l23.663-28.405c1.6-3.131,5.15-6.344,8.654-6.344h36.971a6.249,6.249,0,0,1,6.134,6.344l.172,29.065a34.506,34.506,0,0,1-.823,7.694l-15.863,39.343C940.5,1606.444,936.627,1609.285,933.123,1609.285Z"/><rect class="a" width="59.906" height="58.299" rx="6.332" transform="translate(881.872 1550.986)"/><circle class="a" cx="32.351" cy="32.351" r="32.351" transform="translate(879.474 1547.784)"/><path class="a" d="M927.084,1586.5h6.978a1.983,1.983,0,0,0,.568-1.136c0-.568-1.46-2.677-1.46-5.031v-.392c0-2.353,1.46-4.463,1.46-5.031a1.983,1.983,0,0,0-.568-1.136h-6.978Z"/><path class="a" d="M933.016,1585.387l232.075-12.122a3.9,3.9,0,0,0,3.621-4.687l-2.343-11.383a3.907,3.907,0,0,0-3.743-3.117l-215.044-4.547a5.3,5.3,0,0,0-5.4,4.849c-.569,6.688-2.3,18.065-7.553,20.64C927.084,1578.722,933.016,1585.387,933.016,1585.387Z"/><line class="a" y1="5.851" x2="198.037" transform="translate(949.61 1569.057)"/><path class="a" d="M905.771,1559.152,779.237,1364.23a3.906,3.906,0,0,0-5.871-.792l-8.686,7.721a3.905,3.905,0,0,0-.828,4.8l103.584,188.507a5.3,5.3,0,0,0,6.9,2.252c6.077-2.851,16.8-7.038,21.651-3.78C902.965,1567.622,905.771,1559.152,905.771,1559.152Z"/><path class="a" d="M909.7,1563.734l-3.489-6.044a1.984,1.984,0,0,0-1.268.076c-.492.284-1.589,2.6-3.627,3.781l-.339.195c-2.038,1.177-4.595.967-5.087,1.251a1.983,1.983,0,0,0-.7,1.06l3.489,6.044Z"/><line class="a" x1="104.085" y1="168.579" transform="translate(784.314 1381.441)"/><path class="a" d="M896.676,1595.867l-105.54,207.044a3.905,3.905,0,0,0,2.249,5.48l11.029,3.661a3.9,3.9,0,0,0,4.571-1.682l111.46-183.96a5.3,5.3,0,0,0-1.5-7.1c-5.508-3.838-14.493-11.028-14.1-16.861C905.414,1594.063,896.676,1595.867,896.676,1595.867Z"/><path class="a" d="M898.679,1590.174l-3.489,6.044a1.983,1.983,0,0,0,.7,1.06c.492.284,3.049.074,5.087,1.25l.339.2c2.038,1.177,3.135,3.5,3.627,3.781a1.984,1.984,0,0,0,1.268.076l3.489-6.044Z"/><line class="a" x1="93.951" y2="174.43" transform="translate(803.502 1615.477)"/><circle class="a" cx="18.724" cy="18.724" r="18.724" transform="translate(893.1 1561.411)"/></g></svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

@@ -0,0 +1,63 @@
.Layout {
height: 100vh;
width: 100vw;
display: flex;
header {
width: 15vw;
-webkit-box-shadow: 3px 6px 24px -10px rgba(0,0,0,0.75);
-moz-box-shadow: 3px 6px 24px -10px rgba(0,0,0,0.75);
box-shadow: 3px 6px 24px -10px rgba(0,0,0,0.75);
z-index: 1;
ul {
width: 100;
padding: 0;
margin: 0;
list-style-type: none;
li {
&.logo {
border-bottom: 1px solid #DDDFE1;
padding: 2vw;
svg {
width: 8vw;
}
}
&:nth-child(2) {
margin-top: 0.5vw;
}
padding: 1vw 2vw;
border-left: 2px solid transparent;
font-size: 0.8vw;
font-family: SegoeUISemiLight;
a {
text-decoration: none;
color: #6A707E;
display: flex;
align-items: center;
}
svg {
.icon-path {
fill:#aaa;
}
width: 1vw;
margin-right: 1vw;
}
&.selected {
background: #E5F5F5;
border-color: #00999A;
a {
color: #00999A;
}
svg {
.icon-path{
fill: #00999a;
}
}
}
}
}
}
main {
flex: 1;
background: #B2D9EA;
}
}
@@ -0,0 +1,51 @@
// node_modules
import React from "react";
import { Link } from "react-router-dom";
import SVG from "react-inlinesvg";
// local imports
import "./Layout.scss";
import Logo from '../../assets/logo.svg';
import IconDashboard from '../../assets/icon-dashboard.svg';
import IconAlerts from '../../assets/icon-alerts.svg';
import IconReports from '../../assets/icon-reports.svg';
import IconModels from '../../assets/icon-models.svg';
import IconMaintenance from '../../assets/icon-maintenance.svg';
import IconSettings from '../../assets/icon-settings.svg';
import Toast from "../Toast";
export const Layout = ({ children, selected, showToast, setShowToast }) => {
return (
<div className="Layout">
<header>
<nav>
<ul>
<li className="logo">
<Link to="/dashboard"><SVG src={Logo} /></Link>
</li>
<li className={selected === 'dashboard' ? 'selected' : ''}>
<Link to="/dashboard"><SVG src={IconDashboard} />Dashboard</Link>
</li>
<li className={selected === 'alerts' ? 'selected' : ''}>
<Link to="/alerts"><SVG src={IconAlerts} />Alerts</Link>
</li>
<li className={selected === 'reports' ? 'selected' : ''}>
<Link to="/dashboard"><SVG src={IconReports} />Reports</Link>
</li>
<li>
<Link to="/dashboard"><SVG src={IconModels} />Models</Link>
</li>
<li>
<Link to="/dashboard"><SVG src={IconMaintenance} />Maintenance</Link>
</li>
<li>
<Link to="/dashboard"><SVG src={IconSettings} />Settings</Link>
</li>
</ul>
</nav>
</header>
<main>{children}</main>
{showToast && <Toast onClose={() => {setShowToast(false)}} />}
</div>
);
}
@@ -0,0 +1,24 @@
.Toast {
position: absolute;
bottom: 1vw;
right: 1vw;
height: 2vw;
width: 10vw;
background: #00999A;
display: flex;
align-items: center;
color: white;
font-size: 0.7vw;
padding: 0 0.7vw;
.icon-tick {
width: 1vw;
margin-right: 0.3vw;
}
.icon-close {
width: 0.8vw;
cursor: pointer;
}
span {
flex: 1;
}
}
@@ -0,0 +1,16 @@
import React, { Component } from 'react';
import IconClose from '../../assets/icon-close.svg';
import IconTick from '../../assets/icon-tick.svg';
import './Toast.scss';
export default class Toast extends Component {
render() {
return (
<div className="Toast">
<img src={IconTick} alt="icon tick" className="icon-tick" />
<span>Alert resolved</span>
<img src={IconClose} alt="icon close" className="icon-close" onClick={this.props.onClose} />
</div>
)
}
}
@@ -0,0 +1,8 @@
// node_modules
import React from 'react';
import ReactDOM from 'react-dom';
// local components
import App from './App';
ReactDOM.render(<App />, document.getElementById('root'));
@@ -0,0 +1,264 @@
.Alerts {
position: relative;
height: 100%;
width: 100%;
.turbine {
width: 19vw;
position: absolute;
top: 7vw;
left: 34vw;
// max-height: 80vh;
}
.line {
position: absolute;
&.power-generated {
top: 14vw;
left: 28.5vw;
width: 11vw;
}
&.vibration {
top: 30vw;
left: 26.5vw;
width: 14vw;
}
&.oil-temperature {
top: 25vw;
left: 42.25vw;
width: 11vw;
}
&.security {
top: 11.2vw;
left: 42.8vw;
width: 15vw;
}
&.wind-direction {
top: 17.1vw;
left: 43vw;
width: 11.5vw;
}
&.wind-speed {
top: 11.9vw;
left: 47vw;
width: 9vw;
}
}
.loader {
position: absolute;
top: 40%;
left: calc(50% - 265px);
width: 450px;
background: white;
padding: 50px 40px;
-webkit-box-shadow: 3px 6px 24px -10px rgba(0,0,0,0.75);
-moz-box-shadow: 3px 6px 24px -10px rgba(0,0,0,0.75);
box-shadow: 3px 6px 24px -10px rgba(0,0,0,0.75);
p {
color: #00999A;
font-family: SegoeUISemiBold;
font-size: 20px;
margin: 0;
margin-bottom: 20px;
text-align: center;
}
.progress-border {
background: #E4F4F4;
border: 1px solid #BBE0E0;
width: 450px;
height: 30px;
.progress {
background: #00999A;
height: 100%;
}
}
}
.content-wrap {
height: 100%;
width: 100%;
.menu {
position: absolute;
top: 2.5vw;
left: calc(50% - 13vw);
button {
width: 7vw;
color: #707070B2;
background: #E4F4F4;
border: 1px solid #707070B2;
height: 2.2vw;
text-align: center;
font-family: SegoeUISemiBold;
font-size: 0.7vw;
outline: none;
cursor: pointer;
&.selected {
color: #00999A;
border-color: #00999A;
}
&:first-child {
border-top-left-radius: 4px;
border-bottom-left-radius: 4px;
}
&:last-child {
border-top-right-radius: 4px;
border-bottom-right-radius: 4px;
}
}
}
.refresh {
position: absolute;
top: 2.5vw;
cursor: pointer;
left: calc(50% + 2vw);
width: 5vw;
color: #707070B2;
background: #E4F4F4;
border: 1px solid #707070B2;
height: 2.2vw;
text-align: center;
font-family: SegoeUISemiBold;
font-size: 0.7vw;
outline: none;
justify-content: center;
display: flex;
align-items: center;
border-radius: 4px;
.icon {
width: 0.8vw;
margin-right: 0.35vw;
}
}
.stats-panels {
position: absolute;
top: 34vw;
left: 47vw;
display: flex;
}
.left-panels {
position: absolute;
top: 10vw;
left: 8vw;
display: flex;
flex-direction: column;
}
.right-panels {
position: absolute;
top: 10vw;
right: 8vw;
display: flex;
flex-direction: column;
}
.left-panels, .right-panels {
.panel {
margin-bottom: 30px;
}
.bottom-panels {
display: flex;
.panel {
margin-right: 2vw;
height: 3.5vw;
width: 7.5vw;
.stat {
font-size: 1.6vw;
font-family: SegoeUISemiLight;
color: #757B88;
}
}
}
}
.panel {
background: white;
padding: 20px;
padding: 1vw;
width: 19vw;
-webkit-box-shadow: 3px 6px 24px -10px rgba(0,0,0,0.75);
-moz-box-shadow: 3px 6px 24px -10px rgba(0,0,0,0.75);
box-shadow: 3px 6px 24px -10px rgba(0,0,0,0.75);
&.power {
top: 10vw;
left: 10vw;
position: absolute;
}
&.vibrations {
top: 29vw;
left: 6vw;
position: absolute;
}
&.oil {
top: 21vw;
left: 52vw;
position: absolute;
}
&.security {
top: 7vw;
left: 56vw;
width: 15vw;
position: absolute;
}
.graph {
width: 100%;
height: auto;
margin-left: -0.3vw;
margin-bottom: -0.3vw;
&.power-generated, &.vibration, &.oil-temperature {
.e,.f{fill:#777;font-size:10px;font-family:SegoeUISemilight,Segoe UI;font-weight:300;letter-spacing:.02em}.f{fill:#333}.h{opacity:.039}
}
&.wind-speed-farm, &.wind-direction-farm {
.d,.e{fill:#777;font-size:10px;font-family:SegoeUISemilight,Segoe UI;font-weight:300;letter-spacing:.02em}.e{fill:#333}.g{opacity:.039}
}
&.wind-speed-turbine, &.wind-direction-turbine {
.c,.d{fill:#777;font-size:10px;font-family:SegoeUISemilight,Segoe UI;font-weight:300;letter-spacing:.02em}.d{fill:#333}.f{opacity:.039}
}
}
p {
color: #6C7280;
font-family: SegoeUISemiLight;
font-size: 0.8vw;
margin: 0;
&.padded {
padding: 1.5vw 3vw;
text-align: center;
line-height: 1.5;
}
}
&.small {
width: 6vw;
height: 2.4vw;
margin-right: 0.5vw;
h3 {
font-size: 0.8vw;
margin-bottom: 10px;
}
}
.panel-heading {
display: flex;
width: 100%;
justify-content: space-between;
}
h3 {
color: #00999A;
font-size: 1vw;
font-family: SegoeUISemiBold;
margin: 0;
margin-bottom: 5px;
}
.legend {
color: #777777;
font-size: 0.55vw;
font-family: SegoeUISemiLight;
display: flex;
align-items: center;
.ball {
height: 0.3vw;
width: 0.3vw;
border-radius: 50%;
background: #777777;
margin-left: 0.6vw;
margin-right: 0.2vw;
&.actual {
background: #00C3FF;
}
}
}
}
}
}
@@ -0,0 +1,211 @@
import React, { Component } from 'react';
import SVG from 'react-inlinesvg';
import './Alerts.scss';
import TurbineImage from '../../assets/turbine.svg';
import LinePowerGenerated from '../../assets/line-power-generated.svg';
import LineOilTemperature from '../../assets/line-oil-temperature.svg';
import LineVibration from '../../assets/line-vibration.svg';
import LineWindSpeed from '../../assets/line-wind-speed.svg';
import LineWindDirection from '../../assets/line-wind-direction.svg';
import LineSecurityAlert from '../../assets/line-security-alerts.svg';
import GraphPowerGenerated from '../../assets/graph-power-generated.svg';
import GraphOilTemperature from '../../assets/graph-oil-temperature.svg';
import GraphVibration from '../../assets/graph-vibration.svg';
import GraphWindSpeedFarm from '../../assets/graph-wind-speed-farm.svg';
import GraphWindDirectionFarm from '../../assets/graph-wind-direction-farm.svg';
import GraphWindSpeedTurbine from '../../assets/graph-wind-speed-turbine.svg';
import GraphWindDirectionTurbine from '../../assets/graph-wind-direction-turbine.svg';
import GraphWindSpeedTurbineV2 from '../../assets/graph-wind-speed-after.svg';
import GraphWindDirectionTurbineV2 from '../../assets/graph-wind-direction-after.svg';
import RefreshIcon from '../../assets/icon-refresh.svg';
export default class Alerts extends Component {
state = {
progress: 0,
selectedView: 'progress',
hasRefreshed: false
}
componentDidMount() {
this.props.onSelect('alerts');
this.startLoading()
}
startLoading() {
this.interval = setInterval(() => {
this.setState({ progress: this.state.progress + 1 }, () => {
if (this.state.progress === 100) {
clearInterval(this.interval);
this.setState({ selectedView: 'mechanical', progress: 0 });
}
})
}, 30)
}
onRefresh = () => {
this.setState({ selectedView: 'progress' }, () => {
this.interval = setInterval(() => {
this.setState({ progress: this.state.progress + 1 }, () => {
if (this.state.progress === 100) {
clearInterval(this.interval)
this.setState({ selectedView: 'environmental', progress: 0, hasRefreshed: true });
}
})
}, 30)
})
}
render() {
const { selectedView, progress, hasRefreshed } = this.state;
return (
<div className="Alerts">
<img src={TurbineImage} alt="turbine" className="turbine"/>
{selectedView === 'mechanical' && <>
<img src={LinePowerGenerated} alt="" className="line power-generated" />
<img src={LineOilTemperature} alt="" className="line oil-temperature" />
<img src={LineVibration} alt="" className="line vibration" />
<img src={LineSecurityAlert} alt="" className="line security" />
</>}
{selectedView === 'environmental' && <>
<img src={LineWindSpeed} alt="" className="line wind-speed" />
<img src={LineWindDirection} alt="" className="line wind-direction" />
</>}
{selectedView === 'progress' && <div className="loader">
<p>Running Query for Unit 34</p>
<div className="progress-border">
<div className="progress" style={{width: `${progress}%`}}></div>
</div>
</div>}
{selectedView !== 'progress' && <div className="content-wrap">
<>
<div className="menu">
<button onClick={() => this.setState({ selectedView: 'mechanical'})} className={selectedView === 'mechanical' ? 'selected' : ''}>Mechanical</button>
<button onClick={() => this.setState({ selectedView: 'environmental'})} className={selectedView === 'environmental' ? 'selected' : ''}>Environmental</button>
</div>
<button className="refresh" onClick={this.onRefresh}>
<img className="icon" src={RefreshIcon} alt="refresh" />
Refresh
</button>
</>
{selectedView === 'mechanical' && <div className="mechanical-wrap">
<div className="panel power">
<div className="panel-heading">
<h3>Power Generated</h3>
<span className="legend">
<div className="ball"></div>Expected
<div className="ball actual"></div>Actual
</span>
</div>
<SVG src={GraphPowerGenerated} alt="" className="graph power-generated" />
</div>
<div className="panel vibrations">
<div className="panel-heading">
<h3>Vibration</h3>
<span className="legend">
<div className="ball"></div>Expected
<div className="ball actual"></div>Actual
</span>
</div>
<SVG src={GraphVibration} alt="" className="graph vibration" />
</div>
<div className="panel security">
<div className="panel-heading">
<h3>Security Alert</h3>
</div>
<p className="padded">No events visible to current user</p>
</div>
<div className="panel oil">
<div className="panel-heading">
<h3>Oil Temperature</h3>
<span className="legend">
<div className="ball"></div>Expected
<div className="ball actual"></div>Actual
</span>
</div>
<SVG src={GraphOilTemperature} alt="" className="graph oil-temperature" />
</div>
<div className="stats-panels">
<div className="panel small">
<div className="panel-heading">
<h3>Fire</h3>
</div>
<p>None detected</p>
</div>
<div className="panel small">
<div className="panel-heading">
<h3>Malfunction</h3>
</div>
<p>None detected</p>
</div>
<div className="panel small">
<div className="panel-heading">
<h3>Part Failure</h3>
</div>
<p>None detected</p>
</div>
</div>
</div>}
{selectedView === 'environmental' && <div className="environmental-wrap">
<div className="left-panels">
<div className="panel">
<div className="panel-heading">
<h3>Wind Speed (farm)</h3>
</div>
<SVG src={GraphWindSpeedFarm} alt="" className="graph wind-speed-farm" />
</div>
<div className="panel">
<div className="panel-heading">
<h3>Wind Direction (farm)</h3>
</div>
<SVG src={GraphWindDirectionFarm} alt="" className="graph wind-direction-farm" />
</div>
<div className="bottom-panels">
<div className="panel">
<div className="panel-heading">
<h3>Temperature</h3>
</div>
<div className="stat">32°F</div>
</div>
<div className="panel">
<div className="panel-heading">
<h3>Humidity</h3>
</div>
<div className="stat">12%</div>
</div>
</div>
</div>
<div className="right-panels">
<div className="panel">
<div className="panel-heading">
<h3>Wind Speed (turbine)</h3>
</div>
<SVG src={hasRefreshed ? GraphWindSpeedTurbineV2 : GraphWindSpeedTurbine} alt="" className="graph wind-speed-turbine" />
</div>
<div className="panel">
<div className="panel-heading">
<h3>Wind Direction (turbine)</h3>
</div>
<SVG src={hasRefreshed ? GraphWindDirectionTurbineV2 : GraphWindDirectionTurbine} alt="" className="graph wind-direction-turbine" />
</div>
<div className="bottom-panels">
<div className="panel">
<div className="panel-heading">
<h3>Temperature</h3>
</div>
<div className="stat">32°F</div>
</div>
<div className="panel">
<div className="panel-heading">
<h3>Humidity</h3>
</div>
<div className="stat">12%</div>
</div>
</div>
</div>
</div>}
</div>}
</div>
)
}
}

Some files were not shown because too many files have changed in this diff Show More