diff --git a/.editorconfig b/.editorconfig index 9e56b25..82a1273 100644 --- a/.editorconfig +++ b/.editorconfig @@ -155,7 +155,7 @@ csharp_space_between_parentheses = false csharp_space_between_square_brackets = false # License header -file_header_template = Licensed to the .NET Foundation under one or more agreements.\nThe .NET Foundation licenses this file to you under the MIT license. +# file_header_template = Licensed to the .NET Foundation under one or more agreements.\nThe .NET Foundation licenses this file to you under the MIT license. # C++ Files [*.{cpp,h,in}] diff --git a/.gitignore b/.gitignore index 90ba8d2..a5934c1 100644 --- a/.gitignore +++ b/.gitignore @@ -351,3 +351,4 @@ MigrationBackup/ build/dev/ !build/dev/.gitkeep +dist/ diff --git a/build/Get-Cert-Content.ps1 b/build/Get-Cert-Content.ps1 new file mode 100644 index 0000000..ab85e95 --- /dev/null +++ b/build/Get-Cert-Content.ps1 @@ -0,0 +1,19 @@ +[CmdletBinding()] +param ( + [Parameter(Mandatory = $True)] + [string] + $Cert, + [Parameter(Mandatory = $True)] + [string] + $Pass +) + +$flag = [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::Exportable +$collection = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2Collection +$collection.Import($Cert, $Pass, $flag) +$pkcs12ContentType = [System.Security.Cryptography.X509Certificates.X509ContentType]::Pkcs12 +$clearBytes = $collection.Export($pkcs12ContentType) +$fileContentEncoded = [System.Convert]::ToBase64String($clearBytes) +$secret = ConvertTo-SecureString -String $fileContentEncoded -AsPlainText –Force + +Write-Host $fileContentEncoded diff --git a/build/pack-node.ps1 b/build/pack-node.ps1 new file mode 100644 index 0000000..93abd8a --- /dev/null +++ b/build/pack-node.ps1 @@ -0,0 +1,26 @@ +[CmdletBinding()] +param ( + [Parameter(Mandatory = $True)] + [string] + $Project +) + +Write-Host "Packing logic-node project at ``${Project}``" -ForegroundColor Blue +$RepositoryRoot = Resolve-Path (Join-Path $PSScriptRoot '../') +$SdkDir = Join-path $PSScriptRoot 'tools/gira' +$DistDir = Resolve-Path (Join-Path $PSScriptRoot '../dist') + +Write-Host "Repository: ``${RepositoryRoot}``" +Write-Host "SDK-Tools: ``${SdkDir}``" +Write-Host "Output: ``${DistDir}``" + +$NodeToolPath = Join-Path -Path $SdkDir -ChildPath 'LogicNodeTool.exe' + +$CreateNodeArgs = @( + 'create' + "${Project}" + "${DistDir}" +) + +Write-Host "${NodeToolPath} ${CreateNodeArgs}" -ForegroundColor Magenta +. $NodeToolPath $CreateNodeArgs diff --git a/build/sign-node.ps1 b/build/sign-node.ps1 new file mode 100644 index 0000000..ecefbd2 --- /dev/null +++ b/build/sign-node.ps1 @@ -0,0 +1,42 @@ +[CmdletBinding()] +param ( + [Parameter(Mandatory = $True)] + [string] + $ZipFile, + [Parameter(Mandatory = $True)] + [string] + $Cert, + [Parameter(Mandatory = $True)] + [string] + $Pass +) + +$Location = Get-Location +Write-Host "Signing logic-node project at ``${ZipFile}``" -ForegroundColor Blue +$RepositoryRoot = Resolve-Path (Join-Path $PSScriptRoot '../') +$ZipFile = Resolve-Path $ZipFile +$Cert = Resolve-Path $Cert +$SdkDir = Join-path $PSScriptRoot 'tools/gira' +$DistDir = Resolve-Path (Join-Path $PSScriptRoot '../dist') + +Write-Host "Repository: ``${RepositoryRoot}``" +Write-Host "SDK-Tools: ``${SdkDir}``" +Write-Host "Output: ``${DistDir}``" + +$SignToolPath = Join-Path -Path $SdkDir -ChildPath 'SignLogicNodes.exe' + +Set-Location $SdkDir + +try { + $SignNodeArgs = @( + "${Cert}" + "${Pass}" + "${ZipFile}" + ) + + Write-Host "${SignToolPath} ${SignNodeArgs}" -ForegroundColor Magenta + . $SignToolPath $SignNodeArgs +} +finally { + Set-Location $Location +} diff --git a/dotnet/src/LogicNodesSDK/LogicModule.SignatureHelper.dll b/build/tools/gira/LogicModule.SignatureHelper.dll similarity index 100% rename from dotnet/src/LogicNodesSDK/LogicModule.SignatureHelper.dll rename to build/tools/gira/LogicModule.SignatureHelper.dll diff --git a/dotnet/src/LogicNodesSDK/LogicNodeTool.exe b/build/tools/gira/LogicNodeTool.exe similarity index 100% rename from dotnet/src/LogicNodesSDK/LogicNodeTool.exe rename to build/tools/gira/LogicNodeTool.exe diff --git a/dotnet/src/LogicNodesSDK/LogicNodeTool.exe.config b/build/tools/gira/LogicNodeTool.exe.config similarity index 100% rename from dotnet/src/LogicNodesSDK/LogicNodeTool.exe.config rename to build/tools/gira/LogicNodeTool.exe.config diff --git a/dotnet/src/LogicNodesSDK/NJsonSchema.LICENSE.txt b/build/tools/gira/NJsonSchema.LICENSE.txt similarity index 100% rename from dotnet/src/LogicNodesSDK/NJsonSchema.LICENSE.txt rename to build/tools/gira/NJsonSchema.LICENSE.txt diff --git a/dotnet/src/LogicNodesSDK/NJsonSchema.dll b/build/tools/gira/NJsonSchema.dll similarity index 100% rename from dotnet/src/LogicNodesSDK/NJsonSchema.dll rename to build/tools/gira/NJsonSchema.dll diff --git a/dotnet/src/LogicNodesSDK/Newtonsoft.Json.LICENSE.md b/build/tools/gira/Newtonsoft.Json.LICENSE.md similarity index 100% rename from dotnet/src/LogicNodesSDK/Newtonsoft.Json.LICENSE.md rename to build/tools/gira/Newtonsoft.Json.LICENSE.md diff --git a/dotnet/src/LogicNodesSDK/Newtonsoft.Json.dll b/build/tools/gira/Newtonsoft.Json.dll similarity index 100% rename from dotnet/src/LogicNodesSDK/Newtonsoft.Json.dll rename to build/tools/gira/Newtonsoft.Json.dll diff --git a/dotnet/src/LogicNodesSDK/SharpCompress.LICENSE.txt b/build/tools/gira/SharpCompress.LICENSE.txt similarity index 100% rename from dotnet/src/LogicNodesSDK/SharpCompress.LICENSE.txt rename to build/tools/gira/SharpCompress.LICENSE.txt diff --git a/dotnet/src/LogicNodesSDK/SharpCompress.dll b/build/tools/gira/SharpCompress.dll similarity index 100% rename from dotnet/src/LogicNodesSDK/SharpCompress.dll rename to build/tools/gira/SharpCompress.dll diff --git a/dotnet/src/LogicNodesSDK/SignLogicNodes.exe b/build/tools/gira/SignLogicNodes.exe similarity index 100% rename from dotnet/src/LogicNodesSDK/SignLogicNodes.exe rename to build/tools/gira/SignLogicNodes.exe diff --git a/dotnet/src/LogicNodesSDK/SignLogicNodes.exe.config b/build/tools/gira/SignLogicNodes.exe.config similarity index 100% rename from dotnet/src/LogicNodesSDK/SignLogicNodes.exe.config rename to build/tools/gira/SignLogicNodes.exe.config diff --git a/dotnet/src/LogicNodesSDK/gira_manifest_schema.json b/build/tools/gira/gira_manifest_schema.json similarity index 100% rename from dotnet/src/LogicNodesSDK/gira_manifest_schema.json rename to build/tools/gira/gira_manifest_schema.json diff --git a/dist/.gitkeep b/dist/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/dist/Necati_Meral_Yahoo_De.Logic.Doorbird-1.0.0.zip b/dist/Necati_Meral_Yahoo_De.Logic.Doorbird-1.0.0.zip new file mode 100644 index 0000000..d8ef8e6 Binary files /dev/null and b/dist/Necati_Meral_Yahoo_De.Logic.Doorbird-1.0.0.zip differ diff --git a/dotnet/Directory.Build.props b/dotnet/Directory.Build.props index 1d82fa8..5df9e3c 100644 --- a/dotnet/Directory.Build.props +++ b/dotnet/Directory.Build.props @@ -9,19 +9,64 @@ + Necati_Meral_Yahoo_De + necati_meral_yahoo_de + + + true + + true + + $(MSBuildThisFileDirectory) - + + + + + + + + - + + + + + - + + + $(SolutionDir)src\LogicNodesSDK\LogicModule.Nodes.Helpers.dll + + + $(SolutionDir)src\LogicNodesSDK\LogicModule.ObjectModel.dll + + + + + + $(SolutionDir)src\LogicNodesSDK\LogicModule.Nodes.TestHelper.dll + + all runtime; build; native; contentfiles; analyzers @@ -31,9 +76,10 @@ + - + 1.0.3 @@ -53,6 +99,8 @@ 2.4.1 + 4.18.4 + diff --git a/dotnet/NecatiMeral.LogicNodes.sln b/dotnet/NecatiMeral.LogicNodes.sln index a246658..27be8c6 100644 --- a/dotnet/NecatiMeral.LogicNodes.sln +++ b/dotnet/NecatiMeral.LogicNodes.sln @@ -9,7 +9,15 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{B4F57F38-7B5 EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{1EE7FB73-9DEA-4D45-9CD0-2F4BFE480FF6}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NecatiMeral.LogicNodes.Tests", "test\NecatiMeral.LogicNodes.Tests\NecatiMeral.LogicNodes.Tests.csproj", "{8A4DFEEE-CC9F-467E-8903-ED93D272D884}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NecatiMeral.LogicNodes.Tests", "test\NecatiMeral.LogicNodes.Tests\NecatiMeral.LogicNodes.Tests.csproj", "{8A4DFEEE-CC9F-467E-8903-ED93D272D884}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Projektmappenelemente", "Projektmappenelemente", "{40954188-75ED-47A1-9E72-5B1B3F7EA544}" + ProjectSection(SolutionItems) = preProject + Directory.Build.props = Directory.Build.props + logic-node.props = logic-node.props + EndProjectSection +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NecatiMeral.Logic.ComfortOnline", "src\NecatiMeral.Logic.ComfortOnline\NecatiMeral.Logic.ComfortOnline.csproj", "{90731595-1712-4F0C-82C9-56408D26064E}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -25,6 +33,10 @@ Global {8A4DFEEE-CC9F-467E-8903-ED93D272D884}.Debug|Any CPU.Build.0 = Debug|Any CPU {8A4DFEEE-CC9F-467E-8903-ED93D272D884}.Release|Any CPU.ActiveCfg = Release|Any CPU {8A4DFEEE-CC9F-467E-8903-ED93D272D884}.Release|Any CPU.Build.0 = Release|Any CPU + {90731595-1712-4F0C-82C9-56408D26064E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {90731595-1712-4F0C-82C9-56408D26064E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {90731595-1712-4F0C-82C9-56408D26064E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {90731595-1712-4F0C-82C9-56408D26064E}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -32,6 +44,7 @@ Global GlobalSection(NestedProjects) = preSolution {3F1FF768-FD27-46A2-961C-B6EFDF9CB3B3} = {B4F57F38-7B57-4225-9BD4-264ABC47DE33} {8A4DFEEE-CC9F-467E-8903-ED93D272D884} = {1EE7FB73-9DEA-4D45-9CD0-2F4BFE480FF6} + {90731595-1712-4F0C-82C9-56408D26064E} = {B4F57F38-7B57-4225-9BD4-264ABC47DE33} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {471310B7-D205-4EEC-A063-0A534164AB9A} diff --git a/dotnet/logic-node.props b/dotnet/logic-node.props new file mode 100644 index 0000000..56c73a5 --- /dev/null +++ b/dotnet/logic-node.props @@ -0,0 +1,14 @@ + + + + + + + + + diff --git a/dotnet/src/NecatiMeral.Logic.ComfortOnline/ComfortOnlineConsts.cs b/dotnet/src/NecatiMeral.Logic.ComfortOnline/ComfortOnlineConsts.cs new file mode 100644 index 0000000..ebbab5a --- /dev/null +++ b/dotnet/src/NecatiMeral.Logic.ComfortOnline/ComfortOnlineConsts.cs @@ -0,0 +1,15 @@ +namespace Necati_Meral_Yahoo_De.Logic.ComfortOnline; +public static class ComfortOnlineConsts +{ + public const string ComfortOnlineBaseAddress = "https://www.comfort-online.com/"; + + public static class ErrorCodes + { + public const string Ok = "Ok"; + public const string InitialRequestFailed = "InitialRequestFailed"; + public const string MissingRequestVerificationToken = "MissingRequestVerificationToken"; + public const string InvalidCredentials = "InvalidCredentials"; + public const string LoginFailed = "LoginFailed"; + public const string UnexpectedError = "UnexpectedError: "; + } +} diff --git a/dotnet/src/NecatiMeral.Logic.ComfortOnline/ComfortOnlinePageParser.cs b/dotnet/src/NecatiMeral.Logic.ComfortOnline/ComfortOnlinePageParser.cs new file mode 100644 index 0000000..ddc4e47 --- /dev/null +++ b/dotnet/src/NecatiMeral.Logic.ComfortOnline/ComfortOnlinePageParser.cs @@ -0,0 +1,58 @@ +using System.Text.RegularExpressions; + +namespace Necati_Meral_Yahoo_De.Logic.ComfortOnline; +public class ComfortOnlinePageParser +{ + readonly Regex _spanRegex = new Regex("(.*)<\\/span>", RegexOptions.Compiled | RegexOptions.IgnoreCase); + readonly Regex _inputRegex = new Regex("]*>", RegexOptions.Compiled | RegexOptions.IgnoreCase); + + public ComfortOnlinePlantSectionInfo Parse(string body) + { + var info = new ComfortOnlinePlantSectionInfo(); + + var spanMatches = _spanRegex.Matches(body); + foreach (Match match in spanMatches) + { + if(match.Groups.Count == 3) + { + info[match.Groups[1].Value] = match.Groups[2].Value; + } + } + + var inputMatches = _inputRegex.Matches(body); + foreach (Match match in inputMatches) + { + if (match.Groups.Count == 5) + { + if (string.IsNullOrEmpty(match.Groups[1].Value)) + { + info[match.Groups[3].Value] = match.Groups[4].Value; + } + else + { + info[match.Groups[1].Value] = match.Groups[2].Value; + } + } + } + + return info; + } + + public class ComfortOnlinePlantSectionInfo + { + Dictionary _dict; + + public IReadOnlyDictionary Values => _dict; + + public string this[string key] + { + get => _dict[key]; + set => _dict[key] = value; + } + + public ComfortOnlinePlantSectionInfo() + { + _dict = new Dictionary(); + } + } +} diff --git a/dotnet/src/NecatiMeral.Logic.ComfortOnline/ComfortOnlineRequestNode.cs b/dotnet/src/NecatiMeral.Logic.ComfortOnline/ComfortOnlineRequestNode.cs new file mode 100644 index 0000000..415b947 --- /dev/null +++ b/dotnet/src/NecatiMeral.Logic.ComfortOnline/ComfortOnlineRequestNode.cs @@ -0,0 +1,153 @@ +using System.CodeDom; +using System.Net; +using System.Net.Http; +using System.Text.RegularExpressions; +using LogicModule.ObjectModel.TypeSystem; +using Necati_Meral_Yahoo_De.Helpers; +using Necati_Meral_Yahoo_De.Http; +using Necati_Meral_Yahoo_De.LogicNodes; + +namespace Necati_Meral_Yahoo_De.Logic.ComfortOnline; +public class ComfortOnlineRequestNode : LocalizablePrefixLogicNodeBase +{ + protected ITypeService TypeService { get; } + protected ComfortOnlinePageParser Parser { get; } + protected IHttpClient HttpClient { get; } + + [Input(DisplayOrder = 1, IsRequired = true)] + public BoolValueObject Trigger { get; private set; } + + [Input(IsDefaultShown = true, DisplayOrder = 2)] + public StringValueObject PlantId { get; } + + [Input(IsDefaultShown = true, DisplayOrder = 3)] + public StringValueObject PlantSection { get; } + + [Input(IsDefaultShown = false, DisplayOrder = 4)] + public StringValueObject UserName { get; } + + [Input(IsDefaultShown = false, DisplayOrder = 5)] + public StringValueObject Password { get; } + + [Output(IsDefaultShown = true, DisplayOrder = 1)] + public StringValueObject Data { get; private set; } + + [Output(IsDefaultShown = false, DisplayOrder = 99)] + public StringValueObject Diagnostics { get; private set; } + + public ComfortOnlineRequestNode(INodeContext context) + : base(context, LogicNodeConsts.InputPrefix) + { + context.ThrowIfNull("context"); + + TypeService = context.GetService(); + Parser = new ComfortOnlinePageParser(); + + Trigger = TypeService.CreateBool("BINARY", "Trigger", false); + PlantId = TypeService.CreateString("STRING", "PlantId", string.Empty); + PlantSection = TypeService.CreateString("STRING", "PlantSection", string.Empty); + UserName = TypeService.CreateString("STRING", "UserName", string.Empty); + Password = TypeService.CreateString("STRING", "Password", string.Empty); + Data = TypeService.CreateString("STRING", "Data", string.Empty); + Diagnostics = TypeService.CreateString("STRING", "Diagnostics", string.Empty); + + HttpClient = CreateHttpClient(); + } + + public override void Execute() + { + if(Trigger.HasValue && Trigger.WasSet) + { + try + { + AsyncHelper.RunSync(ExecuteAsync); + } + catch (AggregateException ex) + { + Diagnostics.Value = $"{ComfortOnlineConsts.ErrorCodes.UnexpectedError} {ex.Message}"; + throw ex.InnerException; + } + } + } + + protected virtual async Task ExecuteAsync() + { + var loginSucceeded = await LoginAsync(); + if (loginSucceeded) + { + var plantSectionResponse = await HttpClient.GetStringAsync($"/Measurand/Values?plant={PlantId.Value}&name={PlantSection.Value}"); + var parsed = Parser.Parse(plantSectionResponse); + + var entries = parsed.Values.Select(p => string.Format("\"{0}\": \"{1}\"", p.Key, string.Join(",", p.Value))); + + Data.Value = $"{{{string.Join(", ", entries)}}}"; + Diagnostics.Value = ComfortOnlineConsts.ErrorCodes.Ok; + } + } + + protected virtual async Task LoginAsync() + { + string initialRequestBody; + try + { + initialRequestBody = await HttpClient.GetStringAsync("/Account/Login"); + } + catch (Exception) + { + Diagnostics.Value = ComfortOnlineConsts.ErrorCodes.InitialRequestFailed; + return false; + } + + var requestVerificationToken = GetRequestVerificationToken(initialRequestBody); + if (string.IsNullOrEmpty(requestVerificationToken)) + { + Diagnostics.Value = ComfortOnlineConsts.ErrorCodes.MissingRequestVerificationToken; + return false; + } + + try + { + var loginPage = await HttpClient.PostAsync("/Account/Login", new Dictionary + { + { "UserName", UserName.Value }, + { "Password", Password.Value }, + { "__RequestVerificationToken", requestVerificationToken } + }); + + // Basic detection if we got redirected to login page = login failed + if (loginPage.Contains("
]*>"); + if (matches.Success) + { + return matches.Groups[1].Value; + } + return string.Empty; + } +} diff --git a/dotnet/src/NecatiMeral.Logic.ComfortOnline/Manifest.json b/dotnet/src/NecatiMeral.Logic.ComfortOnline/Manifest.json new file mode 100644 index 0000000..3a97acc --- /dev/null +++ b/dotnet/src/NecatiMeral.Logic.ComfortOnline/Manifest.json @@ -0,0 +1,33 @@ +{ + "PackageFormatVersion": "1.0", + "Assembly": "Necati_Meral_Yahoo_De.Logic.ComfortOnline.dll", + "PackageName": { + "en": "ComfortOnline nodes" + }, + "DependentFiles": [ + "NecatiMeral.LogicNodes.dll" + ], + "Version": "1.0.0", + "Author": "Necati Meral", + "Copyright": "COPYRIGHT", + "DeveloperId": "necati_meral_yahoo_de", + "License": "Free", + "PackageId": "CB18E39C-C1A8-421F-882A-4E3B4B36A999", + "Nodes": [ + { + "Type": "Necati_Meral_Yahoo_De.Logic.ComfortOnline.ComfortOnlineRequestNode", + "Name": { + "en": "ComfortOnline DataPoint", + "de": "ComfortOnline Eingang" + }, + "IsConverter": false, + "Category": "Node", + "DefaultIcon": "icons/kwb-node-logo.png", + "HelpTooltip": { + "en": "Loads asset information from ComfortOnline and provides it as JSON.", + "de": "Lädt Anlageninformationen aus ComfortOnline und stellt diese als JSON bereit." + }, + "HelpFileReference": null + } + ] +} diff --git a/dotnet/src/NecatiMeral.Logic.ComfortOnline/NecatiMeral.Logic.ComfortOnline.csproj b/dotnet/src/NecatiMeral.Logic.ComfortOnline/NecatiMeral.Logic.ComfortOnline.csproj new file mode 100644 index 0000000..4aac14d --- /dev/null +++ b/dotnet/src/NecatiMeral.Logic.ComfortOnline/NecatiMeral.Logic.ComfortOnline.csproj @@ -0,0 +1,37 @@ + + + + + + net48 + enable + Necati_Meral_Yahoo_De.Logic.ComfortOnline + Necati_Meral_Yahoo_De.Logic.ComfortOnline + + + + + + + + + + + + + PreserveNewest + + + PreserveNewest + + + + + + + \ No newline at end of file diff --git a/dotnet/src/NecatiMeral.Logic.ComfortOnline/NetHttpClient.cs b/dotnet/src/NecatiMeral.Logic.ComfortOnline/NetHttpClient.cs new file mode 100644 index 0000000..ee7d7b1 --- /dev/null +++ b/dotnet/src/NecatiMeral.Logic.ComfortOnline/NetHttpClient.cs @@ -0,0 +1,26 @@ +using System.Net.Http; +using Necati_Meral_Yahoo_De.Http; + +namespace Necati_Meral_Yahoo_De.Logic.ComfortOnline; +public class NetHttpClient : IHttpClient +{ + protected HttpClient HttpClient { get; } + + public NetHttpClient(HttpClient httpClient) + { + HttpClient = httpClient; + } + + public async Task GetStringAsync(string requestUri) + { + return await HttpClient.GetStringAsync(requestUri).ConfigureAwait(false); + } + + public async Task PostAsync(string requestUri, IDictionary formData) + { + var formContent = new FormUrlEncodedContent(formData); + + var response = await HttpClient.PostAsync("/Account/Login", formContent).ConfigureAwait(false); + return await response.Content.ReadAsStringAsync().ConfigureAwait(false); + } +} diff --git a/dotnet/src/NecatiMeral.Logic.ComfortOnline/icons/kwb-node-logo.png b/dotnet/src/NecatiMeral.Logic.ComfortOnline/icons/kwb-node-logo.png new file mode 100644 index 0000000..9edd8fb Binary files /dev/null and b/dotnet/src/NecatiMeral.Logic.ComfortOnline/icons/kwb-node-logo.png differ diff --git a/dotnet/src/NecatiMeral.LogicNodes/Helpers/AsyncHelper.cs b/dotnet/src/NecatiMeral.LogicNodes/Helpers/AsyncHelper.cs new file mode 100644 index 0000000..5a32758 --- /dev/null +++ b/dotnet/src/NecatiMeral.LogicNodes/Helpers/AsyncHelper.cs @@ -0,0 +1,131 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace Necati_Meral_Yahoo_De.Helpers; +public static class AsyncHelper +{ + /// + /// Execute's an async Task method which has a void return value synchronously + /// + /// Task method to execute + public static void RunSync(Func task) + { + var oldContext = SynchronizationContext.Current; + var synch = new ExclusiveSynchronizationContext(); + SynchronizationContext.SetSynchronizationContext(synch); + synch.Post(async _ => + { + try + { + await task(); + } + catch (Exception e) + { + synch.InnerException = e; + throw; + } + finally + { + synch.EndMessageLoop(); + } + }, null); + synch.BeginMessageLoop(); + + SynchronizationContext.SetSynchronizationContext(oldContext); + } + + /// + /// Execute's an async Task method which has a T return type synchronously + /// + /// Return Type + /// Task method to execute + /// + public static T RunSync(Func> task) + { + var oldContext = SynchronizationContext.Current; + var synch = new ExclusiveSynchronizationContext(); + SynchronizationContext.SetSynchronizationContext(synch); + T ret = default(T); + synch.Post(async _ => + { + try + { + ret = await task(); + } + catch (Exception e) + { + synch.InnerException = e; + throw; + } + finally + { + synch.EndMessageLoop(); + } + }, null); + synch.BeginMessageLoop(); + SynchronizationContext.SetSynchronizationContext(oldContext); + return ret; + } + + private class ExclusiveSynchronizationContext : SynchronizationContext + { + private bool done; + public Exception InnerException { get; set; } + readonly AutoResetEvent workItemsWaiting = new AutoResetEvent(false); + readonly Queue> items = + new Queue>(); + + public override void Send(SendOrPostCallback d, object state) + { + throw new NotSupportedException("We cannot send to our same thread"); + } + + public override void Post(SendOrPostCallback d, object state) + { + lock (items) + { + items.Enqueue(Tuple.Create(d, state)); + } + workItemsWaiting.Set(); + } + + public void EndMessageLoop() + { + Post(_ => done = true, null); + } + + public void BeginMessageLoop() + { + while (!done) + { + Tuple task = null; + lock (items) + { + if (items.Count > 0) + { + task = items.Dequeue(); + } + } + if (task != null) + { + task.Item1(task.Item2); + if (InnerException != null) // the method threw an exeption + { + throw new AggregateException("AsyncHelpers.Run method threw an exception.", InnerException); + } + } + else + { + workItemsWaiting.WaitOne(); + } + } + } + + public override SynchronizationContext CreateCopy() + { + return this; + } + } +} \ No newline at end of file diff --git a/dotnet/src/NecatiMeral.LogicNodes/Helpers/EnvironmentHelper.cs b/dotnet/src/NecatiMeral.LogicNodes/Helpers/EnvironmentHelper.cs new file mode 100644 index 0000000..257f312 --- /dev/null +++ b/dotnet/src/NecatiMeral.LogicNodes/Helpers/EnvironmentHelper.cs @@ -0,0 +1,10 @@ +using System; + +namespace Necati_Meral_Yahoo_De.LogicNodes.Helpers; +public static class EnvironmentHelper +{ + public static bool IsSimulation() + { + return Environment.OSVersion.VersionString.ToUpperInvariant().Contains("WIN"); + } +} diff --git a/dotnet/src/NecatiMeral.LogicNodes/Http/IHttpClient.cs b/dotnet/src/NecatiMeral.LogicNodes/Http/IHttpClient.cs new file mode 100644 index 0000000..5330d2e --- /dev/null +++ b/dotnet/src/NecatiMeral.LogicNodes/Http/IHttpClient.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Necati_Meral_Yahoo_De.Http; +public interface IHttpClient +{ + Task GetStringAsync(string requestUri); + Task PostAsync(string requestUri, IDictionary formData); +} diff --git a/dotnet/src/NecatiMeral.LogicNodes/LogicNodeConsts.cs b/dotnet/src/NecatiMeral.LogicNodes/LogicNodeConsts.cs new file mode 100644 index 0000000..3d47aa4 --- /dev/null +++ b/dotnet/src/NecatiMeral.LogicNodes/LogicNodeConsts.cs @@ -0,0 +1,5 @@ +namespace Necati_Meral_Yahoo_De.LogicNodes; +public static class LogicNodeConsts +{ + public const string InputPrefix = "Input"; +} diff --git a/dotnet/src/NecatiMeral.LogicNodes/NecatiMeral.LogicNodes.csproj b/dotnet/src/NecatiMeral.LogicNodes/NecatiMeral.LogicNodes.csproj index 207ce16..2284ff0 100644 --- a/dotnet/src/NecatiMeral.LogicNodes/NecatiMeral.LogicNodes.csproj +++ b/dotnet/src/NecatiMeral.LogicNodes/NecatiMeral.LogicNodes.csproj @@ -1,38 +1,17 @@ - + + + + AnyCPU {3F1FF768-FD27-46A2-961C-B6EFDF9CB3B3} Library - net45 + net48 - - - ..\LogicNodesSDK\LogicModule.Nodes.Helpers.dll - - - ..\LogicNodesSDK\LogicModule.ObjectModel.dll - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - PreserveNewest - - - diff --git a/dotnet/src/NecatiMeral.LogicNodes/Node.cs b/dotnet/src/NecatiMeral.LogicNodes/Node.cs index 20c76f1..5ca3f8d 100644 --- a/dotnet/src/NecatiMeral.LogicNodes/Node.cs +++ b/dotnet/src/NecatiMeral.LogicNodes/Node.cs @@ -1,4 +1,4 @@ -namespace NecatiMeral.LogicNodes; +namespace Necati_Meral_Yahoo_De.LogicNodes; public class Node : LogicNodeBase { diff --git a/dotnet/test/NecatiMeral.LogicNodes.Tests/ComfortOnlineListenerNodeTests.cs b/dotnet/test/NecatiMeral.LogicNodes.Tests/ComfortOnlineListenerNodeTests.cs new file mode 100644 index 0000000..53b1572 --- /dev/null +++ b/dotnet/test/NecatiMeral.LogicNodes.Tests/ComfortOnlineListenerNodeTests.cs @@ -0,0 +1,61 @@ +using Necati_Meral_Yahoo_De.Logic.ComfortOnline; +using Necati_Meral_Yahoo_De.LogicNodes.Tests; +using Shouldly; + +namespace Necati_Meral_Yahoo_De; +public class ComfortOnlineListenerNodeTests : NodeTestBase +{ + const string PlantId = "10458"; + const string PlantSectionId = "96_0"; + + [Fact] + public void Should_Fail_Login() + { + var node = CreateNode(); + + node.Trigger.Value = true; + node.PlantId.Value = PlantId; + node.PlantSection.Value = PlantSectionId; + + node.Execute(); + + node.Diagnostics.Value.ShouldBe(ComfortOnlineConsts.ErrorCodes.InvalidCredentials); + } + + [Fact] + public void Should_Login() + { + var node = CreateNode(); + + node.Trigger.Value = true; + node.UserName.Value = ComfortOnlineTestConsts.UserName; + node.Password.Value = ComfortOnlineTestConsts.Password; + node.PlantId.Value = PlantId; + node.PlantSection.Value = PlantSectionId; + + node.Execute(); + + node.Diagnostics.Value.ShouldBe(ComfortOnlineConsts.ErrorCodes.Ok); + } + + [Fact] + public void Should_Recover_From_Error() + { + var node = CreateNode(); + + node.Trigger.Value = true; + node.PlantId.Value = PlantId; + node.PlantSection.Value = PlantSectionId; + + node.Execute(); + + node.Diagnostics.Value.ShouldBe(ComfortOnlineConsts.ErrorCodes.InvalidCredentials); + + node.UserName.Value = ComfortOnlineTestConsts.UserName; + node.Password.Value = ComfortOnlineTestConsts.Password; + + node.Execute(); + + node.Diagnostics.Value.ShouldBe(ComfortOnlineConsts.ErrorCodes.Ok); + } +} diff --git a/dotnet/test/NecatiMeral.LogicNodes.Tests/ComfortOnlineTestConsts.cs b/dotnet/test/NecatiMeral.LogicNodes.Tests/ComfortOnlineTestConsts.cs new file mode 100644 index 0000000..d3abf72 --- /dev/null +++ b/dotnet/test/NecatiMeral.LogicNodes.Tests/ComfortOnlineTestConsts.cs @@ -0,0 +1,9 @@ +using System.CodeDom; + +namespace Necati_Meral_Yahoo_De; +public static class ComfortOnlineTestConsts +{ + public const string UserName = "a-mocked-username"; + public const string Password = "a-mocked-password"; + public const string RequestVerificationToken = "k9M5dwHB90NMdg1A7RP0J2J70kxsr-6mLSQDsNblKmMpUztM5TkihU2T6rjgrmcKKWe7o8UHatDZ18rHw4I7ux37QvwhccoAKzq610iI9Qw1"; +} diff --git a/dotnet/test/NecatiMeral.LogicNodes.Tests/NecatiMeral.LogicNodes.Tests.csproj b/dotnet/test/NecatiMeral.LogicNodes.Tests/NecatiMeral.LogicNodes.Tests.csproj index 16b3d0b..81d997b 100644 --- a/dotnet/test/NecatiMeral.LogicNodes.Tests/NecatiMeral.LogicNodes.Tests.csproj +++ b/dotnet/test/NecatiMeral.LogicNodes.Tests/NecatiMeral.LogicNodes.Tests.csproj @@ -4,8 +4,13 @@ AnyCPU {8A4DFEEE-CC9F-467E-8903-ED93D272D884} Library - net45 + net48 + + + + + @@ -15,12 +20,12 @@ - - ..\..\src\LogicNodesSDK\LogicModule.Nodes.TestHelper.dll - - - ..\..\src\LogicNodesSDK\LogicModule.ObjectModel.dll - + + + + + + diff --git a/dotnet/test/NecatiMeral.LogicNodes.Tests/NodeTest.cs b/dotnet/test/NecatiMeral.LogicNodes.Tests/NodeTest.cs deleted file mode 100644 index 7c3fa32..0000000 --- a/dotnet/test/NecatiMeral.LogicNodes.Tests/NodeTest.cs +++ /dev/null @@ -1,20 +0,0 @@ -using LogicModule.Nodes.TestHelper; -using LogicModule.ObjectModel; - -namespace NecatiMeral.LogicNodes.Tests; - -public class NodeTest -{ - protected INodeContext Context { get; } - - public NodeTest() - { - Context = TestNodeContext.Create(); - } - - [Fact] - public void YourTest() - { - - } -} diff --git a/dotnet/test/NecatiMeral.LogicNodes.Tests/NodeTestBase.cs b/dotnet/test/NecatiMeral.LogicNodes.Tests/NodeTestBase.cs new file mode 100644 index 0000000..94c4846 --- /dev/null +++ b/dotnet/test/NecatiMeral.LogicNodes.Tests/NodeTestBase.cs @@ -0,0 +1,20 @@ +using System; +using LogicModule.Nodes.TestHelper; + +namespace Necati_Meral_Yahoo_De.LogicNodes.Tests; + +public abstract class NodeTestBase + where TNode : ILogicNode +{ + protected INodeContext Context { get; } + + protected NodeTestBase() + { + Context = TestNodeContext.Create(); + } + + protected TNode CreateNode() + { + return (TNode)Activator.CreateInstance(typeof(TNode), new object[] { Context }); + } +} diff --git a/dotnet/test/NecatiMeral.LogicNodes.Tests/TestComfortOnlineRequestNode.cs b/dotnet/test/NecatiMeral.LogicNodes.Tests/TestComfortOnlineRequestNode.cs new file mode 100644 index 0000000..ef80182 --- /dev/null +++ b/dotnet/test/NecatiMeral.LogicNodes.Tests/TestComfortOnlineRequestNode.cs @@ -0,0 +1,57 @@ +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using System.Threading.Tasks; +using Moq; +using Necati_Meral_Yahoo_De.Http; +using Necati_Meral_Yahoo_De.Logic.ComfortOnline; + +namespace Necati_Meral_Yahoo_De; +public class TestComfortOnlineRequestNode : ComfortOnlineRequestNode +{ + public TestComfortOnlineRequestNode(INodeContext context) + : base(context) + { + } + + protected override IHttpClient CreateHttpClient() + { + var httpClientMock = new Mock(); + + httpClientMock.Setup(c => c.GetStringAsync(It.Is(x => x.EndsWith("/Account/Login") ) )) + .Returns(() => GetEmbeddedResourceContentAsync("Login")); + + httpClientMock.Setup(c => c.GetStringAsync(It.Is(x => x.Contains("/Measurand/Values")))) + .Returns(() => GetEmbeddedResourceContentAsync("Measurand-Values")); + + httpClientMock.Setup(c => c.PostAsync( + It.Is(x => x.EndsWith("/Account/Login")), + It.Is>(x => + x.ContainsKey("UserName") && x["UserName"] == ComfortOnlineTestConsts.UserName && + x.ContainsKey("Password") && x["Password"] == ComfortOnlineTestConsts.Password && + x.ContainsKey("__RequestVerificationToken") && x["__RequestVerificationToken"] == ComfortOnlineTestConsts.RequestVerificationToken + ))) + .Returns(() => GetEmbeddedResourceContentAsync("Plant-List")); + + httpClientMock.Setup(c => c.PostAsync( + It.Is(x => x.EndsWith("/Account/Login")), + It.Is>(x => + !x.ContainsKey("UserName") || x["UserName"] != ComfortOnlineTestConsts.UserName || + !x.ContainsKey("Password") || x["Password"] != ComfortOnlineTestConsts.Password || + !x.ContainsKey("__RequestVerificationToken") || x["__RequestVerificationToken"] != ComfortOnlineTestConsts.RequestVerificationToken + ))) + .Returns(() => GetEmbeddedResourceContentAsync("Login")); + + return httpClientMock.Object; + } + + protected virtual async Task GetEmbeddedResourceContentAsync(string file) + { + var fullFileName = $"{typeof(TestComfortOnlineRequestNode).Namespace}.TestData.{file}.html"; + using var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(fullFileName); + using var reader = new StreamReader(stream); + + return await reader.ReadToEndAsync().ConfigureAwait(false); + } +} diff --git a/dotnet/test/NecatiMeral.LogicNodes.Tests/TestData/Login.html b/dotnet/test/NecatiMeral.LogicNodes.Tests/TestData/Login.html new file mode 100644 index 0000000..8b7a727 --- /dev/null +++ b/dotnet/test/NecatiMeral.LogicNodes.Tests/TestData/Login.html @@ -0,0 +1,271 @@ + + + + + + + + + Comfort Online + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+

