├── .github └── workflows │ └── dotnet-desktop.yml ├── .gitignore ├── App.axaml ├── App.axaml.cs ├── Assets ├── Fonts │ ├── SourceCodePro-Black.ttf │ ├── SourceCodePro-BlackItalic.ttf │ ├── SourceCodePro-Bold.ttf │ ├── SourceCodePro-BoldItalic.ttf │ ├── SourceCodePro-ExtraLight.ttf │ ├── SourceCodePro-ExtraLightItalic.ttf │ ├── SourceCodePro-Italic.ttf │ ├── SourceCodePro-Light.ttf │ ├── SourceCodePro-LightItalic.ttf │ ├── SourceCodePro-Medium.ttf │ ├── SourceCodePro-MediumItalic.ttf │ ├── SourceCodePro-Regular.ttf │ ├── SourceCodePro-SemiBold.ttf │ └── SourceCodePro-SemiBoldItalic.ttf └── avalonia-logo.ico ├── Communication └── MqttClient.cs ├── FodyWeavers.xml ├── Hardware ├── BaseEzoMeasurement.cs ├── BaseMeasurement.cs ├── DistanceMeasurement.cs ├── Gpio.cs ├── IGpio.cs ├── MeasurementResult.cs ├── PhMeasurement.cs ├── RedoxMeasurement.cs ├── TemperatureMeasurement.cs └── WinGpioMock.cs ├── Helper ├── Log.cs ├── Persistence.cs ├── PoolControlConfig.cs ├── PoolControlHelper.cs └── PropertySetter.cs ├── LICENSE ├── Pages ├── Cistern.axaml ├── Cistern.axaml.cs ├── FilterPump.axaml ├── FilterPump.axaml.cs ├── Overview.axaml ├── Overview.axaml.cs ├── PhConfig.axaml ├── PhConfig.axaml.cs ├── PhValue.axaml ├── PhValue.axaml.cs ├── Redox.axaml ├── Redox.axaml.cs ├── RedoxConfig.axaml ├── RedoxConfig.axaml.cs ├── SolarHeater.axaml ├── SolarHeater.axaml.cs ├── TemperatureConfig.axaml └── TemperatureConfig.axaml.cs ├── PoolControl.csproj ├── PoolControl.sln ├── Program.cs ├── README.md ├── Resource.Designer.cs ├── Resource.de.resx ├── Resource.resx ├── Styles ├── PoolResources.axaml ├── SideBar.axaml └── Styles.axaml ├── Time └── TimeTrigger.cs ├── UserControls ├── DoubleMeasurementControl.axaml ├── DoubleMeasurementControl.axaml.cs ├── MeasurementControl.axaml ├── MeasurementControl.axaml.cs ├── NumControl.axaml └── NumControl.axaml.cs ├── ViewLocator.cs ├── ViewModels ├── Distance.cs ├── EzoBase.cs ├── FilterPump.cs ├── MainWindowViewModel.cs ├── MeasurementModelBase.cs ├── Ph.cs ├── PoolData.cs ├── PumpModel.cs ├── Redox.cs ├── RelayConfig.cs ├── SolarHeater.cs ├── Switch.cs ├── Temperature.cs └── ViewModelBase.cs ├── Views ├── MainView.axaml ├── MainView.axaml.cs ├── MainWindow.axaml └── MainWindow.axaml.cs ├── appsettings.json ├── poolcontrolviewmodel.json ├── w1_slave └── winpoolcontrolviewmodel.json /.github/workflows/dotnet-desktop.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. 2 | # They are provided by a third-party and are governed by 3 | # separate terms of service, privacy policy, and support 4 | # documentation. 5 | 6 | # This workflow will build, test, sign and package a WPF or Windows Forms desktop application 7 | # built on .NET Core. 8 | # To learn how to migrate your existing application to .NET Core, 9 | # refer to https://docs.microsoft.com/en-us/dotnet/desktop-wpf/migration/convert-project-from-net-framework 10 | # 11 | # To configure this workflow: 12 | # 13 | # 1. Configure environment variables 14 | # GitHub sets default environment variables for every workflow run. 15 | # Replace the variables relative to your project in the "env" section below. 16 | # 17 | # 2. Signing 18 | # Generate a signing certificate in the Windows Application 19 | # Packaging Project or add an existing signing certificate to the project. 20 | # Next, use PowerShell to encode the .pfx file using Base64 encoding 21 | # by running the following Powershell script to generate the output string: 22 | # 23 | # $pfx_cert = Get-Content '.\SigningCertificate.pfx' -Encoding Byte 24 | # [System.Convert]::ToBase64String($pfx_cert) | Out-File 'SigningCertificate_Encoded.txt' 25 | # 26 | # Open the output file, SigningCertificate_Encoded.txt, and copy the 27 | # string inside. Then, add the string to the repo as a GitHub secret 28 | # and name it "Base64_Encoded_Pfx." 29 | # For more information on how to configure your signing certificate for 30 | # this workflow, refer to https://github.com/microsoft/github-actions-for-desktop-apps#signing 31 | # 32 | # Finally, add the signing certificate password to the repo as a secret and name it "Pfx_Key". 33 | # See "Build the Windows Application Packaging project" below to see how the secret is used. 34 | # 35 | # For more information on GitHub Actions, refer to https://github.com/features/actions 36 | # For a complete CI/CD sample to get started with GitHub Action workflows for Desktop Applications, 37 | # refer to https://github.com/microsoft/github-actions-for-desktop-apps 38 | 39 | name: .NET Core Desktop 40 | 41 | on: 42 | push: 43 | branches: [ "main" ] 44 | pull_request: 45 | branches: [ "main" ] 46 | 47 | jobs: 48 | 49 | build: 50 | 51 | strategy: 52 | matrix: 53 | configuration: [Debug, Release] 54 | 55 | runs-on: windows-latest # For a list of available runner types, refer to 56 | # https://help.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idruns-on 57 | 58 | env: 59 | Solution_Name: your-solution-name # Replace with your solution name, i.e. MyWpfApp.sln. 60 | Test_Project_Path: your-test-project-path # Replace with the path to your test project, i.e. MyWpfApp.Tests\MyWpfApp.Tests.csproj. 61 | Wap_Project_Directory: your-wap-project-directory-name # Replace with the Wap project directory relative to the solution, i.e. MyWpfApp.Package. 62 | Wap_Project_Path: your-wap-project-path # Replace with the path to your Wap project, i.e. MyWpf.App.Package\MyWpfApp.Package.wapproj. 63 | 64 | steps: 65 | - name: Checkout 66 | uses: actions/checkout@v3 67 | with: 68 | fetch-depth: 0 69 | 70 | # Install the .NET Core workload 71 | - name: Install .NET Core 72 | uses: actions/setup-dotnet@v3 73 | with: 74 | dotnet-version: 6.0.x 75 | 76 | # Add MSBuild to the PATH: https://github.com/microsoft/setup-msbuild 77 | - name: Setup MSBuild.exe 78 | uses: microsoft/setup-msbuild@v1.0.2 79 | 80 | # Execute all unit tests in the solution 81 | - name: Execute unit tests 82 | run: dotnet test 83 | 84 | # Restore the application to populate the obj folder with RuntimeIdentifiers 85 | - name: Restore the application 86 | run: msbuild $env:Solution_Name /t:Restore /p:Configuration=$env:Configuration 87 | env: 88 | Configuration: ${{ matrix.configuration }} 89 | 90 | # Decode the base 64 encoded pfx and save the Signing_Certificate 91 | - name: Decode the pfx 92 | run: | 93 | $pfx_cert_byte = [System.Convert]::FromBase64String("${{ secrets.Base64_Encoded_Pfx }}") 94 | $certificatePath = Join-Path -Path $env:Wap_Project_Directory -ChildPath GitHubActionsWorkflow.pfx 95 | [IO.File]::WriteAllBytes("$certificatePath", $pfx_cert_byte) 96 | 97 | # Create the app package by building and packaging the Windows Application Packaging project 98 | - name: Create the app package 99 | run: msbuild $env:Wap_Project_Path /p:Configuration=$env:Configuration /p:UapAppxPackageBuildMode=$env:Appx_Package_Build_Mode /p:AppxBundle=$env:Appx_Bundle /p:PackageCertificateKeyFile=GitHubActionsWorkflow.pfx /p:PackageCertificatePassword=${{ secrets.Pfx_Key }} 100 | env: 101 | Appx_Bundle: Always 102 | Appx_Bundle_Platforms: x86|x64 103 | Appx_Package_Build_Mode: StoreUpload 104 | Configuration: ${{ matrix.configuration }} 105 | 106 | # Remove the pfx 107 | - name: Remove the pfx 108 | run: Remove-Item -path $env:Wap_Project_Directory\GitHubActionsWorkflow.pfx 109 | 110 | # Upload the MSIX package: https://github.com/marketplace/actions/upload-a-build-artifact 111 | - name: Upload build artifacts 112 | uses: actions/upload-artifact@v3 113 | with: 114 | name: MSIX Package 115 | path: ${{ env.Wap_Project_Directory }}\AppPackages 116 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Ww][Ii][Nn]32/ 27 | [Aa][Rr][Mm]/ 28 | [Aa][Rr][Mm]64/ 29 | bld/ 30 | [Bb]in/ 31 | [Oo]bj/ 32 | [Ll]og/ 33 | [Ll]ogs/ 34 | 35 | # Visual Studio 2015/2017 cache/options directory 36 | .vs/ 37 | # Uncomment if you have tasks that create the project's static files in wwwroot 38 | #wwwroot/ 39 | 40 | # Visual Studio 2017 auto generated files 41 | Generated\ Files/ 42 | 43 | # MSTest test Results 44 | [Tt]est[Rr]esult*/ 45 | [Bb]uild[Ll]og.* 46 | 47 | # NUnit 48 | *.VisualState.xml 49 | TestResult.xml 50 | nunit-*.xml 51 | 52 | # Build Results of an ATL Project 53 | [Dd]ebugPS/ 54 | [Rr]eleasePS/ 55 | dlldata.c 56 | 57 | # Benchmark Results 58 | BenchmarkDotNet.Artifacts/ 59 | 60 | # .NET Core 61 | project.lock.json 62 | project.fragment.lock.json 63 | artifacts/ 64 | 65 | # Tye 66 | .tye/ 67 | 68 | # ASP.NET Scaffolding 69 | ScaffoldingReadMe.txt 70 | 71 | # StyleCop 72 | StyleCopReport.xml 73 | 74 | # Files built by Visual Studio 75 | *_i.c 76 | *_p.c 77 | *_h.h 78 | *.ilk 79 | *.meta 80 | *.obj 81 | *.iobj 82 | *.pch 83 | *.pdb 84 | *.ipdb 85 | *.pgc 86 | *.pgd 87 | *.rsp 88 | *.sbr 89 | *.tlb 90 | *.tli 91 | *.tlh 92 | *.tmp 93 | *.tmp_proj 94 | *_wpftmp.csproj 95 | *.log 96 | *.vspscc 97 | *.vssscc 98 | .builds 99 | *.pidb 100 | *.svclog 101 | *.scc 102 | 103 | # Chutzpah Test files 104 | _Chutzpah* 105 | 106 | # Visual C++ cache files 107 | ipch/ 108 | *.aps 109 | *.ncb 110 | *.opendb 111 | *.opensdf 112 | *.sdf 113 | *.cachefile 114 | *.VC.db 115 | *.VC.VC.opendb 116 | 117 | # Visual Studio profiler 118 | *.psess 119 | *.vsp 120 | *.vspx 121 | *.sap 122 | 123 | # Visual Studio Trace Files 124 | *.e2e 125 | 126 | # TFS 2012 Local Workspace 127 | $tf/ 128 | 129 | # Guidance Automation Toolkit 130 | *.gpState 131 | 132 | # ReSharper is a .NET coding add-in 133 | _ReSharper*/ 134 | *.[Rr]e[Ss]harper 135 | *.DotSettings.user 136 | 137 | # TeamCity is a build add-in 138 | _TeamCity* 139 | 140 | # DotCover is a Code Coverage Tool 141 | *.dotCover 142 | 143 | # AxoCover is a Code Coverage Tool 144 | .axoCover/* 145 | !.axoCover/settings.json 146 | 147 | # Coverlet is a free, cross platform Code Coverage Tool 148 | coverage*.json 149 | coverage*.xml 150 | coverage*.info 151 | 152 | # Visual Studio code coverage results 153 | *.coverage 154 | *.coveragexml 155 | 156 | # NCrunch 157 | _NCrunch_* 158 | .*crunch*.local.xml 159 | nCrunchTemp_* 160 | 161 | # MightyMoose 162 | *.mm.* 163 | AutoTest.Net/ 164 | 165 | # Web workbench (sass) 166 | .sass-cache/ 167 | 168 | # Installshield output folder 169 | [Ee]xpress/ 170 | 171 | # DocProject is a documentation generator add-in 172 | DocProject/buildhelp/ 173 | DocProject/Help/*.HxT 174 | DocProject/Help/*.HxC 175 | DocProject/Help/*.hhc 176 | DocProject/Help/*.hhk 177 | DocProject/Help/*.hhp 178 | DocProject/Help/Html2 179 | DocProject/Help/html 180 | 181 | # Click-Once directory 182 | publish/ 183 | 184 | # Publish Web Output 185 | *.[Pp]ublish.xml 186 | *.azurePubxml 187 | # Note: Comment the next line if you want to checkin your web deploy settings, 188 | # but database connection strings (with potential passwords) will be unencrypted 189 | *.pubxml 190 | *.publishproj 191 | 192 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 193 | # checkin your Azure Web App publish settings, but sensitive information contained 194 | # in these scripts will be unencrypted 195 | PublishScripts/ 196 | 197 | # NuGet Packages 198 | *.nupkg 199 | # NuGet Symbol Packages 200 | *.snupkg 201 | # The packages folder can be ignored because of Package Restore 202 | **/[Pp]ackages/* 203 | # except build/, which is used as an MSBuild target. 204 | !**/[Pp]ackages/build/ 205 | # Uncomment if necessary however generally it will be regenerated when needed 206 | #!**/[Pp]ackages/repositories.config 207 | # NuGet v3's project.json files produces more ignorable files 208 | *.nuget.props 209 | *.nuget.targets 210 | 211 | # Microsoft Azure Build Output 212 | csx/ 213 | *.build.csdef 214 | 215 | # Microsoft Azure Emulator 216 | ecf/ 217 | rcf/ 218 | 219 | # Windows Store app package directories and files 220 | AppPackages/ 221 | BundleArtifacts/ 222 | Package.StoreAssociation.xml 223 | _pkginfo.txt 224 | *.appx 225 | *.appxbundle 226 | *.appxupload 227 | 228 | # Visual Studio cache files 229 | # files ending in .cache can be ignored 230 | *.[Cc]ache 231 | # but keep track of directories ending in .cache 232 | !?*.[Cc]ache/ 233 | 234 | # Others 235 | ClientBin/ 236 | ~$* 237 | *~ 238 | *.dbmdl 239 | *.dbproj.schemaview 240 | *.jfm 241 | *.pfx 242 | *.publishsettings 243 | orleans.codegen.cs 244 | 245 | # Including strong name files can present a security risk 246 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 247 | #*.snk 248 | 249 | # Since there are multiple workflows, uncomment next line to ignore bower_components 250 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 251 | #bower_components/ 252 | 253 | # RIA/Silverlight projects 254 | Generated_Code/ 255 | 256 | # Backup & report files from converting an old project file 257 | # to a newer Visual Studio version. Backup files are not needed, 258 | # because we have git ;-) 259 | _UpgradeReport_Files/ 260 | Backup*/ 261 | UpgradeLog*.XML 262 | UpgradeLog*.htm 263 | ServiceFabricBackup/ 264 | *.rptproj.bak 265 | 266 | # SQL Server files 267 | *.mdf 268 | *.ldf 269 | *.ndf 270 | 271 | # Business Intelligence projects 272 | *.rdl.data 273 | *.bim.layout 274 | *.bim_*.settings 275 | *.rptproj.rsuser 276 | *- [Bb]ackup.rdl 277 | *- [Bb]ackup ([0-9]).rdl 278 | *- [Bb]ackup ([0-9][0-9]).rdl 279 | 280 | # Microsoft Fakes 281 | FakesAssemblies/ 282 | 283 | # GhostDoc plugin setting file 284 | *.GhostDoc.xml 285 | 286 | # Node.js Tools for Visual Studio 287 | .ntvs_analysis.dat 288 | node_modules/ 289 | 290 | # Visual Studio 6 build log 291 | *.plg 292 | 293 | # Visual Studio 6 workspace options file 294 | *.opt 295 | 296 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 297 | *.vbw 298 | 299 | # Visual Studio LightSwitch build output 300 | **/*.HTMLClient/GeneratedArtifacts 301 | **/*.DesktopClient/GeneratedArtifacts 302 | **/*.DesktopClient/ModelManifest.xml 303 | **/*.Server/GeneratedArtifacts 304 | **/*.Server/ModelManifest.xml 305 | _Pvt_Extensions 306 | 307 | # Paket dependency manager 308 | .paket/paket.exe 309 | paket-files/ 310 | 311 | # FAKE - F# Make 312 | .fake/ 313 | 314 | # CodeRush personal settings 315 | .cr/personal 316 | 317 | # Python Tools for Visual Studio (PTVS) 318 | __pycache__/ 319 | *.pyc 320 | 321 | # Cake - Uncomment if you are using it 322 | # tools/** 323 | # !tools/packages.config 324 | 325 | # Tabs Studio 326 | *.tss 327 | 328 | # Telerik's JustMock configuration file 329 | *.jmconfig 330 | 331 | # BizTalk build output 332 | *.btp.cs 333 | *.btm.cs 334 | *.odx.cs 335 | *.xsd.cs 336 | 337 | # OpenCover UI analysis results 338 | OpenCover/ 339 | 340 | # Azure Stream Analytics local run output 341 | ASALocalRun/ 342 | 343 | # MSBuild Binary and Structured Log 344 | *.binlog 345 | 346 | # NVidia Nsight GPU debugger configuration file 347 | *.nvuser 348 | 349 | # MFractors (Xamarin productivity tool) working folder 350 | .mfractor/ 351 | 352 | # Local History for Visual Studio 353 | .localhistory/ 354 | 355 | # BeatPulse healthcheck temp database 356 | healthchecksdb 357 | 358 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 359 | MigrationBackup/ 360 | 361 | # Ionide (cross platform F# VS Code tools) working folder 362 | .ionide/ 363 | 364 | # Fody - auto-generated XML schema 365 | FodyWeavers.xsd 366 | 367 | ## 368 | ## Visual studio for Mac 369 | ## 370 | 371 | 372 | # globs 373 | Makefile.in 374 | *.userprefs 375 | *.usertasks 376 | config.make 377 | config.status 378 | aclocal.m4 379 | install-sh 380 | autom4te.cache/ 381 | *.tar.gz 382 | tarballs/ 383 | test-results/ 384 | 385 | # Mac bundle stuff 386 | *.dmg 387 | *.app 388 | 389 | # content below from: https://github.com/github/gitignore/blob/master/Global/macOS.gitignore 390 | # General 391 | .DS_Store 392 | .AppleDouble 393 | .LSOverride 394 | 395 | # Icon must end with two \r 396 | Icon 397 | 398 | 399 | # Thumbnails 400 | ._* 401 | 402 | # Files that might appear in the root of a volume 403 | .DocumentRevisions-V100 404 | .fseventsd 405 | .Spotlight-V100 406 | .TemporaryItems 407 | .Trashes 408 | .VolumeIcon.icns 409 | .com.apple.timemachine.donotpresent 410 | 411 | # Directories potentially created on remote AFP share 412 | .AppleDB 413 | .AppleDesktop 414 | Network Trash Folder 415 | Temporary Items 416 | .apdisk 417 | 418 | # content below from: https://github.com/github/gitignore/blob/master/Global/Windows.gitignore 419 | # Windows thumbnail cache files 420 | Thumbs.db 421 | ehthumbs.db 422 | ehthumbs_vista.db 423 | 424 | # Dump file 425 | *.stackdump 426 | 427 | # Folder config file 428 | [Dd]esktop.ini 429 | 430 | # Recycle Bin used on file shares 431 | $RECYCLE.BIN/ 432 | 433 | # Windows Installer files 434 | *.cab 435 | *.msi 436 | *.msix 437 | *.msm 438 | *.msp 439 | 440 | # Windows shortcuts 441 | *.lnk 442 | 443 | # JetBrains Rider 444 | .idea/ 445 | *.sln.iml 446 | 447 | ## 448 | ## Visual Studio Code 449 | ## 450 | .vscode/* 451 | !.vscode/settings.json 452 | !.vscode/tasks.json 453 | !.vscode/launch.json 454 | !.vscode/extensions.json 455 | /*.DotSettings 456 | -------------------------------------------------------------------------------- /App.axaml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /App.axaml.cs: -------------------------------------------------------------------------------- 1 | using Avalonia; 2 | using Avalonia.Controls; 3 | using Avalonia.Controls.ApplicationLifetimes; 4 | using Avalonia.Markup.Xaml; 5 | using PoolControl.ViewModels; 6 | using PoolControl.Views; 7 | 8 | namespace PoolControl; 9 | 10 | public partial class App : Application 11 | { 12 | public static Window? MainWindow { get; private set; } 13 | 14 | public override void Initialize() 15 | { 16 | AvaloniaXamlLoader.Load(this); 17 | } 18 | 19 | public override void OnFrameworkInitializationCompleted() 20 | { 21 | if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) 22 | { 23 | MainWindow = new MainWindow 24 | { 25 | DataContext = new MainWindowViewModel(), 26 | }; 27 | 28 | desktop.MainWindow = MainWindow; 29 | } 30 | 31 | base.OnFrameworkInitializationCompleted(); 32 | } 33 | } -------------------------------------------------------------------------------- /Assets/Fonts/SourceCodePro-Black.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kretzp/PoolControl/b91c8b53258da2219f890c4df3dd47667a2fde2f/Assets/Fonts/SourceCodePro-Black.ttf -------------------------------------------------------------------------------- /Assets/Fonts/SourceCodePro-BlackItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kretzp/PoolControl/b91c8b53258da2219f890c4df3dd47667a2fde2f/Assets/Fonts/SourceCodePro-BlackItalic.ttf -------------------------------------------------------------------------------- /Assets/Fonts/SourceCodePro-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kretzp/PoolControl/b91c8b53258da2219f890c4df3dd47667a2fde2f/Assets/Fonts/SourceCodePro-Bold.ttf -------------------------------------------------------------------------------- /Assets/Fonts/SourceCodePro-BoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kretzp/PoolControl/b91c8b53258da2219f890c4df3dd47667a2fde2f/Assets/Fonts/SourceCodePro-BoldItalic.ttf -------------------------------------------------------------------------------- /Assets/Fonts/SourceCodePro-ExtraLight.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kretzp/PoolControl/b91c8b53258da2219f890c4df3dd47667a2fde2f/Assets/Fonts/SourceCodePro-ExtraLight.ttf -------------------------------------------------------------------------------- /Assets/Fonts/SourceCodePro-ExtraLightItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kretzp/PoolControl/b91c8b53258da2219f890c4df3dd47667a2fde2f/Assets/Fonts/SourceCodePro-ExtraLightItalic.ttf -------------------------------------------------------------------------------- /Assets/Fonts/SourceCodePro-Italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kretzp/PoolControl/b91c8b53258da2219f890c4df3dd47667a2fde2f/Assets/Fonts/SourceCodePro-Italic.ttf -------------------------------------------------------------------------------- /Assets/Fonts/SourceCodePro-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kretzp/PoolControl/b91c8b53258da2219f890c4df3dd47667a2fde2f/Assets/Fonts/SourceCodePro-Light.ttf -------------------------------------------------------------------------------- /Assets/Fonts/SourceCodePro-LightItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kretzp/PoolControl/b91c8b53258da2219f890c4df3dd47667a2fde2f/Assets/Fonts/SourceCodePro-LightItalic.ttf -------------------------------------------------------------------------------- /Assets/Fonts/SourceCodePro-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kretzp/PoolControl/b91c8b53258da2219f890c4df3dd47667a2fde2f/Assets/Fonts/SourceCodePro-Medium.ttf -------------------------------------------------------------------------------- /Assets/Fonts/SourceCodePro-MediumItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kretzp/PoolControl/b91c8b53258da2219f890c4df3dd47667a2fde2f/Assets/Fonts/SourceCodePro-MediumItalic.ttf -------------------------------------------------------------------------------- /Assets/Fonts/SourceCodePro-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kretzp/PoolControl/b91c8b53258da2219f890c4df3dd47667a2fde2f/Assets/Fonts/SourceCodePro-Regular.ttf -------------------------------------------------------------------------------- /Assets/Fonts/SourceCodePro-SemiBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kretzp/PoolControl/b91c8b53258da2219f890c4df3dd47667a2fde2f/Assets/Fonts/SourceCodePro-SemiBold.ttf -------------------------------------------------------------------------------- /Assets/Fonts/SourceCodePro-SemiBoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kretzp/PoolControl/b91c8b53258da2219f890c4df3dd47667a2fde2f/Assets/Fonts/SourceCodePro-SemiBoldItalic.ttf -------------------------------------------------------------------------------- /Assets/avalonia-logo.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kretzp/PoolControl/b91c8b53258da2219f890c4df3dd47667a2fde2f/Assets/avalonia-logo.ico -------------------------------------------------------------------------------- /Communication/MqttClient.cs: -------------------------------------------------------------------------------- 1 | using MQTTnet; 2 | using MQTTnet.Client; 3 | using Serilog; 4 | using System; 5 | using System.Text; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | using System.Runtime.InteropServices; 9 | using System.Diagnostics; 10 | using PoolControl.Helper; 11 | using Log = PoolControl.Helper.Log; 12 | 13 | namespace PoolControl.Communication; 14 | 15 | public class PoolMqttClient 16 | { 17 | private const string Reason = "SHUTDOWN"; 18 | private const string Win = "win"; 19 | 20 | private bool Shutdown { get; set; } 21 | 22 | private static PoolMqttClient? _instance; 23 | private static readonly object Padlock = new object(); 24 | 25 | public static PoolMqttClient Instance 26 | { 27 | get 28 | { 29 | lock (Padlock) 30 | { 31 | return _instance ??= new PoolMqttClient(Log.Logger); 32 | } 33 | } 34 | } 35 | 36 | private MqttFactory? _mqttFactory; 37 | private IMqttClient? _mqttClient; 38 | private MqttClientOptions? _options; 39 | protected ILogger Logger { get; init; } 40 | 41 | private PoolMqttClient(ILogger? logger) 42 | { 43 | Logger = logger?.ForContext() ?? throw new ArgumentNullException(nameof(Logger)); 44 | _ = InitializeAsync(); 45 | } 46 | 47 | private async Task InitializeAsync() 48 | { 49 | Logger.Information("# Start MQTT #"); 50 | _mqttFactory = new MqttFactory(); 51 | _mqttClient = _mqttFactory.CreateMqttClient(); 52 | _options = new MqttClientOptionsBuilder() 53 | .WithTcpServer(PoolControlConfig.Instance.Settings!.MQTT.Server, PoolControlConfig.Instance.Settings.MQTT.Port) 54 | .WithCredentials(PoolControlConfig.Instance.Settings.MQTT.User, PoolControlConfig.Instance.Settings.MQTT.Password) 55 | .Build(); 56 | 57 | _mqttClient.DisconnectedAsync += async (e) => 58 | { 59 | Logger.Information("# DISCONNECTED FROM SERVER #"); 60 | _ = Task.Delay(TimeSpan.FromSeconds(5)); 61 | if (!Reason.Equals(e.ReasonString)) 62 | { 63 | if (Shutdown) 64 | { 65 | Logger.Information("## SHUTDOWN in Progress while connecting Async. NO CONNECTION will be restarted ##"); 66 | } 67 | else 68 | { 69 | await connectAsync(); 70 | } 71 | } 72 | else 73 | { 74 | Logger.Information("# NO RECONNECTION BECAUSE OF INTENTIONALLY DISCONNECTION #"); 75 | } 76 | }; 77 | 78 | _mqttClient.ConnectedAsync += async (_) => 79 | { 80 | Logger.Information("# CONNECTED WITH SERVER #"); 81 | await sendLwtConnected(); 82 | 83 | // Subscribe to a topic 84 | string topic = $"{PoolControlConfig.Instance.Settings.BaseTopic.Command}#"; 85 | 86 | subscribe(topic); 87 | }; 88 | 89 | _mqttClient.ApplicationMessageReceivedAsync += (e) => { 90 | Logger.Information("# Received Topic={Topic} Payload={Payload} QoS={Qos} Retain={Retain}", 91 | e.ApplicationMessage.Topic, Encoding.UTF8.GetString(e.ApplicationMessage.PayloadSegment.ToArray()), e.ApplicationMessage.QualityOfServiceLevel, e.ApplicationMessage.Retain); 92 | return Task.CompletedTask; 93 | }; 94 | 95 | await connectAsync(); 96 | } 97 | 98 | private async Task sendLwtConnected() 99 | { 100 | string lwtTopic = PoolControlConfig.Instance.Settings!.LWT.Topic; 101 | string? lwtConnectMessage = PoolControlConfig.Instance.Settings.LWT.ConnectMessage; 102 | await publishMessage(lwtTopic, lwtConnectMessage, 2, true); 103 | } 104 | 105 | private async Task sendLwtDisconnected() 106 | { 107 | string lwtTopic = PoolControlConfig.Instance.Settings!.LWT.Topic; 108 | string? lwtDisConnectMessage = PoolControlConfig.Instance.Settings.LWT.DisconnectMessage; 109 | await publishMessage(lwtTopic, lwtDisConnectMessage, 2, true); 110 | Logger.Information("Topic: {Topic} Payload: {Payload}", lwtTopic, lwtDisConnectMessage); 111 | } 112 | 113 | public void register(Func handler) 114 | { 115 | if (_mqttClient != null) _mqttClient.ApplicationMessageReceivedAsync += handler; 116 | } 117 | 118 | public void unRegister(Func handler) 119 | { 120 | if (_mqttClient != null) _mqttClient.ApplicationMessageReceivedAsync -= handler; 121 | } 122 | 123 | private async void subscribe(string topic) 124 | { 125 | if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) 126 | { 127 | topic = Win + topic; 128 | } 129 | 130 | await _mqttClient.SubscribeAsync(new MqttTopicFilterBuilder().WithTopic(topic).Build()); 131 | Logger.Information("# Subscribed topic={Topic}", topic); 132 | } 133 | 134 | public async void unSubscribe(string topic) 135 | { 136 | if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) 137 | { 138 | topic = Win + topic; 139 | } 140 | 141 | await _mqttClient.UnsubscribeAsync(topic); 142 | Logger.Information("# Unsubscribed topic={Topic}", topic); 143 | } 144 | 145 | private async Task connectAsync() 146 | { 147 | try 148 | { 149 | Logger.Information("# CONNECTING ... #"); 150 | if (_mqttClient != null) await _mqttClient.ConnectAsync(_options); 151 | } 152 | catch (Exception ex) 153 | { 154 | Logger.Error(ex, "# RECONNECTING FAILED ##"); 155 | } 156 | } 157 | 158 | public void Disconnect() 159 | { 160 | try 161 | { 162 | _ = sendLwtDisconnected(); 163 | Thread.Sleep(3000); 164 | Shutdown = true; 165 | if (_mqttClient != null) 166 | _ = _mqttClient.DisconnectAsync(new MqttClientDisconnectOptionsBuilder() 167 | .WithReason(MqttClientDisconnectOptionsReason.DisconnectWithWillMessage).Build()); 168 | Logger.Information("# Disconnect started because of shutdown"); 169 | } 170 | catch (Exception ex) 171 | { 172 | Logger.Error(ex, "# DISCONNECTING FAILED #"); 173 | } 174 | } 175 | 176 | public async Task publishMessage(string topic, string? payload, int qos, bool retain) 177 | { 178 | if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) 179 | { 180 | topic = Win + topic; 181 | } 182 | 183 | var message = new MqttApplicationMessageBuilder() 184 | .WithTopic(topic) 185 | .WithPayload(payload) 186 | .WithQualityOfServiceLevel((MQTTnet.Protocol.MqttQualityOfServiceLevel)qos) 187 | .WithRetainFlag(retain) 188 | .Build(); 189 | 190 | if (_mqttClient != null) await _mqttClient.PublishAsync(message); 191 | Logger.Information("# Published Topic={Topic} Payload={Payload} QoS={Qos} Retain={Retain}", message.Topic, Encoding.UTF8.GetString(message.PayloadSegment.ToArray()), message.QualityOfServiceLevel, message.Retain); 192 | 193 | Process currentProc = Process.GetCurrentProcess(); 194 | double bytesInUse = currentProc.PrivateMemorySize64 / 1024.0 / 1024.0; 195 | 196 | Logger.Debug("MBytes in use {Bytes} in {Proc}", bytesInUse, currentProc.ProcessName); 197 | } 198 | } -------------------------------------------------------------------------------- /FodyWeavers.xml: -------------------------------------------------------------------------------- 1 |  2 | 3 | -------------------------------------------------------------------------------- /Hardware/BaseEzoMeasurement.cs: -------------------------------------------------------------------------------- 1 | using PoolControl.ViewModels; 2 | using System; 3 | using System.Device.I2c; 4 | using System.Globalization; 5 | using System.Text; 6 | using System.Threading; 7 | using PoolControl.Helper; 8 | 9 | namespace PoolControl.Hardware; 10 | 11 | public class BaseEzoMeasurement : BaseMeasurement 12 | { 13 | private const int BufferSize = 32; 14 | private const int MillisToWait = 1000; 15 | 16 | protected int I2CAddress 17 | { 18 | get 19 | { 20 | var address = -1; 21 | try 22 | { 23 | if (ModelBase?.Address != null) address = int.Parse(ModelBase.Address); 24 | } 25 | catch (Exception) 26 | { 27 | // ignored 28 | } 29 | 30 | return address; 31 | } 32 | } 33 | 34 | public BaseEzoMeasurement() 35 | { 36 | if (Log.Logger != null) 37 | Logger = Log.Logger.ForContext() ?? throw new ArgumentNullException(nameof(Logger)); 38 | } 39 | 40 | private MeasurementResult GetDeviceInformation() 41 | { 42 | return Send_i2c_command("i"); 43 | } 44 | 45 | private MeasurementResult GetDeviceStatus() 46 | { 47 | return Send_i2c_command("Status"); 48 | } 49 | 50 | private MeasurementResult GetLedState() 51 | { 52 | return Send_i2c_command("L,?"); 53 | } 54 | 55 | public MeasurementResult SetLedStateOn() 56 | { 57 | return Send_i2c_command("L,1"); 58 | } 59 | public MeasurementResult SetLedStateOff() 60 | { 61 | return Send_i2c_command("L,0"); 62 | } 63 | 64 | public MeasurementResult SwitchLedState(bool state) 65 | { 66 | var sw = state ? "1" : "0"; 67 | return Send_i2c_command($"L,{sw}"); 68 | } 69 | 70 | public MeasurementResult FindDevice() 71 | { 72 | return Send_i2c_command("Find"); 73 | } 74 | 75 | public MeasurementResult TerminateFind() 76 | { 77 | return GetLedState(); 78 | } 79 | 80 | protected MeasurementResult TakeReading() 81 | { 82 | return Send_i2c_command("R"); 83 | } 84 | 85 | public MeasurementResult Sleep() 86 | { 87 | return Send_i2c_command("Sleeps"); 88 | } 89 | 90 | public MeasurementResult Awake() 91 | { 92 | return GetDeviceInformation(); 93 | } 94 | 95 | public MeasurementResult FactoryReset() 96 | { 97 | return Send_i2c_command("Factory"); 98 | } 99 | 100 | public MeasurementResult DeviceCalibrated() 101 | { 102 | return Send_i2c_command("Cal,?"); 103 | } 104 | 105 | public MeasurementResult ClearCalibration() 106 | { 107 | return Send_i2c_command("Cal,clear"); 108 | } 109 | 110 | private MeasurementResult SetName(string name) 111 | { 112 | return Send_i2c_command("Name," + name); 113 | } 114 | 115 | public MeasurementResult ClearName() 116 | { 117 | return SetName(""); 118 | } 119 | 120 | public MeasurementResult GetName() 121 | { 122 | return Send_i2c_command("Name,?"); 123 | } 124 | 125 | /* 126 | * Parts of this section has been used from 127 | * https://github.com/letscontrolit/ESPEasy 128 | * */ 129 | protected virtual MeasurementResult Send_i2c_command(string command) 130 | { 131 | var ezoResult = new MeasurementResult { ReturnCode = (int)MeasurementResultCode.Pending, StatusInfo = "Error: ", Result = double.NaN, Device = $"{GetType().Name}@{I2CAddress}", Command = command }; 132 | 133 | try 134 | { 135 | var resString = ""; 136 | 137 | // When we Export the calibration, we have to fetch it multiple times 138 | var export = "Export".Equals(command); 139 | 140 | do 141 | { 142 | ezoResult.ReturnCode = (int)MeasurementResultCode.Pending; 143 | Logger?.Information("> cmd = {Command} to Address {I2CAddress}", command, I2CAddress); 144 | ReadOnlySpan writeBuffer = new(Encoding.ASCII.GetBytes(command)); 145 | 146 | using var i2C = I2cDevice.Create(new I2cConnectionSettings(1, I2CAddress)); 147 | i2C.Write(writeBuffer); 148 | // don't read answer if we want to go to sleep 149 | if (command.Length > 4 && command[..5].ToLower().Equals("sleep")) 150 | { 151 | ezoResult.ReturnCode = 1; 152 | ezoResult.StatusInfo = "Going to sleep"; 153 | return ezoResult; 154 | } 155 | 156 | 157 | Logger?.Debug(" Waiting {MillisToWait} before fetching answer", MillisToWait); 158 | Thread.Sleep(MillisToWait); 159 | 160 | while (ezoResult.ReturnCode == (int)MeasurementResultCode.Pending) 161 | { 162 | Span readBuffer = new(new byte[BufferSize]); 163 | i2C.Read(readBuffer); 164 | 165 | ezoResult.ReturnCode = readBuffer[0]; 166 | 167 | switch (ezoResult.ReturnCode) 168 | { 169 | case (int)MeasurementResultCode.Success: 170 | Logger?.Information("< success, answer = {Bits}", BitConverter.ToString(readBuffer.ToArray())); 171 | 172 | var nullByteIndexLength = readBuffer.IndexOf(0x00) - 1; 173 | if (nullByteIndexLength < 1) 174 | { 175 | Logger?.Information("< success, without answer"); 176 | ezoResult.Result = double.NaN; 177 | ezoResult.StatusInfo = "OK without return value"; 178 | } 179 | else 180 | { 181 | var slice = Encoding.ASCII.GetString(readBuffer.Slice(1, nullByteIndexLength)); 182 | 183 | Logger?.Debug("Slice: {Slice}", slice); 184 | 185 | if (export) 186 | { 187 | if ("*DONE".Equals(slice)) 188 | { 189 | Logger?.Debug("Done!!!"); 190 | export = false; 191 | } 192 | else 193 | { 194 | resString += slice; 195 | } 196 | } 197 | else 198 | { 199 | resString = slice; 200 | try 201 | { 202 | ezoResult.Result = double.Parse(resString, new CultureInfo("en-US")); 203 | } 204 | catch (Exception) 205 | { 206 | if (resString.ToLower().StartsWith("?l,") && resString.Length > 3) 207 | { 208 | ezoResult.Result = double.Parse(resString[3..], new CultureInfo("en-US")); 209 | } 210 | else if (resString.ToLower().StartsWith("?cal,") && resString.Length > 5) 211 | { 212 | ezoResult.Result = double.Parse(resString[5..], new CultureInfo("en-US")); 213 | } 214 | else if (resString.ToLower().StartsWith("?pLock,") && resString.Length > 7) 215 | { 216 | ezoResult.Result = double.Parse(resString[7..], new CultureInfo("en-US")); 217 | } 218 | else if (resString.ToLower().StartsWith("?slope,") && resString.Length > 7) 219 | { 220 | resString = resString[7..]; 221 | } 222 | else if (resString.ToLower().StartsWith("?status") && resString.Length > 7) 223 | { 224 | ezoResult.Result = double.Parse(resString.Split(",")[2], new CultureInfo("en-US")); 225 | } 226 | } 227 | } 228 | Logger?.Information("< success, answer = {ResString}", resString); 229 | ezoResult.StatusInfo = resString; 230 | } 231 | 232 | break; 233 | case (int)MeasurementResultCode.SyntaxError: 234 | Logger?.Information("< syntax error = {Bits}", BitConverter.ToString(readBuffer.ToArray())); 235 | ezoResult.StatusInfo += "syntax error"; 236 | break; 237 | case (int)MeasurementResultCode.Pending: 238 | Logger?.Information("< command pending"); 239 | break; 240 | case (int)MeasurementResultCode.NoDataToSend: 241 | Logger?.Information("< no data"); 242 | break; 243 | default: 244 | Logger?.Information("< command failed = {Bits}", BitConverter.ToString(readBuffer.ToArray())); 245 | ezoResult.StatusInfo += "command failed!"; 246 | break; 247 | } 248 | } 249 | } while (export); 250 | } 251 | catch (Exception ex) 252 | { 253 | Logger?.Error(ex, "Error in EzoResult send_i2c_command {Command} Address {I2CAddress}",command, I2CAddress); 254 | ezoResult.StatusInfo += ex.Message; 255 | } 256 | 257 | ezoResult.TimeStamp = DateTime.Now; 258 | 259 | return ezoResult; 260 | } 261 | 262 | protected override MeasurementResult DoMeasurement() 263 | { 264 | var vmr = DoVoltageMeasurement(); 265 | ((EzoBase)ModelBase!).Voltage = vmr.Result; 266 | 267 | return TakeReading(); 268 | } 269 | 270 | protected MeasurementResult DoVoltageMeasurement() 271 | { 272 | var mr = GetDeviceStatus(); 273 | if (mr.Result <= 0) 274 | { 275 | throw new ArgumentOutOfRangeException($"Error in {GetType().Name} <= 0"); 276 | } 277 | 278 | return mr; 279 | } 280 | } -------------------------------------------------------------------------------- /Hardware/BaseMeasurement.cs: -------------------------------------------------------------------------------- 1 | using PoolControl.ViewModels; 2 | using Serilog; 3 | 4 | namespace PoolControl.Hardware; 5 | 6 | public abstract class BaseMeasurement 7 | { 8 | protected ILogger? Logger { get; init; } 9 | 10 | public MeasurementModelBase? ModelBase { get; set; } 11 | 12 | protected abstract MeasurementResult DoMeasurement(); 13 | 14 | public MeasurementResult Measure() 15 | { 16 | var result = DoMeasurement(); 17 | 18 | if (result.ReturnCode != 1) return result; 19 | if (ModelBase == null) return result; 20 | 21 | ModelBase.PublishMessageValue(); 22 | ModelBase.Value = result.Result; 23 | ModelBase.TimeStamp = result.TimeStamp; 24 | 25 | return result; 26 | } 27 | } -------------------------------------------------------------------------------- /Hardware/DistanceMeasurement.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using PoolControl.Helper; 4 | using PoolControl.ViewModels; 5 | 6 | namespace PoolControl.Hardware; 7 | 8 | public class DistanceMeasurement : BaseMeasurement 9 | { 10 | public DistanceMeasurement() 11 | { 12 | Logger = Log.Logger?.ForContext() ?? throw new ArgumentNullException(nameof(Logger)); 13 | } 14 | 15 | private double MeasureOneTime() 16 | { 17 | // Set Trigger High 18 | Gpio.Instance.On(((Distance)ModelBase!).Trigger, true); 19 | 20 | // Set Trigger Low after 1 ms 21 | Thread.Sleep(1); 22 | Gpio.Instance.Off(((Distance)ModelBase).Trigger, true); 23 | 24 | DateTime start = DateTime.Now; 25 | DateTime end = DateTime.Now; 26 | 27 | // Start/Stop time 28 | while (Gpio.Instance.ReadPin(((Distance)ModelBase).Echo) == 0) 29 | { 30 | start = DateTime.Now; 31 | } 32 | 33 | // Here is a possible memory leak, if sensor doesn't respond 34 | while (Gpio.Instance.ReadPin(((Distance)ModelBase).Echo) == 1) 35 | { 36 | end = DateTime.Now; 37 | } 38 | 39 | // Time gone 40 | var diff = end.Ticks - start.Ticks; 41 | 42 | // Calculate speed of sound (343,2 m / s) 43 | return diff * 0.003432 / 2; 44 | } 45 | 46 | private double MeasureMultipleTimes() 47 | { 48 | try 49 | { 50 | var distance = 0.0; 51 | var no = ((Distance)ModelBase!).NumberOfMeasurements; 52 | for (var i = 0; i < no; i++) 53 | { 54 | distance += MeasureOneTime(); 55 | } 56 | 57 | return distance / no; 58 | } 59 | catch (Exception ex) 60 | { 61 | Logger?.Error("Error: {Message}", ex.Message); 62 | } 63 | 64 | return -1.0; 65 | } 66 | 67 | protected override MeasurementResult DoMeasurement() 68 | { 69 | var result = new MeasurementResult 70 | { 71 | Device = GetType().Name, Command = "length", 72 | Result = MeasureMultipleTimes(), 73 | TimeStamp = DateTime.Now 74 | }; 75 | if (result.Result < 0) 76 | { 77 | result.ReturnCode = 99; 78 | result.StatusInfo = "Error"; 79 | } 80 | else 81 | { 82 | result.ReturnCode = 1; 83 | result.StatusInfo = "OK"; 84 | } 85 | 86 | return result; 87 | } 88 | } -------------------------------------------------------------------------------- /Hardware/Gpio.cs: -------------------------------------------------------------------------------- 1 | using Serilog; 2 | using System; 3 | using System.Device.Gpio; 4 | using System.Runtime.InteropServices; 5 | using Log = PoolControl.Helper.Log; 6 | 7 | namespace PoolControl.Hardware; 8 | 9 | public class Gpio : IGpio 10 | { 11 | private static IGpio? _instance; 12 | private static readonly object Padlock = new(); 13 | 14 | protected ILogger Logger { get; private init; } 15 | private readonly GpioController _controller; 16 | 17 | private Gpio(ILogger? logger) 18 | { 19 | Logger = logger?.ForContext() ?? throw new ArgumentException(nameof(Logger)); 20 | _controller = new GpioController(PinNumberingScheme.Logical); 21 | } 22 | 23 | public static IGpio Instance 24 | { 25 | get 26 | { 27 | lock (Padlock) 28 | { 29 | return _instance ??= RuntimeInformation.IsOSPlatform(OSPlatform.Windows) 30 | ? WinGpioMock.Instance 31 | : new Gpio(Log.Logger); 32 | } 33 | } 34 | } 35 | 36 | public int ReadPin(int pin) 37 | { 38 | return (int)_controller.Read(pin); 39 | } 40 | 41 | public void OpenPinModeOutput(int pin, bool highIsOn) 42 | { 43 | Open(pin, PinMode.Output, highIsOn); 44 | } 45 | 46 | public void OpenPinModeInput(int pin, bool highIsOn) 47 | { 48 | Open(pin, PinMode.Input, highIsOn); 49 | } 50 | 51 | public void Open(int pin, PinMode mode, bool highIsOn) 52 | { 53 | if (pin < 1) 54 | { 55 | Logger.Warning("Error pin {Pin} open", pin); 56 | return; 57 | } 58 | Logger.Debug("Try to open {Pin}", pin); 59 | _controller.OpenPin(pin, mode, highIsOn ? PinValue.Low : PinValue.High); 60 | Logger.Information("Opened {Pin}", pin); 61 | } 62 | 63 | public void OpenPinModeOutput(int[] pins, bool highIsOn) 64 | { 65 | foreach (var t in pins) 66 | { 67 | OpenPinModeOutput(t, highIsOn); 68 | } 69 | } 70 | 71 | public void Close(int pin) 72 | { 73 | if (pin < 1) 74 | { 75 | Logger.Warning("Error pin {Pin} Close", pin); 76 | return; 77 | } 78 | Logger.Debug("Try to Close {Pin}", pin); 79 | _controller.ClosePin(pin); 80 | Logger.Information("Closed {Pin}", pin); 81 | } 82 | 83 | public void Close(int[] pins, bool highIsOn) 84 | { 85 | foreach (var t in pins) 86 | { 87 | Close(t); 88 | } 89 | } 90 | 91 | public void DoSwitch(int pin, bool state, bool highIsOn) 92 | { 93 | if (pin < 1) 94 | { 95 | Logger.Warning("Try pin {Pin} switch, which is not allowed", pin); 96 | return; 97 | } 98 | Logger.Debug("Try state {State} highIsOn {HighIsOn} pin {Pin}", state, highIsOn, pin); 99 | _controller.Write(pin, highIsOn ? state : !state); 100 | Logger.Information("state {State} highIsOn {HighIsOn} pin {Pin}", state, highIsOn, pin); 101 | } 102 | public void DoSwitch(int[] pin, bool state, bool highIsOn) 103 | { 104 | foreach (var t in pin) 105 | { 106 | DoSwitch(t, state, highIsOn); 107 | } 108 | } 109 | 110 | 111 | public void On(int pin, bool highIsOn) 112 | { 113 | if (pin < 1) 114 | { 115 | Logger.Warning("Error pin {Pin} on", pin); 116 | return; 117 | } 118 | Logger.Debug("Try On {Pin}", pin); 119 | _controller.Write(pin, highIsOn ? PinValue.High : PinValue.Low); 120 | Logger.Information("On {Pin}", pin); 121 | } 122 | 123 | public void On(int[] pin, bool highIsOn) 124 | { 125 | foreach (var t in pin) 126 | { 127 | On(t, highIsOn); 128 | } 129 | } 130 | 131 | public void Off(int pin, bool highIsOn) 132 | { 133 | if (pin == 0) 134 | { 135 | Logger.Warning("Error pin {Pin} off", pin); 136 | return; 137 | } 138 | Logger.Debug("Try Off {Pin}", pin); 139 | _controller.Write(pin, highIsOn ? PinValue.Low : PinValue.High); 140 | Logger.Information("Off {Pin}", pin); 141 | } 142 | public void Off(int[] pin, bool highIsOn) 143 | { 144 | foreach (var t in pin) 145 | { 146 | Off(t, highIsOn); 147 | } 148 | } 149 | 150 | public void Dispose() 151 | { 152 | Logger.Debug("Try to dispose"); 153 | _controller.Dispose(); 154 | Logger.Information("Disposed"); 155 | } 156 | } -------------------------------------------------------------------------------- /Hardware/IGpio.cs: -------------------------------------------------------------------------------- 1 | using System.Device.Gpio; 2 | 3 | namespace PoolControl.Hardware; 4 | 5 | public interface IGpio 6 | { 7 | int ReadPin(int pin); 8 | void Close(int pin); 9 | void Close(int[] pins, bool highIsOn); 10 | void DoSwitch(int pin, bool state, bool highIsOn); 11 | void DoSwitch(int[] pin, bool state, bool highIsOn); 12 | void Off(int pin, bool highIsOn); 13 | void Off(int[] pin, bool highIsOn); 14 | void On(int pin, bool highIsOn); 15 | void On(int[] pin, bool highIsOn); 16 | void Open(int pin, PinMode mode, bool highIsOn); 17 | void OpenPinModeInput(int pin, bool highIsOn); 18 | void OpenPinModeOutput(int pin, bool highIsOn); 19 | void OpenPinModeOutput(int[] pins, bool highIsOn); 20 | void Dispose(); 21 | } -------------------------------------------------------------------------------- /Hardware/MeasurementResult.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Newtonsoft.Json; 3 | 4 | namespace PoolControl.Hardware; 5 | 6 | [JsonObject(MemberSerialization.OptOut)] 7 | public class MeasurementResult 8 | { 9 | public int ReturnCode { get; set; } 10 | 11 | public double Result { get; set; } 12 | 13 | public DateTime TimeStamp { get; set; } 14 | 15 | public string? StatusInfo { get; set; } 16 | 17 | public string? Device { get; set; } 18 | 19 | public string? Command { get; set; } 20 | } 21 | 22 | public enum MeasurementResultCode 23 | { 24 | Success = 1, 25 | SyntaxError = 2, 26 | Pending = 254, 27 | NoDataToSend = 255 28 | } -------------------------------------------------------------------------------- /Hardware/PhMeasurement.cs: -------------------------------------------------------------------------------- 1 | using PoolControl.ViewModels; 2 | using System; 3 | using System.Globalization; 4 | using System.Runtime.InteropServices; 5 | using JetBrains.Annotations; 6 | using PoolControl.Helper; 7 | 8 | namespace PoolControl.Hardware; 9 | 10 | public class PhMeasurement : BaseEzoMeasurement 11 | { 12 | public decimal? Temperature { get; set; } 13 | 14 | private int _i; 15 | private bool _add = true; 16 | private double _ph = 6.9; 17 | 18 | public PhMeasurement() 19 | { 20 | Logger = Log.Logger?.ForContext() ?? throw new ArgumentNullException(nameof(Logger)); 21 | } 22 | 23 | protected override MeasurementResult Send_i2c_command(string command) 24 | { 25 | if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) return base.Send_i2c_command(command); 26 | if (command.ToLower().Equals("status")) 27 | { 28 | return new MeasurementResult { Result = 3.85, ReturnCode = 1, TimeStamp = DateTime.Now, StatusInfo = "?STATUS,P,3.85", Device = $"{GetType().Name}.{ModelBase?.Name}@{I2CAddress}", Command = "Status" }; 29 | } 30 | else 31 | { 32 | _i++; 33 | 34 | var a = _i % 10; 35 | if (a == 0) _add = !_add; 36 | var m = _add ? 1 : -1; 37 | _ph += m * 0.1; 38 | 39 | var code = $"Using WinBaseEZOMock: Class {this.GetType().Name} Command {command} Address {I2CAddress}"; 40 | Logger?.Information("{Code}", code); 41 | return new MeasurementResult { Result = _ph, ReturnCode = 1, TimeStamp = DateTime.Now, StatusInfo = code, Device = $"{GetType().Name}.{ModelBase?.Name}@{I2CAddress}", Command = "ph" }; 42 | } 43 | 44 | } 45 | 46 | 47 | public MeasurementResult MidCalibration(double where) 48 | { 49 | return Calibrate("mid", where); 50 | } 51 | 52 | public MeasurementResult LowCalibration(double where) 53 | { 54 | return Calibrate("low", where); 55 | } 56 | 57 | public MeasurementResult HighCalibration(double where) 58 | { 59 | return Calibrate("high", where); 60 | } 61 | 62 | private MeasurementResult Calibrate(string where, double what) 63 | { 64 | return Send_i2c_command(String.Format(new CultureInfo("en-US"), "Cal,{0},{1:#0.00}", where, what)); 65 | } 66 | 67 | public MeasurementResult Slope() 68 | { 69 | return Send_i2c_command("Slope,?"); 70 | } 71 | 72 | public MeasurementResult ExtendedPhScaleOn() 73 | { 74 | return Send_i2c_command("pHext,1"); 75 | } 76 | 77 | public MeasurementResult ExtendedPhScaleOff() 78 | { 79 | return Send_i2c_command("pHext,0"); 80 | } 81 | 82 | public MeasurementResult GetExtendedPhScale() 83 | { 84 | return Send_i2c_command("pHext,?"); 85 | } 86 | 87 | public MeasurementResult GetTemperatureCompensation() 88 | { 89 | return Send_i2c_command("T,?"); 90 | } 91 | 92 | public MeasurementResult SetTemperatureCompensation(decimal temperature) 93 | { 94 | return Send_i2c_command(String.Format(new CultureInfo("en-US"), "T,{0:#0.0}", temperature)); 95 | } 96 | 97 | private MeasurementResult TakeReadingTemperatureCompensation(decimal? temperature) 98 | { 99 | if (temperature != null) 100 | { 101 | return Send_i2c_command(String.Format(new CultureInfo("en-US"), "RT,{0:#0.0}", temperature)); 102 | } 103 | else 104 | { 105 | return TakeReading(); 106 | } 107 | } 108 | 109 | protected override MeasurementResult DoMeasurement() 110 | { 111 | MeasurementResult vmr = DoVoltageMeasurement(); 112 | ((EzoBase)ModelBase!).Voltage = vmr.Result; 113 | 114 | return TakeReadingTemperatureCompensation(Temperature); 115 | } 116 | } -------------------------------------------------------------------------------- /Hardware/RedoxMeasurement.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.InteropServices; 3 | using JetBrains.Annotations; 4 | using PoolControl.Helper; 5 | 6 | namespace PoolControl.Hardware; 7 | 8 | 9 | public class RedoxMeasurement : BaseEzoMeasurement 10 | { 11 | private int _i; 12 | private bool _add = true; 13 | private double _redox = 730; 14 | 15 | public RedoxMeasurement() 16 | { 17 | Logger = Log.Logger?.ForContext() ?? throw new ArgumentNullException(nameof(Logger)); 18 | } 19 | 20 | protected override MeasurementResult Send_i2c_command(string command) 21 | { 22 | if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) 23 | { 24 | if (command.ToLower().Equals("status")) 25 | { 26 | return new MeasurementResult { Result = 3.84, ReturnCode = 1, TimeStamp = DateTime.Now, StatusInfo = "?STATUS,P,3.84", Device = $"{GetType().Name}.{ModelBase?.Name}@{I2CAddress}", Command = "Status" }; 27 | } 28 | else 29 | { 30 | _i++; 31 | 32 | var a = _i % 10; 33 | if (a == 0) _add = !_add; 34 | var m = _add ? 1 : -1; 35 | _redox += m * 15; 36 | 37 | var code = $"Using WinBaseEZOMock: Class {this.GetType().Name} Command {command} Address {I2CAddress}"; 38 | Logger?.Information("{Code}", code); 39 | return new MeasurementResult { Result = _redox, ReturnCode = 1, TimeStamp = DateTime.Now, StatusInfo = code, Device = $"{GetType().Name}.{ModelBase?.Name}@{I2CAddress}", Command = "ph" }; 40 | } 41 | } 42 | 43 | return base.Send_i2c_command(command); 44 | } 45 | 46 | public MeasurementResult Calibrate(int value) 47 | { 48 | return Send_i2c_command("Cal," + value); 49 | } 50 | } -------------------------------------------------------------------------------- /Hardware/TemperatureMeasurement.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using PoolControl.ViewModels; 4 | using System.Runtime.InteropServices; 5 | using PoolControl.Helper; 6 | 7 | namespace PoolControl.Hardware; 8 | 9 | public class TemperatureMeasurement : BaseMeasurement 10 | { 11 | private string Ds18B20Address => @"/sys/bus/w1/devices/" + ModelBase?.Address + @"/w1_slave"; 12 | private const string WindowsDs18B20Address = "w1_slave"; 13 | 14 | private int _i; 15 | private double _temp = 25.1; 16 | private const double PoolTemp = 25.9; 17 | private bool _add = true; 18 | 19 | public TemperatureMeasurement() 20 | { 21 | Logger = Log.Logger?.ForContext() ?? throw new ArgumentNullException(nameof(Logger)); 22 | } 23 | 24 | private MeasurementResult EmulateWindowsTemp() 25 | { 26 | _i++; 27 | var retValue = PoolTemp; 28 | 29 | if (ModelBase is { Name: "SolarHeater" }) 30 | { 31 | var a = _i % 10; 32 | if (a == 0) _add = !_add; 33 | var m = _add ? 1 : -1; 34 | _temp += m * 1; 35 | retValue = _temp; 36 | } 37 | 38 | var code = $"Using WinBaseEZOMock: Sensor: {ModelBase?.Name} Temperature: {retValue}"; 39 | Logger?.Information("{Code}", code); 40 | return new MeasurementResult { Result = retValue, ReturnCode = 1, TimeStamp = DateTime.Now, StatusInfo = code, Device = $"{GetType().Name}.{ModelBase?.Name}@{Ds18B20Address}", Command = "temperature" }; 41 | } 42 | 43 | protected override MeasurementResult DoMeasurement() 44 | { 45 | var mr = new MeasurementResult { Device = $"{GetType().Name}.{ModelBase?.Name}", Command = "temperature" }; 46 | 47 | if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) 48 | { 49 | 50 | } 51 | 52 | var isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); 53 | 54 | try 55 | { 56 | var content = File.ReadAllLines(isWindows?WindowsDs18B20Address:Ds18B20Address); 57 | foreach (var t in content) 58 | { 59 | Logger?.Debug("{T}",t); 60 | } 61 | var tempData = content[1].Split(new[] { ' ' }, StringSplitOptions.None)[9][2..]; 62 | Logger?.Debug("{TempData}", tempData); 63 | mr.TimeStamp = DateTime.Now; 64 | mr.Result = double.Parse(tempData) / 1000; 65 | mr.StatusInfo = "OK"; 66 | mr.ReturnCode = 1; 67 | Logger?.Debug("Temperature {Temp}", this.GetType().Name); 68 | switch (mr.Result) 69 | { 70 | case > 80: 71 | throw new ArgumentException("Error in DS18B20, Temperature > 80 °C"); 72 | case 0: 73 | throw new ArgumentException("Error in DS18B20, Temperature > 0 °C"); 74 | } 75 | } 76 | catch (Exception ex) 77 | { 78 | Logger?.Error("Error in getting temperature: Key: {Temp}, Name: {Name}", ((Temperature)ModelBase!).Key, ModelBase.Name); 79 | Logger?.Error("{Message}", ex.Message); 80 | Logger?.Verbose(ex, "Verbose Error Logging"); 81 | mr.Result = double.NaN; 82 | mr.ReturnCode = 99; 83 | mr.StatusInfo = ex.Message; 84 | } 85 | 86 | if(isWindows) 87 | { 88 | mr = EmulateWindowsTemp(); 89 | } 90 | 91 | return mr; 92 | } 93 | } -------------------------------------------------------------------------------- /Hardware/WinGpioMock.cs: -------------------------------------------------------------------------------- 1 | using Serilog; 2 | using System; 3 | using System.Device.Gpio; 4 | using Log = PoolControl.Helper.Log; 5 | 6 | namespace PoolControl.Hardware; 7 | 8 | public class WinGpioMock : IGpio 9 | { 10 | private static IGpio? _instance; 11 | private static readonly object Padlock = new(); 12 | 13 | private ILogger Logger { get; init; } 14 | 15 | private WinGpioMock(ILogger? logger) 16 | { 17 | Logger = logger?.ForContext() ?? throw new ArgumentNullException(nameof(logger)); 18 | } 19 | 20 | public static IGpio Instance 21 | { 22 | get 23 | { 24 | lock (Padlock) 25 | { 26 | return _instance ??= new WinGpioMock(Log.Logger); 27 | } 28 | } 29 | } 30 | 31 | public void OpenPinModeOutput(int pin, bool highIsOn) 32 | { 33 | Open(pin, PinMode.Output, highIsOn); 34 | } 35 | 36 | public void OpenPinModeOutput(int[] pins, bool highIsOn) 37 | { 38 | foreach (var t in pins) 39 | { 40 | OpenPinModeOutput(t, highIsOn); 41 | } 42 | } 43 | 44 | public void Close(int pin) 45 | { 46 | if (pin == 0) 47 | { 48 | Logger.Warning("Try pin 0 Close"); 49 | return; 50 | } 51 | Logger.Debug("Try to Close {Pin}", pin); 52 | Logger.Information("Using WinGpioMock"); 53 | Logger.Information("Closed {Pin}", pin); 54 | } 55 | 56 | public void Close(int[] pins, bool highIsOn) 57 | { 58 | foreach (var t in pins) 59 | { 60 | Close(t); 61 | } 62 | } 63 | 64 | public void DoSwitch(int pin, bool state, bool highIsOn) 65 | { 66 | if (pin == 0) 67 | { 68 | Logger.Warning("Try pin 0 switch"); 69 | return; 70 | } 71 | Logger.Debug("Try state {State} highIsOn {HighIsOn} pin {Pin}", state, highIsOn, pin); 72 | Logger.Information("Using WinGpioMock"); 73 | Logger.Information("state {State} highIsOn {HighIsOn} pin {Pin}", state, highIsOn, pin); 74 | } 75 | public void DoSwitch(int[] pin, bool state, bool highIsOn) 76 | { 77 | foreach (var t in pin) 78 | { 79 | DoSwitch(t, state, highIsOn); 80 | } 81 | } 82 | 83 | 84 | public void On(int pin, bool highIsOn) 85 | { 86 | if (pin == 0) 87 | { 88 | Logger.Warning("Try pin 0 on"); 89 | return; 90 | } 91 | Logger.Debug("Try On {Pin}", pin); 92 | Logger.Information("Using WinGpioMock"); 93 | Logger.Information("On {Pin}", pin); 94 | } 95 | 96 | public void On(int[] pin, bool highIsOn) 97 | { 98 | foreach (var t in pin) 99 | { 100 | On(t, highIsOn); 101 | } 102 | } 103 | 104 | public void Off(int pin, bool highIsOn) 105 | { 106 | if (pin == 0) 107 | { 108 | Logger.Warning("Try pin 0 off"); 109 | return; 110 | } 111 | Logger.Debug("Try Off {Pin}", pin); 112 | Logger.Information("Using WinGpioMock"); 113 | Logger.Information("Off {Pin}", pin); 114 | } 115 | public void Off(int[] pin, bool highIsOn) 116 | { 117 | foreach (var t in pin) 118 | { 119 | Off(t, highIsOn); 120 | } 121 | } 122 | 123 | public void Dispose() 124 | { 125 | Logger.Debug("Try to dispose"); 126 | Logger.Information("Using WinGpioMock"); 127 | Logger.Information("Disposed"); 128 | } 129 | 130 | public void Open(int pin, PinMode mode, bool highIsOn) 131 | { 132 | if (pin == 0) 133 | { 134 | Logger.Warning("Try pin 0 open"); 135 | return; 136 | } 137 | Logger.Debug("Try to open {Pin}", pin); 138 | Logger.Information("Using WinGpioMock"); 139 | Logger.Information("Opened {Pin} {Mode}", pin, mode); 140 | } 141 | 142 | public void OpenPinModeInput(int pin, bool highIsOn) 143 | { 144 | Open(pin, PinMode.Input, highIsOn); 145 | } 146 | 147 | public int ReadPin(int pin) 148 | { 149 | return (int)(DateTime.Now.Ticks % 2); 150 | } 151 | } -------------------------------------------------------------------------------- /Helper/Log.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.Extensions.Configuration; 3 | using Serilog; 4 | 5 | namespace PoolControl.Helper; 6 | 7 | public class Log 8 | { 9 | private static ILogger? _logger; 10 | private static readonly object Padlock = new object(); 11 | 12 | public static ILogger? Logger 13 | { 14 | get 15 | { 16 | lock (Padlock) 17 | { 18 | if (_logger != null) return _logger; 19 | try 20 | { 21 | var configuration = new ConfigurationBuilder() 22 | .AddJsonFile("appsettings.json") 23 | .Build(); 24 | 25 | _logger = new LoggerConfiguration() 26 | .ReadFrom.Configuration(configuration) 27 | .CreateLogger(); 28 | 29 | _logger.ForContext().Information("Logger created!"); 30 | } 31 | catch (Exception ex) 32 | { 33 | Console.WriteLine(ex.Message); 34 | } 35 | 36 | return _logger; 37 | } 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /Helper/Persistence.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Runtime.InteropServices; 4 | using Newtonsoft.Json; 5 | using Serilog; 6 | 7 | namespace PoolControl.Helper; 8 | 9 | public class Persistence 10 | { 11 | private static Persistence? _instance; 12 | private static readonly object Padlock = new(); 13 | 14 | private bool _persistenceInUse; 15 | 16 | public static Persistence Instance 17 | { 18 | get 19 | { 20 | lock (Padlock) 21 | { 22 | return _instance ??= new Persistence(Log.Logger); 23 | } 24 | } 25 | } 26 | 27 | protected ILogger Logger { get; init; } 28 | 29 | private string PersistenceFile { get; set; } 30 | 31 | private Persistence(ILogger? logger) 32 | { 33 | Logger = logger?.ForContext() ?? throw new ArgumentNullException(nameof(logger)); 34 | PersistenceFile = PoolControlConfig.Instance.Settings!.PersistenceFile; 35 | if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) 36 | { 37 | PersistenceFile = "win" + PersistenceFile; 38 | } 39 | } 40 | 41 | public string Serialize(object? o) 42 | { 43 | var json = ""; 44 | try 45 | { 46 | json = JsonConvert.SerializeObject(o, Formatting.Indented); 47 | Logger.Information("Persistence Serialized {Json}", json); 48 | } 49 | catch (Exception ex) 50 | { 51 | Logger.Error(ex, "Serialize Exception:"); 52 | } 53 | 54 | return json; 55 | } 56 | 57 | public void Save(object? o) 58 | { 59 | try 60 | { 61 | while (_persistenceInUse) 62 | { 63 | } 64 | 65 | _persistenceInUse = true; 66 | using (StreamWriter file = File.CreateText(PersistenceFile)) 67 | { 68 | var serializer = new JsonSerializer 69 | { 70 | Formatting = Formatting.Indented 71 | }; 72 | serializer.Serialize(file, o); 73 | } 74 | Logger.Information("Persistence Saved {O}", o); 75 | } 76 | catch (Exception ex) 77 | { 78 | Logger.Error(ex, "Save Exception:"); 79 | } 80 | finally 81 | { 82 | _persistenceInUse = false; 83 | } 84 | } 85 | 86 | public T? Load() 87 | { 88 | try 89 | { 90 | while (_persistenceInUse) 91 | { 92 | } 93 | 94 | _persistenceInUse = true; 95 | using var file = File.OpenText(PersistenceFile); 96 | var serializer = new JsonSerializer 97 | { 98 | Formatting = Formatting.Indented 99 | }; 100 | var o = serializer.Deserialize(new JsonTextReader(file)); 101 | Logger.Information("Persistence Loaded {O}", o); 102 | return o; 103 | } 104 | catch (Exception ex) 105 | { 106 | Logger.Error(ex, "Load Exception:"); 107 | return Activator.CreateInstance(); 108 | } 109 | finally 110 | { 111 | _persistenceInUse=false; 112 | } 113 | } 114 | } -------------------------------------------------------------------------------- /Helper/PoolControlConfig.cs: -------------------------------------------------------------------------------- 1 | using JetBrains.Annotations; 2 | using Microsoft.Extensions.Configuration; 3 | 4 | namespace PoolControl.Helper; 5 | 6 | public class PoolControlConfig 7 | { 8 | private static PoolControlConfig? _instance; 9 | private static readonly object Padlock = new(); 10 | 11 | public static PoolControlConfig Instance 12 | { 13 | get 14 | { 15 | lock (Padlock) 16 | { 17 | return _instance ??= new PoolControlConfig(); 18 | } 19 | 20 | } 21 | } 22 | 23 | private IConfiguration? Config { get; set; } 24 | public Settings? Settings { get; private set; } 25 | 26 | private PoolControlConfig() 27 | { 28 | Config = new ConfigurationBuilder() 29 | .AddJsonFile("appsettings.json") 30 | .Build(); 31 | Settings = Config.GetRequiredSection("Settings").Get(); 32 | } 33 | } 34 | 35 | public class BaseTopicSettings 36 | { 37 | public string Command { get; set; } = "basetopic/cmd/"; 38 | public string State { get; set; } = "basetopic/state/"; 39 | } 40 | 41 | public class LWTSettings 42 | { 43 | public string? ConnectMessage { get; set; } = "Connected"; 44 | public string? DisconnectMessage { get; set; } = "Connection Lost"; 45 | public string Topic { get; set; } = "basetopic/LWT"; 46 | } 47 | 48 | public class MQTTSettings 49 | { 50 | public string Password { get; set; } = ""; 51 | public int Port { get; set; } = 1883; 52 | public string Server { get; set; } = ""; 53 | 54 | public string User { get; set; } = ""; 55 | } 56 | 57 | public class Settings 58 | { 59 | public BaseTopicSettings BaseTopic { get; set; } = null!; 60 | public LWTSettings LWT { get; set; } = null!; 61 | public MQTTSettings MQTT { get; set; } = null!; 62 | 63 | public string PersistenceFile { get; set; } = "poolcontrolviewmodel.json"; 64 | 65 | public int PersistenceSaveIntervalInSec { get; set; } = 60; 66 | } -------------------------------------------------------------------------------- /Helper/PoolControlHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | using System.Linq.Expressions; 4 | 5 | namespace PoolControl.Helper; 6 | 7 | public static class PoolControlHelper 8 | { 9 | public static string GetPropertyName(Expression> propertyLambda) 10 | { 11 | MemberExpression? me = propertyLambda.Body as MemberExpression; 12 | if (me == null) 13 | { 14 | throw new ArgumentException("You must pass a lambda of the form: '() => Class.Property' or '() => object.Property'"); 15 | } 16 | 17 | string result = string.Empty; 18 | do 19 | { 20 | result = me.Member.Name + "." + result; 21 | me = me.Expression as MemberExpression; 22 | } while (me != null); 23 | 24 | result = result.Remove(result.Length - 1); // remove the trailing "." 25 | return result; 26 | } 27 | 28 | public static decimal getDecimal(string value) 29 | { 30 | return decimal.Parse(value, new CultureInfo("en-US")); 31 | } 32 | 33 | public static string format0Decimal(double value) 34 | { 35 | return Math.Round(value, 0).ToString("#0", new CultureInfo("en-US")); 36 | } 37 | 38 | public static string format1Decimal(double value) 39 | { 40 | return Math.Round(value, 1).ToString("#0.0", new CultureInfo("en-US")); 41 | } 42 | 43 | public static string format2Decimal(double value) 44 | { 45 | return Math.Round(value, 2).ToString("#0.00", new CultureInfo("en-US")); 46 | } 47 | 48 | public static string format3Decimal(double value) 49 | { 50 | return Math.Round(value, 3).ToString("#0.000", new CultureInfo("en-US")); 51 | } 52 | } -------------------------------------------------------------------------------- /Helper/PropertySetter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Globalization; 4 | using System.Reflection; 5 | 6 | namespace PoolControl.Helper; 7 | 8 | public class Result 9 | { 10 | public bool Success { get; init; } 11 | public string? Message { get; init; } 12 | } 13 | 14 | public static class PropertySetter 15 | { 16 | private static bool setProperty(object? parent, PropertyInfo? myPropInfo, string value) 17 | { 18 | if (myPropInfo?.PropertyType == typeof(decimal)) 19 | { 20 | myPropInfo.SetValue(parent, decimal.Parse(value, new CultureInfo("en-US"))); 21 | } 22 | else if (myPropInfo?.PropertyType == typeof(int)) 23 | { 24 | myPropInfo.SetValue(parent, int.Parse(value, new CultureInfo("en-US"))); 25 | } 26 | else if (myPropInfo?.PropertyType == typeof(double)) 27 | { 28 | myPropInfo.SetValue(parent, double.Parse(value, new CultureInfo("en-US"))); 29 | } 30 | else if (myPropInfo?.PropertyType == typeof(float)) 31 | { 32 | myPropInfo.SetValue(parent, float.Parse(value, new CultureInfo("en-US"))); 33 | } 34 | else if (myPropInfo?.PropertyType == typeof(string)) 35 | { 36 | myPropInfo.SetValue(parent, value); 37 | } 38 | else if (myPropInfo?.PropertyType == typeof(bool)) 39 | { 40 | myPropInfo.SetValue(parent, !(value.StartsWith("0") || value.ToLower().StartsWith("fa") || value.ToLower().StartsWith("of"))); 41 | } 42 | else 43 | { 44 | return false; 45 | } 46 | 47 | return true; 48 | } 49 | 50 | /* 51 | * baseObject 52 | * | 53 | * --- objectNameToSet[key] 54 | * | 55 | * ---propertyName = propertyValue 56 | * or 57 | * 58 | * baseObject 59 | * | 60 | * ---propertyName = propertyValue 61 | * 62 | */ 63 | public static Result setProperty(object? baseObject, string propertyName, string propertyValue, string objectNameToSet = "", string key = "") 64 | { 65 | var info = $"setting Object: {baseObject} Object2: {objectNameToSet} Key: {key} Property: {propertyName} Value: {propertyValue}"; 66 | 67 | PropertyInfo? childPropertyInfo = null; 68 | 69 | try 70 | { 71 | object? propertyObject; 72 | if (string.IsNullOrEmpty(objectNameToSet)) 73 | { 74 | propertyObject = baseObject; 75 | } 76 | else 77 | { 78 | if (string.IsNullOrEmpty(key)) 79 | { 80 | propertyObject = baseObject?.GetType().GetProperty(objectNameToSet)?.GetValue(baseObject); 81 | } 82 | else 83 | { 84 | var dict = (Dictionary)baseObject?.GetType().GetProperty(objectNameToSet + "Obj")?.GetValue(baseObject)!; 85 | 86 | propertyObject = dict[key]; 87 | } 88 | 89 | childPropertyInfo = propertyObject?.GetType().GetProperty(propertyName); 90 | } 91 | 92 | if (!setProperty(propertyObject, childPropertyInfo, propertyValue)) 93 | { 94 | return new Result { Success = false, Message = $"Error while setting Object: {propertyObject} Object2: {objectNameToSet} Key: {key} Property: {childPropertyInfo?.Name} Value: {propertyValue}" }; 95 | } 96 | } 97 | catch (Exception ex) 98 | { 99 | return new Result { Success = false, Message = $"{ex.Message} while {info}" }; 100 | } 101 | 102 | return new Result { Success = true, Message = "Success: {info}" }; 103 | } 104 | } -------------------------------------------------------------------------------- /Pages/Cistern.axaml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 10 | 11 | 12 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /Pages/Cistern.axaml.cs: -------------------------------------------------------------------------------- 1 | using Avalonia.Controls; 2 | using Avalonia.Markup.Xaml; 3 | 4 | namespace PoolControl.Pages; 5 | 6 | public partial class Cistern : UserControl 7 | { 8 | public Cistern() 9 | { 10 | InitializeComponent(); 11 | } 12 | } -------------------------------------------------------------------------------- /Pages/FilterPump.axaml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 28 | 29 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /Pages/FilterPump.axaml.cs: -------------------------------------------------------------------------------- 1 | using Avalonia.Controls; 2 | using Avalonia.Markup.Xaml; 3 | 4 | namespace PoolControl.Pages; 5 | 6 | public partial class FilterPump : UserControl 7 | { 8 | public FilterPump() 9 | { 10 | InitializeComponent(); 11 | } 12 | } -------------------------------------------------------------------------------- /Pages/Overview.axaml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 15 | 18 | 19 | 20 | 23 | 24 | 31 | 34 | 35 | 36 | 39 | 42 | 43 | 44 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /Pages/Overview.axaml.cs: -------------------------------------------------------------------------------- 1 | using Avalonia.Controls; 2 | using Avalonia.Interactivity; 3 | using Avalonia.Markup.Xaml; 4 | 5 | namespace PoolControl.Pages; 6 | 7 | public partial class Overview : UserControl 8 | { 9 | public Overview() 10 | { 11 | InitializeComponent(); 12 | } 13 | 14 | private void OnButtonClick(object sender, RoutedEventArgs e) 15 | { 16 | var temperatureConfig = new TemperatureConfig(); 17 | var temperature = ((Button)sender).Tag; 18 | temperatureConfig.DataContext = temperature; 19 | temperatureConfig.ShowDialog(App.MainWindow); 20 | } 21 | 22 | private void OnButtonClickPh(object sender, RoutedEventArgs e) 23 | { 24 | var pHConfig = new PhConfig(); 25 | var temperature = ((Button)sender).Tag; 26 | pHConfig.DataContext = temperature; 27 | pHConfig.ShowDialog(App.MainWindow); 28 | } 29 | 30 | private void OnButtonClickRedox(object sender, RoutedEventArgs e) 31 | { 32 | var redoxConfig = new RedoxConfig(); 33 | var temperature = ((Button)sender).Tag; 34 | redoxConfig.DataContext = temperature; 35 | redoxConfig.ShowDialog(App.MainWindow); 36 | } 37 | 38 | private void OnButtonClickDistance(object sender, RoutedEventArgs e) 39 | { 40 | var temperatureConfig = new TemperatureConfig(); 41 | var temperature = ((Button)sender).Tag; 42 | temperatureConfig.DataContext = temperature; 43 | temperatureConfig.ShowDialog(App.MainWindow); 44 | } 45 | } -------------------------------------------------------------------------------- /Pages/PhConfig.axaml: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 19 | 20 | 23 | 24 | 27 | 28 | 31 | 32 | 35 | 36 | 39 | 40 | 43 | 44 | 47 | 48 | 51 | 52 | 53 | 16 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 31 | 32 |