+ Ja Nein +
+ +
+

+

+ + +

+ Bestätigen Abbrechen +
+ + + diff --git a/dotnet/test/NecatiMeral.LogicNodes.Tests/TestData/Measurand-Values.html b/dotnet/test/NecatiMeral.LogicNodes.Tests/TestData/Measurand-Values.html new file mode 100644 index 0000000..62cd8e9 --- /dev/null +++ b/dotnet/test/NecatiMeral.LogicNodes.Tests/TestData/Measurand-Values.html @@ -0,0 +1,1069 @@ + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + +
+ + Menü + + +

+ KWB EF2 - Kessel / Plant = 10458 +

+ +
+ +
+ + + + + + + + + + + + + + + + +
+ +
+ +
+ + + + +
+ + + +
+ +
+ + +
+ + + + + + +
+ Dieser Bereich ist für Diagramme reserviert +
+ + + + + + + + + + + +
+ +
+
+ + + + + + + + + +
+ +
+
+
+ + + © KWB Impressum +
+
+ Necati_meral@yahoo.de (Basis - Bediener) + + W. Europe Standard Time + (UTC 1h) + +
+
+
+ +
+ + + + +

Menü

+ +
+ + + + + + + + + + +
+ + + diff --git a/dotnet/test/NecatiMeral.LogicNodes.Tests/TestData/Plant-List.html b/dotnet/test/NecatiMeral.LogicNodes.Tests/TestData/Plant-List.html new file mode 100644 index 0000000..2b7a6a9 --- /dev/null +++ b/dotnet/test/NecatiMeral.LogicNodes.Tests/TestData/Plant-List.html @@ -0,0 +1,518 @@ + + + + + + + + +
+ + + + + + +
+ + Menü + + +

+ KWB EF2 +

+ +
+ + + +
+
+
+ + + + © KWB Impressum +
+
+ Necati_meral@yahoo.de (Basis - Bediener) + + W. Europe Standard Time + (UTC 1h) + +
+
+
+ +
+ + + + +

Menü

+ +
+ + + + + + + + + + +
+ + + diff --git a/images/kwb-logo.psd b/images/kwb-logo.psd new file mode 100644 index 0000000..4e87abc Binary files /dev/null and b/images/kwb-logo.psd differ