├── .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 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
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 |
25 |
26 |
27 |
28 |
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 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
--------------------------------------------------------------------------------
/Pages/PhConfig.axaml.cs:
--------------------------------------------------------------------------------
1 | using Avalonia;
2 | using Avalonia.Controls;
3 | using Avalonia.Interactivity;
4 | using Avalonia.Markup.Xaml;
5 |
6 | namespace PoolControl.Pages;
7 |
8 | public partial class PhConfig : Window
9 | {
10 | public PhConfig()
11 | {
12 | InitializeComponent();
13 | }
14 |
15 | private void OnButtonClick(object sender, RoutedEventArgs e)
16 | {
17 | Close();
18 | }
19 | }
--------------------------------------------------------------------------------
/Pages/PhValue.axaml:
--------------------------------------------------------------------------------
1 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/Pages/PhValue.axaml.cs:
--------------------------------------------------------------------------------
1 | using Avalonia.Controls;
2 | using Avalonia.Markup.Xaml;
3 |
4 | namespace PoolControl.Pages;
5 |
6 | public partial class PhValue : UserControl
7 | {
8 | public PhValue()
9 | {
10 | InitializeComponent();
11 | }
12 | }
--------------------------------------------------------------------------------
/Pages/Redox.axaml:
--------------------------------------------------------------------------------
1 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/Pages/Redox.axaml.cs:
--------------------------------------------------------------------------------
1 | using Avalonia;
2 | using Avalonia.Controls;
3 | using Avalonia.Markup.Xaml;
4 |
5 | namespace PoolControl.Pages;
6 |
7 | public partial class Redox : UserControl
8 | {
9 | public Redox()
10 | {
11 | InitializeComponent();
12 | }
13 | }
--------------------------------------------------------------------------------
/Pages/RedoxConfig.axaml:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
--------------------------------------------------------------------------------
/Pages/RedoxConfig.axaml.cs:
--------------------------------------------------------------------------------
1 | using Avalonia;
2 | using Avalonia.Controls;
3 | using Avalonia.Interactivity;
4 | using Avalonia.Markup.Xaml;
5 |
6 | namespace PoolControl.Pages;
7 |
8 | public partial class RedoxConfig : Window
9 | {
10 | public RedoxConfig()
11 | {
12 | InitializeComponent();
13 | }
14 |
15 | private void OnButtonClick(object sender, RoutedEventArgs e)
16 | {
17 | Close();
18 | }
19 | }
--------------------------------------------------------------------------------
/Pages/SolarHeater.axaml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/Pages/SolarHeater.axaml.cs:
--------------------------------------------------------------------------------
1 | using Avalonia;
2 | using Avalonia.Controls;
3 | using Avalonia.Markup.Xaml;
4 |
5 | namespace PoolControl.Pages;
6 |
7 | public partial class SolarHeater : UserControl
8 | {
9 | public SolarHeater()
10 | {
11 | InitializeComponent();
12 | }
13 | }
--------------------------------------------------------------------------------
/Pages/TemperatureConfig.axaml:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
--------------------------------------------------------------------------------
/Pages/TemperatureConfig.axaml.cs:
--------------------------------------------------------------------------------
1 | using Avalonia;
2 | using Avalonia.Controls;
3 | using Avalonia.Interactivity;
4 | using Avalonia.Markup.Xaml;
5 |
6 | namespace PoolControl.Pages;
7 |
8 | public partial class TemperatureConfig : Window
9 | {
10 | public TemperatureConfig()
11 | {
12 | InitializeComponent();
13 | }
14 |
15 | private void OnButtonClick(object sender, RoutedEventArgs e)
16 | {
17 | Close();
18 | }
19 | }
--------------------------------------------------------------------------------
/PoolControl.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | WinExe
4 | net8.0
5 | enable
6 |
7 | copyused
8 | true
9 |
10 |
11 |
12 | %(Filename)
13 |
14 |
15 | Designer
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 | FilterPump.axaml
69 |
70 |
71 | RedoxConfig.axaml
72 |
73 |
74 | PhValue.axaml
75 |
76 |
77 | Redox.axaml
78 |
79 |
80 | SolarHeater.axaml
81 |
82 |
83 | Cistern.axaml
84 |
85 |
86 | PhConfig.axaml
87 |
88 |
89 | True
90 | True
91 | Resource.resx
92 |
93 |
94 |
95 |
96 | PublicResXFileCodeGenerator
97 | Resource.Designer.cs
98 |
99 |
100 |
101 |
102 | Always
103 |
104 |
105 | Never
106 |
107 |
108 | PreserveNewest
109 |
110 |
111 | PreserveNewest
112 |
113 |
114 |
115 |
--------------------------------------------------------------------------------
/PoolControl.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 17
4 | VisualStudioVersion = 17.1.32407.343
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PoolControl", "PoolControl.csproj", "{887CCB16-1E5C-4CE6-916D-119DDEBA7D62}"
7 | EndProject
8 | Global
9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
10 | Debug|Any CPU = Debug|Any CPU
11 | Release|Any CPU = Release|Any CPU
12 | EndGlobalSection
13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
14 | {887CCB16-1E5C-4CE6-916D-119DDEBA7D62}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
15 | {887CCB16-1E5C-4CE6-916D-119DDEBA7D62}.Debug|Any CPU.Build.0 = Debug|Any CPU
16 | {887CCB16-1E5C-4CE6-916D-119DDEBA7D62}.Release|Any CPU.ActiveCfg = Release|Any CPU
17 | {887CCB16-1E5C-4CE6-916D-119DDEBA7D62}.Release|Any CPU.Build.0 = Release|Any CPU
18 | EndGlobalSection
19 | GlobalSection(SolutionProperties) = preSolution
20 | HideSolutionNode = FALSE
21 | EndGlobalSection
22 | GlobalSection(ExtensibilityGlobals) = postSolution
23 | SolutionGuid = {4D0718B9-67B4-44EE-84FC-A7AAD304AE8F}
24 | EndGlobalSection
25 | EndGlobal
26 |
--------------------------------------------------------------------------------
/Program.cs:
--------------------------------------------------------------------------------
1 | using Avalonia;
2 | using Avalonia.ReactiveUI;
3 | using System;
4 |
5 | namespace PoolControl;
6 |
7 | internal static class Program
8 | {
9 | // Initialization code. Don't use any Avalonia, third-party APIs or any
10 | // SynchronizationContext-reliant code before AppMain is called: things aren't initialized
11 | // yet and stuff might break.
12 | [STAThread]
13 | public static void Main(string[] args) => BuildAvaloniaApp()
14 | .StartWithClassicDesktopLifetime(args);
15 |
16 | // Avalonia configuration, don't remove; also used by visual designer.
17 | public static AppBuilder BuildAvaloniaApp()
18 | => AppBuilder.Configure()
19 | .UsePlatformDetect()
20 | .LogToTrace()
21 | .UseReactiveUI();
22 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # PoolControl
2 |
3 | Update: This software is in production in my pool environment for about 1 year without any issues ;-)
4 |
5 | This is a brandnew Pool Control Software based on an Raspberry Pi, coded in .net CORE 8.0. It runs with mocks in an Windows Environment.
6 | Hardware:
7 | - Raspberry Pi 4 4GB (Raspberry Pi 3B+ 1GB is sufficient)
8 | - Waveshare 3,5" TouchDisplay
9 | - Raspberry Pi GPIO Extension Board 4x GPIO Header of Pi (beyond the Display)
10 | - 8-Kanal-Relais 5V
11 | - Logic Converter TSX0108E, to convert 3V of GPIOs to 5V for relay switching
12 | - Atlas Ezo Redox Platine (blue, I2C)
13 | - Atlas Ezo pH Platine (red, I2C)
14 | - Housing designed with Tinkercad and printed with Prusa
15 |
16 | Software
17 | - OS: Raspbian bullseye 5.15.32 mit voller Desktop-Unterstützung
18 | - .net Core 8.0, in Visual Studio C#
19 | - Avalonia UI Touch-UI with XAML and ReactiveUI / Fody
20 | - Mulit Language with Resource-File, german und englisch implemented
21 | - Serilog als Logger, Console, File, Syslog
22 | - MQTTnet for Communication
23 | - NewtonsoftJSON for serializing Json and Configuration
24 | - System.Device.GPIO for GPIO handling
25 | - I2C Driver for Ezo Ph and Redox Measurement
26 | - Driver für GPIO HC-SR04 Sensor
27 |
28 | Features:
29 | - Measurement oft DS18B20 OneWire temperature sensors
30 | - 8-channel relay hanbdlong for pool filter, solar heater, redox, pH and pool lamp
31 | - automatic pH acid dosing
32 | - automatic redox handling (salt electrolyse)
33 | - turn pool filtre on off depending on temperature and cinfigured times 2 x 3h / day at temperatures below 20°C and up to 2 x 4 h a day at temperatures > 30 °C (linear)
34 | - Measurment of filling of my rain water cistern
35 | - automaitc handling ofsolar heater over DS18B20 sensor
36 | - cleaning of solar heater at s specified time per day
37 | - All measurments will be transfered over MQTT (temperatures, pH, redox, cistern)
38 | - all configuration parameters are abel to be configured over MQTT
39 | - all relays could be switched over MQTT
40 | - pH and redox sSensor could be handled over MQTT, including calibration
41 | - I integrated this in Openhab as generic MQTT Thing
42 | - Notification of manual inputs with related MQTT topigs
43 |
44 | The printable .stl files could be found here (Housing for Raspberry Pi 3B+ an 4):
45 | https://www.thingiverse.com/thing:5383565
46 |
47 | To install, I assume, that you have a raspberry pi running with a Desktop environment. If not, checkout https://www.raspberrypi.com/software/ an download the imager and install latest raspberry pi os (32bit) with Desktop environment (bookworm is not working with waveshare at the moment but bullseye does!).
48 | To install the Waveshare 3,5" TouchDisplay follow the instructions on the waveshare page. I use 3.5 inch RPi LCD (V) Rev2.0:
49 | https://www.waveshare.com/wiki/3.5inch_RPi_LCD_(B)
50 | In short:
51 | ```
52 | git clone https://github.com/waveshare/LCD-show.git
53 | cd LCD-show/
54 | chmod +x LCD35B-show-V2
55 | ./LCD35B-show-V2
56 | ```
57 | Then install .net:
58 | ```
59 | wget -O - https://raw.githubusercontent.com/pjgpetecodes/dotnet8pi/main/install.sh | sudo bash
60 | ```
61 | Impressions:
62 |
63 | 
64 |
65 | 
66 |
67 | 
68 |
69 | 
70 |
71 | 
72 |
73 | 
74 |
75 | 
76 |
77 | 
78 |
79 | 
80 |
81 |
--------------------------------------------------------------------------------
/Styles/PoolResources.axaml:
--------------------------------------------------------------------------------
1 |
5 |
7 |
8 |
9 |
10 |
11 |
12 |
14 |
15 |
16 |
17 |
18 |
19 |
21 |
22 |
23 |
24 |
25 |
26 |
28 |
29 |
30 |
31 |
32 |
33 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
--------------------------------------------------------------------------------
/Styles/SideBar.axaml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
52 |
53 |
66 |
69 |
72 |
73 |
76 |
77 |
--------------------------------------------------------------------------------
/Styles/Styles.axaml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
16 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 | 40 0 40 0
39 | avares://PoolControl/Assets/Fonts#Source Code Pro
40 |
41 |
42 |
47 |
55 |
64 |
75 |
85 |
88 |
91 |
99 |
105 |
110 |
113 |
117 |
124 |
130 |
135 |
140 |
145 |
150 |
156 |
161 |
165 |
169 |
173 |
177 |
182 |
185 |
188 |
191 |
194 |
195 |
--------------------------------------------------------------------------------
/Time/TimeTrigger.cs:
--------------------------------------------------------------------------------
1 | using Serilog;
2 | using System;
3 | using System.Threading;
4 | using Log = PoolControl.Helper.Log;
5 |
6 | namespace PoolControl.Time;
7 |
8 | public class TimeTrigger
9 | {
10 | private readonly ILogger _logger;
11 |
12 | public string Name { get; init; } = "NoName";
13 |
14 | public TimeSpan StartTime { get; set; }
15 |
16 | public TimeSpan Period { get; init; }
17 |
18 | public DateTime TriggerTime { get; private set; }
19 |
20 | private Timer? _timer;
21 |
22 |
23 | public TimeTrigger()
24 | {
25 | _logger = Log.Logger?.ForContext() ?? throw new ArgumentNullException(nameof(_logger));
26 | }
27 |
28 | private void OnTimerTicked(object? state)
29 | {
30 | OnTimeTriggered?.Invoke();
31 | InitiateTimer();
32 | }
33 |
34 | public void InitiateTimer()
35 | {
36 | if(StartTime <= TimeSpan.Zero || Period <= TimeSpan.Zero)
37 | {
38 | _logger.Debug("Timer {Name} not started because of zero values StartTime: {StartTime} Period: {Period}", Name, StartTime, Period);
39 | return;
40 | }
41 |
42 | DateTime now = DateTime.Now;
43 | var triggerTime = DateTime.Today + StartTime - now;
44 | while(triggerTime < TimeSpan.Zero)
45 | {
46 | triggerTime += Period;
47 | }
48 |
49 | if (_timer == null)
50 | {
51 | var autoEvent = new AutoResetEvent(false);
52 | _timer = new Timer(OnTimerTicked, autoEvent, triggerTime, Period);
53 | _logger.Debug("New Timer {Name} created", Name);
54 | }
55 | else
56 | {
57 | _timer.Change(triggerTime, Period);
58 | }
59 |
60 | TriggerTime = now + triggerTime;
61 | _logger.Debug("Timer: {Name} TriggerTime: {TriggerTime} StartTime: {StartTime} Period: {Period}", Name, TriggerTime, StartTime, Period);
62 | }
63 |
64 | public event Action? OnTimeTriggered;
65 | }
--------------------------------------------------------------------------------
/UserControls/DoubleMeasurementControl.axaml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/UserControls/DoubleMeasurementControl.axaml.cs:
--------------------------------------------------------------------------------
1 | using Avalonia;
2 | using Avalonia.Controls;
3 | using Avalonia.Media;
4 |
5 | namespace PoolControl.UserControls;
6 |
7 | public partial class DoubleMeasurementControl : UserControl
8 | {
9 | public DoubleMeasurementControl()
10 | {
11 | InitializeComponent();
12 | }
13 |
14 | public static readonly StyledProperty LabelProperty = AvaloniaProperty.Register(nameof(Label));
15 | public static readonly StyledProperty FirstMeasurementProperty = AvaloniaProperty.Register(nameof(FirstMeasurement));
16 | public static readonly StyledProperty SecondMeasurementProperty = AvaloniaProperty.Register(nameof(SecondMeasurement));
17 | public static readonly StyledProperty ButtonBackgroundProperty = AvaloniaProperty.Register(nameof(ButtonBackground));
18 |
19 | public string Label
20 | {
21 | get => this.GetValue(LabelProperty);
22 | set => this.SetValue(LabelProperty, value);
23 | }
24 |
25 | public string FirstMeasurement
26 | {
27 | get => this.GetValue(FirstMeasurementProperty);
28 | set => this.SetValue(FirstMeasurementProperty, value);
29 | }
30 |
31 | public string SecondMeasurement
32 | {
33 | get => this.GetValue(SecondMeasurementProperty);
34 | set => this.SetValue(SecondMeasurementProperty, value);
35 | }
36 |
37 | public IBrush ButtonBackground
38 | {
39 | get => this.GetValue(ButtonBackgroundProperty);
40 | set => this.SetValue(ButtonBackgroundProperty, value);
41 | }
42 | }
--------------------------------------------------------------------------------
/UserControls/MeasurementControl.axaml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/UserControls/MeasurementControl.axaml.cs:
--------------------------------------------------------------------------------
1 | using Avalonia;
2 | using Avalonia.Controls;
3 | using Avalonia.Media;
4 |
5 | namespace PoolControl.UserControls;
6 |
7 | public partial class MeasurementControl : UserControl
8 | {
9 | public MeasurementControl()
10 | {
11 | InitializeComponent();
12 | }
13 |
14 | public static readonly StyledProperty LabelProperty = AvaloniaProperty.Register(nameof(Label));
15 | public static readonly StyledProperty MeasurementProperty = AvaloniaProperty.Register(nameof(Measurement));
16 | public static readonly StyledProperty ButtonBackgroundProperty = AvaloniaProperty.Register(nameof(ButtonBackground));
17 |
18 | public string Label
19 | {
20 | get => this.GetValue(LabelProperty);
21 | set => this.SetValue(LabelProperty, value);
22 | }
23 |
24 | public string Measurement
25 | {
26 | get => this.GetValue(MeasurementProperty);
27 | set => this.SetValue(MeasurementProperty, value);
28 | }
29 |
30 | public IBrush ButtonBackground
31 | {
32 | get => this.GetValue(ButtonBackgroundProperty);
33 | set => this.SetValue(ButtonBackgroundProperty, value);
34 | }
35 | }
--------------------------------------------------------------------------------
/UserControls/NumControl.axaml:
--------------------------------------------------------------------------------
1 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/UserControls/NumControl.axaml.cs:
--------------------------------------------------------------------------------
1 | using Avalonia;
2 | using Avalonia.Controls;
3 | using Avalonia.Markup.Xaml;
4 | using Avalonia.Media;
5 | using ReactiveUI.Fody.Helpers;
6 |
7 | namespace PoolControl.UserControls;
8 |
9 | public partial class NumControl : UserControl
10 | {
11 | public NumControl()
12 | {
13 | InitializeComponent();
14 | }
15 |
16 | private void InitializeComponent()
17 | {
18 | AvaloniaXamlLoader.Load(this);
19 | }
20 |
21 | public static readonly StyledProperty LabelProperty = AvaloniaProperty.Register(nameof(Label));
22 | public static readonly StyledProperty FormatStringProperty = AvaloniaProperty.Register(nameof(FormatString), "0");
23 | public static readonly StyledProperty NumValueProperty = AvaloniaProperty.Register(nameof(NumValue));
24 | public static readonly StyledProperty MinimumProperty = AvaloniaProperty.Register(nameof(Minimum));
25 | public static readonly StyledProperty MaximumProperty = AvaloniaProperty.Register(nameof(Maximum));
26 | public static readonly StyledProperty IncrementProperty = AvaloniaProperty.Register(nameof(Increment));
27 | public static readonly StyledProperty ColumnWidthProperty = AvaloniaProperty.Register(nameof(ColumnWidth));
28 | public static readonly StyledProperty ControlBackgroundProperty = AvaloniaProperty.Register(nameof(ControlBackground));
29 |
30 | [Reactive]
31 | public string Label
32 | {
33 | get => this.GetValue(LabelProperty);
34 | set => this.SetValue(LabelProperty, value);
35 | }
36 |
37 | [Reactive]
38 | public string FormatString
39 | {
40 | get => this.GetValue(FormatStringProperty);
41 | set => this.SetValue(FormatStringProperty, value);
42 | }
43 |
44 | [Reactive]
45 | public double NumValue
46 | {
47 | get => this.GetValue(NumValueProperty);
48 | set => this.SetValue(NumValueProperty, value);
49 | }
50 |
51 | [Reactive]
52 | public double Minimum
53 | {
54 | get => this.GetValue(MinimumProperty);
55 | set => this.SetValue(MinimumProperty, value);
56 | }
57 |
58 | [Reactive]
59 | public double Maximum
60 | {
61 | get => this.GetValue(MaximumProperty);
62 | set => this.SetValue(MaximumProperty, value);
63 | }
64 |
65 | [Reactive]
66 | public double Increment
67 | {
68 | get => this.GetValue(IncrementProperty);
69 | set => this.SetValue(IncrementProperty, value);
70 | }
71 |
72 | [Reactive]
73 | public GridLength ColumnWidth
74 | {
75 | get => this.GetValue(ColumnWidthProperty);
76 | set => this.SetValue(ColumnWidthProperty, value);
77 | }
78 |
79 | [Reactive]
80 | public IBrush ControlBackground
81 | {
82 | get => this.GetValue(ControlBackgroundProperty);
83 | set => this.SetValue(ControlBackgroundProperty, value);
84 | }
85 | }
--------------------------------------------------------------------------------
/ViewLocator.cs:
--------------------------------------------------------------------------------
1 | using Avalonia.Controls;
2 | using Avalonia.Controls.Templates;
3 | using PoolControl.ViewModels;
4 | using System;
5 |
6 | namespace PoolControl;
7 |
8 | public class ViewLocator : IDataTemplate
9 | {
10 | public Control Build(object data)
11 | {
12 | var name = data.GetType().FullName!.Replace("ViewModel", "View");
13 | var type = Type.GetType(name);
14 |
15 | if (type != null)
16 | {
17 | return (Control)Activator.CreateInstance(type)!;
18 | }
19 | else
20 | {
21 | return new TextBlock { Text = "Not Found: " + name };
22 | }
23 | }
24 |
25 | public bool Match(object data)
26 | {
27 | return data is ViewModelBase;
28 | }
29 | }
--------------------------------------------------------------------------------
/ViewModels/Distance.cs:
--------------------------------------------------------------------------------
1 | using Newtonsoft.Json;
2 | using ReactiveUI;
3 | using ReactiveUI.Fody.Helpers;
4 | using System;
5 | using System.Globalization;
6 | using PoolControl.Helper;
7 |
8 | namespace PoolControl.ViewModels;
9 |
10 | [JsonObject(MemberSerialization.OptIn)]
11 | public class Distance : MeasurementModelBase
12 | {
13 | ///
14 | /// This Model will be used to hold the data for a Distance of a Water Box, and then it will be calculated to liters
15 | ///
16 | public Distance()
17 | {
18 | // Calculate Liter
19 | this.WhenAnyValue(ds => ds.Value, GetLiter).ToPropertyEx(this, ds => ds.ValueL, deferSubscription: true);
20 |
21 | // Publish Liter
22 | this.WhenAnyValue(ds => ds.ValueL).Subscribe(_ => PublishMessageValueL());
23 |
24 | // Create view
25 | this.WhenAnyValue(ds => ds.NameL, ds => ds.ValueL, ds => ds.ViewFormatL, ds => ds.UnitSignL, (_, temperature, viewFormat, unitSign) => $"{LocationNameL}: {temperature.ToString(viewFormat)} {unitSign}").ToPropertyEx(this, ds => ds.FullTextL, deferSubscription: true);
26 | this.WhenAnyValue(ds => ds.NameL, ds => ds.ValueL, (_, _) => $"{LocationNameL}:").ToPropertyEx(this, ds => ds.LabelL, deferSubscription: true);
27 | this.WhenAnyValue(ds => ds.ValueL, ds => ds.ViewFormatL, ds => ds.UnitSignL, (value, viewFormat, unitSign) => $"{value.ToString(viewFormat)} {unitSign}").ToPropertyEx(this, ds => ds.ValueWithUnitL, deferSubscription: true);
28 | }
29 |
30 | private double GetLiter(double cm)
31 | {
32 | return 7.854 * (90 + 9.26 - cm * 0.95);
33 | }
34 |
35 | protected void PublishMessageValueL()
36 | {
37 | PublishMessageWithType(PoolControlHelper.GetPropertyName(() => ValueL), InterfaceFormatDecimalPointL, false);
38 | }
39 |
40 | [Reactive]
41 | [JsonProperty]
42 | public string? NameL { get; set; }
43 |
44 | public string LocationNameL
45 | {
46 | get
47 | {
48 | var ret = "Nix";
49 | try
50 | {
51 | if (NameL != null) ret = (string)typeof(Resource).GetProperty(NameL)?.GetValue(null)!;
52 | }
53 | catch (Exception)
54 | {
55 | // ignored
56 | }
57 |
58 | return ret;
59 | }
60 | }
61 |
62 | [Reactive]
63 | [JsonProperty]
64 | public string? UnitSignL { get; set; }
65 |
66 | [Reactive]
67 | [JsonProperty]
68 | public string? ViewFormatL { get; set; }
69 |
70 | [Reactive]
71 | [JsonProperty]
72 | public string? InterfaceFormatL { get; set; }
73 |
74 | [JsonIgnore]
75 | public string InterfaceFormatDecimalPointL => ValueL.ToString(InterfaceFormatL, new CultureInfo("en-US"));
76 |
77 | [JsonIgnore]
78 | [ObservableAsProperty]
79 | public double ValueL { get; }
80 |
81 | [JsonIgnore]
82 | [ObservableAsProperty]
83 | public string? FullTextL { get; }
84 |
85 | [JsonIgnore]
86 | [ObservableAsProperty]
87 | public string? LabelL { get; }
88 |
89 | [JsonIgnore]
90 | [ObservableAsProperty]
91 | public string? ValueWithUnitL { get; }
92 |
93 | [Reactive]
94 | [JsonProperty]
95 | public int NumberOfMeasurements { get; set; }
96 |
97 | [JsonIgnore]
98 | public int Trigger
99 | {
100 | get
101 | {
102 | var address = -1;
103 | try
104 | {
105 | address = int.Parse(Address?.Split('/')[0] ?? string.Empty);
106 | }
107 | catch (Exception)
108 | {
109 | // ignored
110 | }
111 |
112 | return address;
113 | }
114 | }
115 |
116 | [JsonIgnore]
117 | public int Echo
118 | {
119 | get
120 | {
121 | var address = -1;
122 | try
123 | {
124 | address = int.Parse(Address?.Split('/')[1] ?? string.Empty);
125 | }
126 | catch (Exception)
127 | {
128 | // ignored
129 | }
130 |
131 | return address;
132 | }
133 | }
134 | }
--------------------------------------------------------------------------------
/ViewModels/EzoBase.cs:
--------------------------------------------------------------------------------
1 | using ReactiveUI;
2 | using ReactiveUI.Fody.Helpers;
3 | using System;
4 | using Newtonsoft.Json;
5 | using PoolControl.Hardware;
6 | using System.Reactive;
7 | using PoolControl.Helper;
8 |
9 | namespace PoolControl.ViewModels;
10 |
11 | ///
12 | /// This is the base class for all Ezo Products Data
13 | ///
14 | [JsonObject(MemberSerialization.OptIn)]
15 | public abstract class EzoBase : MeasurementModelBase
16 | {
17 | protected EzoBase()
18 | {
19 | OnFind = ReactiveCommand.Create(Find_Button_Clicked);
20 | OnCalibrated = ReactiveCommand.Create(Calibrated_Button_Clicked);
21 | OnClearCalibration = ReactiveCommand.Create(ClearCalibrated_Button_Clicked);
22 |
23 | // Change LED and publish Message
24 | this.WhenAnyValue(r => r.LedOn).Subscribe(_ => SwitchLedAndPublishMessage());
25 | this.WhenAnyValue(r => r.Voltage).Subscribe(voltage => { PublishMessageWithType(PoolControlHelper.GetPropertyName(() => Voltage), GetInterfaceFormatDecimalPoint(voltage), false); });
26 | }
27 |
28 | private void ClearCalibrated_Button_Clicked()
29 | {
30 | ((BaseEzoMeasurement)BaseMeasurement!).ClearCalibration();
31 | }
32 |
33 | private void Calibrated_Button_Clicked()
34 | {
35 | SensorsCalibrated = (int)((BaseEzoMeasurement)BaseMeasurement!).DeviceCalibrated().Result;
36 | }
37 |
38 | private void Find_Button_Clicked()
39 | {
40 | ((BaseEzoMeasurement)BaseMeasurement!).FindDevice();
41 | }
42 |
43 | public abstract void OnValueChange();
44 |
45 | public override void PublishMessageValue()
46 | {
47 | base.PublishMessageValue();
48 | OnValueChange();
49 | }
50 |
51 | protected void SwitchLedAndPublishMessage()
52 | {
53 | new BaseEzoMeasurement { ModelBase = this }.SwitchLedState(LedOn);
54 | PublishMessageWithType(PoolControlHelper.GetPropertyName(() => LedOn), LedOn ? "1" : "0", true);
55 | }
56 |
57 | public ReactiveCommand OnFind { get; }
58 |
59 | public ReactiveCommand OnCalibrated { get; }
60 |
61 | public ReactiveCommand OnClearCalibration { get; }
62 |
63 |
64 | [JsonIgnore]
65 | public Switch? Switch { get; set; }
66 |
67 | [JsonIgnore]
68 | public Switch? FilterPumpSwitch { get; set; }
69 |
70 | [Reactive]
71 | [JsonProperty]
72 | public bool LedOn { get; set; }
73 |
74 | [Reactive]
75 | [JsonProperty]
76 | public int SensorsCalibrated { get; set; }
77 |
78 | [Reactive]
79 | [JsonProperty]
80 | public double Voltage { get; set; }
81 | }
--------------------------------------------------------------------------------
/ViewModels/FilterPump.cs:
--------------------------------------------------------------------------------
1 | using ReactiveUI;
2 | using ReactiveUI.Fody.Helpers;
3 | using System;
4 | using Newtonsoft.Json;
5 | using PoolControl.Helper;
6 | using PoolControl.Time;
7 |
8 | namespace PoolControl.ViewModels;
9 |
10 | ///
11 | /// This class ist used to hold the data of filter pump, the pool pump
12 | ///
13 | [JsonObject(MemberSerialization.OptIn)]
14 | public class FilterPump : PumpModel
15 | {
16 | private const string DateTimeFormat = "yyyy-MM-ddTHH:mm:ss";
17 | private const double Diff = -20.0;
18 | private const double Factor = 6 * 60;
19 | private const double Max = 237.0 * 60;
20 | private double _oldTemperature;
21 |
22 | public FilterPump()
23 | {
24 | Logger = Log.Logger?.ForContext() ?? throw new ArgumentNullException(nameof(Logger));
25 |
26 | StartTriggerMorning = InitializeTrigger(StartTimerTriggered, Daily, GetTimerName(PoolControlHelper.GetPropertyName(() => StartTriggerMorning)));
27 | StartTriggerNoon = InitializeTrigger(StartTimerTriggered, Daily, GetTimerName(PoolControlHelper.GetPropertyName(() => StartTriggerNoon)));
28 | EndTriggerMorning = InitializeTrigger(EndTimerTriggered, Daily, GetTimerName(PoolControlHelper.GetPropertyName(() => EndTriggerMorning)));
29 | EndTriggerNoon = InitializeTrigger(EndTimerTriggered, Daily, GetTimerName(PoolControlHelper.GetPropertyName(() => EndTriggerNoon)));
30 | FilterOffTrigger = InitializeTrigger(EndTimerTriggered, Daily, GetTimerName(PoolControlHelper.GetPropertyName(() => FilterOffTrigger)));
31 |
32 | this.WhenAnyValue(x => x.StandardFilterRunTime).Subscribe(standardFilterTime => { PublishMessage(PoolControlHelper.GetPropertyName(() => StandardFilterRunTime), standardFilterTime.ToString(), true ); Recalculate(); });
33 | this.WhenAnyValue(x => x.StartMorning).Subscribe(startMorning => { PublishMessage(PoolControlHelper.GetPropertyName(() => StartMorning), startMorning.ToString(), true); Recalculate(); });
34 | this.WhenAnyValue(x => x.StartNoon).Subscribe(startAfternoon => { PublishMessage(PoolControlHelper.GetPropertyName(() => StartNoon), startAfternoon.ToString(), true); Recalculate(); });
35 | this.WhenAnyValue(x => x.FilterOff).Subscribe(filterOff => { PublishMessage(PoolControlHelper.GetPropertyName(() => FilterOff), filterOff.ToString(), true); Recalculate(); });
36 | this.WhenAnyValue(x => x.NextStart)
37 | .Subscribe(nextStart => PublishMessage(PoolControlHelper.GetPropertyName(() => NextStart),
38 | nextStart?.ToString(DateTimeFormat), true));
39 | this.WhenAnyValue(x => x.NextEnd).Subscribe(nextEnd => PublishMessage(PoolControlHelper.GetPropertyName(() => NextEnd), nextEnd?.ToString(DateTimeFormat), true));
40 | }
41 |
42 | public override void OnTemperatureChange(MeasurementArgs args)
43 | {
44 | if (args.BaseMeasurement is not { ModelBase: not null }) return;
45 | PoolTemperature = (Temperature)args.BaseMeasurement.ModelBase!;
46 | // Only Recalculate if TemperatureChange ist greater than 1 °C
47 | if (!(Math.Abs(PoolTemperature!.Value - _oldTemperature) > 1)) return;
48 | Logger.Debug("Pool temperature change > 1 °C --> Recalculating");
49 | _oldTemperature = PoolTemperature.Value;
50 | Recalculate();
51 | }
52 |
53 | public void Recalculate()
54 | {
55 | if (PoolTemperature != null)
56 | {
57 | var secondsToAdd = (int)Math.Min(Max, Math.Max(StandardFilterRunTime * 60, StandardFilterRunTime * 60 + Factor * (PoolTemperature.Value + Diff)));
58 | Logger.Debug("New secondsToAdd To FilterRunTime: {SecondsToAdd}", secondsToAdd);
59 | StartTrigger(FilterOffTrigger, FilterOff);
60 | StartTrigger(StartTriggerMorning, StartMorning);
61 | StartTrigger(StartTriggerNoon, StartNoon);
62 | StartTrigger(EndTriggerMorning, StartMorning.Add(new TimeSpan(0, 0, 0, secondsToAdd)));
63 | StartTrigger(EndTriggerNoon, StartNoon.Add(new TimeSpan(0, 0, 0, secondsToAdd)));
64 | }
65 |
66 | NextStart = StartTriggerMorning.TriggerTime.CompareTo(StartTriggerNoon.TriggerTime) < 1 ? StartTriggerMorning.TriggerTime : StartTriggerNoon.TriggerTime;
67 | NextEnd = EndTriggerMorning.TriggerTime.CompareTo(EndTriggerNoon.TriggerTime) < 1 ? EndTriggerMorning.TriggerTime : EndTriggerNoon.TriggerTime;
68 | }
69 |
70 | public override void RecalculateThings()
71 | {
72 | Recalculate();
73 | }
74 |
75 | protected override void OnTimerTicked(object? state)
76 | {
77 | // Nothing to do.
78 | }
79 |
80 | [JsonIgnore]
81 | protected Temperature? PoolTemperature { get; set; }
82 |
83 | [JsonIgnore]
84 | public TimeTrigger StartTriggerMorning { get; set; }
85 |
86 | [JsonIgnore]
87 | public TimeTrigger StartTriggerNoon { get; set; }
88 |
89 | [JsonIgnore]
90 | public TimeTrigger EndTriggerMorning { get; set; }
91 |
92 | [JsonIgnore]
93 | public TimeTrigger EndTriggerNoon { get; set; }
94 |
95 | [JsonIgnore]
96 | public TimeTrigger FilterOffTrigger { get; set; }
97 |
98 | [Reactive]
99 | [JsonProperty]
100 | public int StandardFilterRunTime { get; set; }
101 |
102 | [Reactive]
103 | [JsonProperty]
104 | public TimeSpan StartMorning { get; set; }
105 |
106 | [Reactive]
107 | [JsonProperty]
108 | public TimeSpan StartNoon { get; set; }
109 |
110 | [Reactive]
111 | [JsonProperty]
112 | public TimeSpan FilterOff { get; set; }
113 |
114 | [Reactive]
115 | [JsonProperty]
116 | public DateTime? NextStart { get; set; }
117 |
118 | [Reactive]
119 | [JsonProperty]
120 | public DateTime? NextEnd { get; set; }
121 | }
--------------------------------------------------------------------------------
/ViewModels/MeasurementModelBase.cs:
--------------------------------------------------------------------------------
1 | using Newtonsoft.Json;
2 | using ReactiveUI;
3 | using ReactiveUI.Fody.Helpers;
4 | using System;
5 | using System.Globalization;
6 | using PoolControl.Hardware;
7 | using PoolControl.Helper;
8 |
9 | namespace PoolControl.ViewModels;
10 |
11 | // Base mode for all measurements
12 | [JsonObject(MemberSerialization.OptIn)]
13 | public class MeasurementModelBase : ViewModelBase
14 | {
15 | private const string A = "PoolControl.Hardware.";
16 | private const string V = "Measurement";
17 |
18 | public MeasurementModelBase()
19 | {
20 | // Send InotifyChanged to View for Label, FullText and Temperature with Unit
21 | this.WhenAnyValue(ds => ds.Name, ds => ds.Value, ds => ds.ViewFormat, ds => ds.UnitSign, (_, temperature, viewFormat, unitSign) => $"{LocationName}: {temperature.ToString(viewFormat)} {unitSign}").ToPropertyEx(this, ds => ds.FullText, deferSubscription: true);
22 | this.WhenAnyValue(ds => ds.Name, ds => ds.Value, (_, _) => $"{LocationName}:").ToPropertyEx(this, ds => ds.Label, deferSubscription: true);
23 | this.WhenAnyValue(ds => ds.Value, ds => ds.ViewFormat, ds => ds.UnitSign, (value, viewFormat, unitSign) => $"{value.ToString(viewFormat)} {unitSign}").ToPropertyEx(this, ds => ds.ValueWithUnit, deferSubscription: true);
24 |
25 | // RestartReading and Publish
26 | this.WhenAnyValue(ds => ds.IntervalInSec).Subscribe(_ => this.RestartTimerAndPublishNewInterval());
27 |
28 | var name = GetType().Name;
29 | var t = Type.GetType(A + name + V);
30 |
31 | if (t != null) BaseMeasurement = (BaseMeasurement?)Activator.CreateInstance(t);
32 | if (BaseMeasurement != null) BaseMeasurement.ModelBase = this;
33 | }
34 |
35 | [JsonIgnore]
36 | public BaseMeasurement? BaseMeasurement { get; set; }
37 |
38 | [Reactive]
39 | [JsonProperty]
40 | public double Value { get; set; }
41 |
42 | [Reactive]
43 | [JsonProperty]
44 | public string? Address { get; set; }
45 |
46 | [Reactive]
47 | [JsonProperty]
48 | public string? UnitSign { get; set; }
49 |
50 | [Reactive]
51 | [JsonProperty]
52 | public string? ViewFormat { get; set; }
53 |
54 | [Reactive]
55 | [JsonProperty]
56 | public string? InterfaceFormat { get; set; }
57 |
58 | [Reactive]
59 | [JsonProperty]
60 | public DateTime TimeStamp { get; set; }
61 |
62 | [JsonIgnore]
63 | [ObservableAsProperty]
64 | public string? FullText { get; }
65 |
66 | [JsonIgnore]
67 | [ObservableAsProperty]
68 | public string? Label { get; }
69 |
70 | [JsonIgnore]
71 | [ObservableAsProperty]
72 | public string? ValueWithUnit { get; }
73 |
74 | [JsonIgnore]
75 | public string InterfaceFormatLocal => GetInterfaceFormatLocal(Value);
76 |
77 | [JsonIgnore]
78 | public string InterfaceFormatDecimalPoint => GetInterfaceFormatDecimalPoint(Value);
79 |
80 | public string GetInterfaceFormatDecimalPoint(double o)
81 | {
82 | return o.ToString(InterfaceFormat, new CultureInfo("en-US"));
83 | }
84 |
85 | public string GetInterfaceFormatLocal(double o)
86 | {
87 | return o.ToString(InterfaceFormat);
88 | }
89 |
90 | public virtual void PublishMessageValue()
91 | {
92 | PublishMessageWithType(PoolControlHelper.GetPropertyName(() => Value), InterfaceFormatDecimalPoint, false);
93 | PublishMessageWithType(PoolControlHelper.GetPropertyName(() => TimeStamp), TimeStamp.ToString("yyyy-MM-ddTHH:mm:ss"), false);
94 | }
95 |
96 | protected override void OnTimerTicked(object? state)
97 | {
98 | var mr = BaseMeasurement?.Measure();
99 |
100 | if (mr is { ReturnCode: (int)MeasurementResultCode.Success })
101 | {
102 | Logger.Information("{Message}", $"Measurement successful: Class: {GetType().Name} Value {Value} at {TimeStamp:yyyy-MM-dd HH:mm:ss-fff}, Status: {mr.StatusInfo}");
103 | MeasurementTaken?.Invoke(new MeasurementArgs { BaseMeasurement = BaseMeasurement });
104 | }
105 | else
106 | {
107 | if (mr != null) Logger.Error("Error: Code: {RetCode}, Status: {StatInf}", mr.ReturnCode, mr.StatusInfo);
108 | }
109 | }
110 |
111 | public event MeasurementHandler? MeasurementTaken;
112 | }
113 |
114 |
115 | public delegate void MeasurementHandler(MeasurementArgs args);
116 |
117 | public class MeasurementArgs : EventArgs
118 | {
119 | public BaseMeasurement? BaseMeasurement { get; init; }
120 | }
--------------------------------------------------------------------------------
/ViewModels/Ph.cs:
--------------------------------------------------------------------------------
1 | using ReactiveUI;
2 | using ReactiveUI.Fody.Helpers;
3 | using System;
4 | using Newtonsoft.Json;
5 | using System.Threading;
6 | using System.Reactive;
7 | using PoolControl.Hardware;
8 | using PoolControl.Helper;
9 |
10 | namespace PoolControl.ViewModels;
11 |
12 | ///
13 | /// Base class for pH measurement
14 | ///
15 | [JsonObject(MemberSerialization.OptIn)]
16 | public class Ph : EzoBase
17 | {
18 | public Ph()
19 | {
20 | Logger = Log.Logger?.ForContext() ?? throw new ArgumentNullException(nameof(Logger));
21 |
22 | OnMidCal = ReactiveCommand.Create(MidCal_Button_Clicked);
23 | OnLowCal = ReactiveCommand.Create(LowCal_Button_Clicked);
24 | OnHighCal = ReactiveCommand.Create(HighCal_Button_Clicked);
25 | OnGetSlope = ReactiveCommand.Create(GetSlope_Button_Clicked);
26 |
27 | // Publish changes via MQTT
28 | this.WhenAnyValue(p => p.MaxValue).Subscribe(value => PublishMessageWithType(PoolControlHelper.GetPropertyName(() => MaxValue), PoolControlHelper.format1Decimal(value), true));
29 | this.WhenAnyValue(p => p.AcidInjectionDuration).Subscribe(_ => RestartPhTimerAndPublishNewInterval());
30 | this.WhenAnyValue(p => p.AcidInjectionRecurringPeriod).Subscribe(_ => RestartPhTimerAndPublishNewInterval());
31 | }
32 |
33 | private void GetSlope_Button_Clicked()
34 | {
35 | Slope = ((PhMeasurement)BaseMeasurement!).Slope().StatusInfo;
36 | }
37 |
38 | private void MidCal_Button_Clicked()
39 | {
40 | ((PhMeasurement)BaseMeasurement!).MidCalibration(MidCal);
41 | }
42 |
43 | private void LowCal_Button_Clicked()
44 | {
45 | ((PhMeasurement)BaseMeasurement!).LowCalibration(LowCal);
46 | }
47 |
48 | private void HighCal_Button_Clicked()
49 | {
50 | ((PhMeasurement)BaseMeasurement!).HighCalibration(HighCal);
51 | }
52 |
53 | public void RestartPhTimerAndPublishNewInterval()
54 | {
55 | PhTimerOn = RestartTimer(PhTimerOn, CheckPh, PhInterval);
56 | PhTimerOff = RestartTimer(PhTimerOff, PhPumpOff, PhInterval + 1000 * AcidInjectionDuration, PhInterval);
57 |
58 | PublishMessageWithType(PoolControlHelper.GetPropertyName(() => AcidInjectionDuration), AcidInjectionDuration.ToString(), true);
59 | PublishMessageWithType(PoolControlHelper.GetPropertyName(() => AcidInjectionRecurringPeriod), AcidInjectionRecurringPeriod.ToString(), true);
60 | }
61 |
62 | private void PhPumpOff(object? state)
63 | {
64 | if (Switch == null) return;
65 | Switch.On = false;
66 | Logger.Information("{Name} Ein({On})", Switch.Name, Switch.On);
67 | }
68 |
69 | protected void CheckPh(object? state)
70 | {
71 | if(WinterMode)
72 | {
73 | Logger.Information("{Message}", $"WinterMode: {Switch?.Name} Ein({Switch?.On}) pH({Value}), MaxPh({MaxValue}) FilterPump({FilterPumpSwitch?.On})");
74 | return;
75 | }
76 |
77 | if (Switch != null)
78 | {
79 | if (FilterPumpSwitch!.On)
80 | {
81 | if (Value > MaxValue)
82 | {
83 | Switch.On = true;
84 | Logger.Information("{Message}", $"{Switch.Name} Ein({Switch.On}) pH({Value}) > MaxPh({MaxValue}) FilterPump({FilterPumpSwitch.On})");
85 | }
86 | else
87 | {
88 | Logger.Information("{Message}", $"{Switch.Name} Ein({Switch.On}) pH({Value}) < MaxPh({MaxValue}) FilterPump({FilterPumpSwitch.On})");
89 | }
90 | }
91 | else
92 | {
93 | if (Switch.On)
94 | {
95 | Switch.On = false;
96 | Logger.Information("{Message}", $"Ein({Switch.On}) FilterPump({FilterPumpSwitch.On})");
97 | }
98 | }
99 | }
100 | }
101 |
102 | public ReactiveCommand OnMidCal { get; }
103 | public ReactiveCommand OnLowCal { get; }
104 | public ReactiveCommand OnHighCal { get; }
105 | public ReactiveCommand OnGetSlope { get; }
106 |
107 | [JsonIgnore]
108 | protected Timer? PhTimerOn { get; private set; }
109 |
110 | [JsonIgnore]
111 | protected Timer? PhTimerOff { get; private set; }
112 |
113 | [JsonIgnore]
114 | public Temperature? PoolTemperature { get; set; }
115 |
116 | [Reactive]
117 | [JsonProperty]
118 | public double MidCal { get; set; }
119 |
120 | [Reactive]
121 | [JsonProperty]
122 | public double LowCal { get; set; }
123 |
124 | [Reactive]
125 | [JsonProperty]
126 | public double HighCal { get; set; }
127 |
128 | [Reactive]
129 | [JsonProperty]
130 | public double MaxValue { get; set; }
131 |
132 | [Reactive]
133 | [JsonProperty]
134 | public int AcidInjectionDuration { get; set; }
135 |
136 | [Reactive]
137 | [JsonProperty]
138 | public int AcidInjectionRecurringPeriod { get; set; }
139 |
140 |
141 | [Reactive]
142 | [JsonProperty]
143 | public string? Slope { get; set; }
144 |
145 | [JsonIgnore]
146 | public int PhInterval => AcidInjectionRecurringPeriod * 60 * 1000;
147 |
148 | public override void OnValueChange()
149 | {
150 | // Nothing to do
151 | }
152 | }
--------------------------------------------------------------------------------
/ViewModels/PoolData.cs:
--------------------------------------------------------------------------------
1 | using ReactiveUI;
2 | using ReactiveUI.Fody.Helpers;
3 | using System;
4 | using Newtonsoft.Json;
5 | using System.Collections.Generic;
6 | using System.Linq;
7 | using PoolControl.Hardware;
8 | using PoolControl.Helper;
9 |
10 | namespace PoolControl.ViewModels;
11 |
12 | ///
13 | /// All PoolData which will be loaded via json at the beginning
14 | ///
15 | [JsonObject(MemberSerialization.OptIn)]
16 | public class PoolData : ViewModelBase
17 | {
18 | public PoolData()
19 | {
20 | Logger = Log.Logger?.ForContext() ?? throw new ArgumentNullException(nameof(Logger));
21 | this.WhenAnyValue(d => d.WinterMode).Subscribe(_ => SetWinterMode());
22 | }
23 |
24 | private void SetWinterMode()
25 | {
26 | if (Temperatures is not null)
27 | {
28 | foreach (var temperature in Temperatures)
29 | {
30 | temperature.WinterMode = WinterMode;
31 | }
32 | }
33 |
34 | if (Switches is not null)
35 | {
36 | foreach (var sw in Switches)
37 | {
38 | sw.WinterMode = WinterMode;
39 | sw.On = false;
40 | }
41 | }
42 |
43 | if (FilterPump is not null)
44 | {
45 | FilterPump.WinterMode = WinterMode;
46 | }
47 |
48 | if (FilterPump is not null)
49 | {
50 | if (SolarHeater != null) SolarHeater.WinterMode = WinterMode;
51 | }
52 |
53 | if (Ph is not null)
54 | {
55 | Ph.WinterMode = WinterMode;
56 | }
57 |
58 | if (Redox is not null)
59 | {
60 | Redox.WinterMode = WinterMode;
61 | }
62 |
63 | if (Distance is not null)
64 | {
65 | Distance.WinterMode = WinterMode;
66 | }
67 | }
68 |
69 | [Reactive]
70 | public List? Temperatures { get; set; }
71 |
72 | [JsonProperty(nameof(Temperatures))]
73 | public Dictionary? TemperaturesDict { get; set; }
74 | public Dictionary? TemperaturesObj { get; set; }
75 |
76 | [Reactive]
77 | public List? Switches { get; set; }
78 |
79 | [JsonProperty(nameof(Switches))]
80 | public Dictionary? SwitchesDict { get; set; }
81 | public Dictionary? SwitchesObj { get; set; }
82 |
83 | [JsonProperty]
84 | public RelayConfig? RelayConfig { get; set; }
85 |
86 | [Reactive]
87 | [JsonProperty]
88 | public FilterPump? FilterPump { get; set; }
89 |
90 | [Reactive]
91 | [JsonProperty]
92 | public SolarHeater? SolarHeater { get; set; }
93 |
94 | [Reactive]
95 | [JsonProperty]
96 | public Ph? Ph { get; set; }
97 |
98 | [Reactive]
99 | [JsonProperty]
100 | public Redox? Redox { get; set; }
101 |
102 | [Reactive]
103 | [JsonProperty]
104 | public Distance? Distance { get; set; }
105 |
106 | public void OpenGpioEchoAndTrigger()
107 | {
108 | if (Distance == null) return;
109 | Gpio.Instance.OpenPinModeOutput(Distance.Trigger, true);
110 | Gpio.Instance.OpenPinModeInput(Distance.Echo, true);
111 | }
112 |
113 | public void CloseGpioEchoAndTrigger()
114 | {
115 | if (Distance == null) return;
116 | Gpio.Instance.Close(Distance.Trigger);
117 | Gpio.Instance.Close(Distance.Echo);
118 | }
119 |
120 | public void OpenGpioSwitches()
121 | {
122 | if (Switches == null) return;
123 | foreach (var sw in Switches.Where(_ => RelayConfig != null))
124 | {
125 | if (RelayConfig != null)
126 | Gpio.Instance.OpenPinModeOutput(RelayConfig.GetGpioForRelayNumber(sw.RelayNumber), sw.HighIsOn);
127 | }
128 | }
129 |
130 | public void CloseGpioSwitches()
131 | {
132 | if (Switches == null) return;
133 | foreach (var sw in Switches.Where(_ => RelayConfig != null))
134 | {
135 | if (RelayConfig != null) Gpio.Instance.Close(RelayConfig.GetGpioForRelayNumber(sw.RelayNumber));
136 | }
137 | }
138 |
139 | public void GpioSwitchesOff()
140 | {
141 | if (Switches == null) return;
142 | foreach (var sw in Switches.Where(_ => RelayConfig != null))
143 | {
144 | if (RelayConfig != null)
145 | Gpio.Instance.Off(RelayConfig.GetGpioForRelayNumber(sw.RelayNumber), sw.HighIsOn);
146 | }
147 | }
148 |
149 | protected override void OnTimerTicked(object? state)
150 | {
151 | // Nothing to do
152 | }
153 | }
--------------------------------------------------------------------------------
/ViewModels/PumpModel.cs:
--------------------------------------------------------------------------------
1 | using Newtonsoft.Json;
2 | using PoolControl.Time;
3 | using System;
4 |
5 | namespace PoolControl.ViewModels;
6 |
7 | ///
8 | /// Base Data for pumps
9 | ///
10 | [JsonObject(MemberSerialization.OptIn)]
11 | public abstract class PumpModel : ViewModelBase
12 | {
13 | protected TimeSpan Daily = new(24, 0, 0);
14 |
15 | [JsonIgnore]
16 | public Switch? Switch { get; set; }
17 |
18 | public abstract void OnTemperatureChange(MeasurementArgs args);
19 |
20 | public abstract void RecalculateThings();
21 |
22 | public void StartTimerTriggered()
23 | {
24 | Logger.Debug("Switching {On} on", GetType().Name);
25 |
26 | if (WinterMode)
27 | {
28 | Logger.Information("WinterMode! Nothing to do!");
29 | return;
30 | }
31 |
32 | if (Switch != null)
33 | {
34 | Switch.On = true;
35 | }
36 | else
37 | {
38 | Logger.Debug("{Name} is null and could not be turned on", GetType().Name);
39 | }
40 | RecalculateThings();
41 | }
42 |
43 | public void EndTimerTriggered()
44 | {
45 | Logger.Debug("Switching {Name} off", GetType().Name);
46 |
47 | if (WinterMode)
48 | {
49 | Logger.Information("WinterMode! Nothing to do!");
50 | }
51 |
52 | if (Switch != null)
53 | {
54 | Switch.On = false;
55 | }
56 | else
57 | {
58 | Logger.Debug("{Name} is null and could not be turned off", GetType().Name);
59 | }
60 | RecalculateThings();
61 | }
62 |
63 | protected static TimeTrigger InitializeTrigger(Action? action, TimeSpan period, string name)
64 | {
65 | var trigger = new TimeTrigger
66 | {
67 | Name = name,
68 | Period = period
69 | };
70 | trigger.OnTimeTriggered += action;
71 |
72 | return trigger;
73 | }
74 |
75 | protected static void StartTrigger(TimeTrigger trigger, TimeSpan startTime)
76 | {
77 | trigger.StartTime = startTime;
78 | trigger.InitiateTimer();
79 | }
80 |
81 | protected string GetTimerName(string name)
82 | {
83 | return $"{GetType().Name}.{name}";
84 | }
85 | }
--------------------------------------------------------------------------------
/ViewModels/Redox.cs:
--------------------------------------------------------------------------------
1 | using ReactiveUI;
2 | using ReactiveUI.Fody.Helpers;
3 | using System;
4 | using Newtonsoft.Json;
5 | using System.Reactive;
6 | using PoolControl.Hardware;
7 | using PoolControl.Helper;
8 |
9 | namespace PoolControl.ViewModels;
10 |
11 | ///
12 | /// Data for Redox Measurement for salting engine
13 | ///
14 | [JsonObject(MemberSerialization.OptIn)]
15 | public class Redox : EzoBase
16 | {
17 | public Redox()
18 | {
19 | Logger = Log.Logger?.ForContext() ?? throw new ArgumentNullException(nameof(Logger));
20 |
21 | OnCal = ReactiveCommand.Create(Cal_Button_Clicked);
22 |
23 | // Publish changes via MQTT
24 | this.WhenAnyValue(r => r.Off).Subscribe(ein => { PublishMessageWithType(PoolControlHelper.GetPropertyName(() => Off), ein.ToString(), true); OnValueChange(); });
25 | this.WhenAnyValue(r => r.On).Subscribe(aus => { PublishMessageWithType(PoolControlHelper.GetPropertyName(() => On), aus.ToString(), true); OnValueChange(); });
26 | }
27 |
28 | private void Cal_Button_Clicked()
29 | {
30 | ((RedoxMeasurement)BaseMeasurement!).Calibrate(Cal);
31 | }
32 |
33 | public ReactiveCommand OnCal { get; }
34 |
35 | [Reactive]
36 | [JsonProperty]
37 | public int On { get; set; }
38 |
39 | [Reactive]
40 | [JsonProperty]
41 | public int Off { get; set; }
42 |
43 | [Reactive]
44 | [JsonProperty]
45 | public int Cal { get; set; }
46 |
47 | public override void OnValueChange()
48 | {
49 | if(WinterMode)
50 | {
51 | Logger.Information("{Message}", $"WinterMode: Ein({Switch?.On}) Redox({Value:#0})");
52 | return;
53 | }
54 |
55 | if (Switch != null)
56 | {
57 | if (FilterPumpSwitch!.On && Value < On)
58 | {
59 | Switch.On = true;
60 | Logger.Information("{Message}", $"Ein({Switch.On}) Redox({Value:#0}) > Ein({On}) FilterPumP({FilterPumpSwitch.On})");
61 | }
62 | else if (!FilterPumpSwitch.On || Value > Off)
63 | {
64 | Switch.On = false;
65 | Logger.Information("{Message}", $"Ein({Switch.On}) Redox({Value:#0}) > Aus({Off}) FilterPumP({FilterPumpSwitch.On})");
66 | }
67 | }
68 | }
69 | }
--------------------------------------------------------------------------------
/ViewModels/RelayConfig.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using Newtonsoft.Json;
3 |
4 | namespace PoolControl.ViewModels;
5 |
6 | ///
7 | /// Configuration for relay to pump attachment
8 | ///
9 | [JsonObject(MemberSerialization.OptIn)]
10 | public class RelayConfig
11 | {
12 | private static RelayConfig? _instance;
13 | private static readonly object Padlock = new object();
14 |
15 | public static RelayConfig? Instance
16 | {
17 | get
18 | {
19 | lock (Padlock)
20 | {
21 | return _instance ??= new RelayConfig();
22 | }
23 | }
24 |
25 | set => _instance = value;
26 | }
27 |
28 | private RelayConfig() { }
29 |
30 | [JsonProperty("RelayToLogicLevelConverter")]
31 | public Dictionary? RelayToLogicLevelConverterDict;
32 |
33 | [JsonProperty("LogicLevelConverterToGpio")]
34 | public Dictionary? LogicLevelConverterToGpioDict;
35 |
36 | public int GetGpioForRelayNumber(int relayNumber)
37 | {
38 | if (RelayToLogicLevelConverterDict != null && RelayToLogicLevelConverterDict.TryGetValue(relayNumber, out var key))
39 | {
40 | if (LogicLevelConverterToGpioDict != null && LogicLevelConverterToGpioDict.TryGetValue(key, out var number))
41 | {
42 | return number;
43 | }
44 | }
45 |
46 | return -1;
47 | }
48 | }
--------------------------------------------------------------------------------
/ViewModels/SolarHeater.cs:
--------------------------------------------------------------------------------
1 | using ReactiveUI;
2 | using ReactiveUI.Fody.Helpers;
3 | using System;
4 | using Newtonsoft.Json;
5 | using PoolControl.Helper;
6 | using PoolControl.Time;
7 |
8 | namespace PoolControl.ViewModels;
9 |
10 | ///
11 | /// Data for solar heater
12 | ///
13 | [JsonObject(MemberSerialization.OptIn)]
14 | public class SolarHeater : PumpModel
15 | {
16 | public SolarHeater()
17 | {
18 | Logger = Log.Logger?.ForContext() ?? throw new ArgumentNullException(nameof(Logger));
19 | TurnOnTrigger = InitializeTrigger(StartTimerTriggered, Daily, GetTimerName(PoolControlHelper.GetPropertyName(() => TurnOnTrigger)));
20 | TurnOffTrigger = InitializeTrigger(EndTimerTriggered, Daily, GetTimerName(PoolControlHelper.GetPropertyName(() => TurnOffTrigger)));
21 |
22 | this.WhenAnyValue(x => x.TurnOnDiff).Subscribe(switchOnDiff => PublishMessageWithType(PoolControlHelper.GetPropertyName(() => TurnOnDiff), PoolControlHelper.format1Decimal(switchOnDiff), true));
23 | this.WhenAnyValue(x => x.TurnOffDiff).Subscribe(switchOffDiff => PublishMessageWithType(PoolControlHelper.GetPropertyName(() => TurnOffDiff), PoolControlHelper.format1Decimal(switchOffDiff), true));
24 | this.WhenAnyValue(x => x.MaxPoolTemp).Subscribe(maxPoolTemp => PublishMessageWithType(PoolControlHelper.GetPropertyName(() => MaxPoolTemp), PoolControlHelper.format1Decimal(maxPoolTemp), true));
25 | this.WhenAnyValue(x => x.SolarHeaterCleaningDuration).Subscribe(cleaningDuration => { PublishMessageWithType(PoolControlHelper.GetPropertyName(() => SolarHeaterCleaningDuration), cleaningDuration.ToString(), true); RecalculateSolarHeatingCleaning(); });
26 | this.WhenAnyValue(x => x.SolarHeaterCleaningTime).Subscribe(cleaningPoint => { PublishMessageWithType(PoolControlHelper.GetPropertyName(() => SolarHeaterCleaningTime), cleaningPoint.ToString(), true); RecalculateSolarHeatingCleaning(); });
27 | }
28 |
29 | public override void OnTemperatureChange(MeasurementArgs args)
30 | {
31 | if (args.BaseMeasurement is { ModelBase: not null })
32 | {
33 | var temp = (Temperature)args.BaseMeasurement.ModelBase;
34 | switch (temp?.Key)
35 | {
36 | case "SolarPreRun":
37 | SolarPreLoopTemperature = temp;
38 | break;
39 | case "SolarHeater":
40 | SolarHeaterTemperature = temp;
41 | break;
42 | case "Pool":
43 | SolarPoolTemperature = temp;
44 | break;
45 | }
46 | }
47 |
48 | if (SolarPreLoopTemperature == null || SolarHeaterTemperature == null || SolarPoolTemperature == null)
49 | {
50 | Logger.Debug($"SolarPreLoopTemperature or SolarHeaterTemperature or SolarPoolTemperature is null");
51 | return;
52 | }
53 |
54 | Logger.Debug($"SolarPreLoopTemperature: {SolarPreLoopTemperature.Value} SolarHeaterTemperature: {SolarHeaterTemperature.Value} SolarPoolTemperature: {SolarPoolTemperature.Value}");
55 |
56 | DateTime now = DateTime.Now;
57 | if (now < NextEnd && now > NextEnd - new TimeSpan(0, 0, SolarHeaterCleaningDuration))
58 | {
59 | Logger.Information("Do nothing because cleaning of solar heater is running");
60 | return;
61 | }
62 |
63 | Logger.Information(SolarPoolTemperature.Value > SolarPreLoopTemperature.Value
64 | ? "Using SolarPreLoop Temperature vor SolarHeating!"
65 | : "Using Pool Temperature vor SolarHeating!");
66 |
67 | var baseTemperature = Math.Min(SolarPreLoopTemperature.Value, SolarPoolTemperature.Value);
68 |
69 | if(WinterMode)
70 | {
71 | Switch!.On = false;
72 |
73 | Logger.Information($"Wintermode!!! {Switch.Name} On({Switch.On}) SolarHeater({SolarHeaterTemperature.Value:#0.0}) > Pool({baseTemperature:#0.0}) + Aus({TurnOffDiff:#0.0}) = Sum({baseTemperature:#0.0}) Max({MaxPoolTemp:#0.0})");
74 | }
75 | else if (SolarHeaterTemperature.Value > baseTemperature + TurnOnDiff)
76 | {
77 | Switch!.On = baseTemperature < MaxPoolTemp;
78 | Logger.Information($"{Switch.Name} On({Switch.On}) SolarHeater({SolarHeaterTemperature.Value:#0.0}) > Pool({baseTemperature:#0.0}) + Ein({TurnOnDiff:#0.0}) = Sum({baseTemperature:#0.0}) Max({MaxPoolTemp:#0.0})");
79 | }
80 | else if (SolarHeaterTemperature.Value < baseTemperature + TurnOffDiff)
81 | {
82 | Switch!.On = false;
83 |
84 | Logger.Information($"{Switch.Name} On({Switch.On}) SolarHeater({SolarHeaterTemperature.Value:#0.0}) > Pool({baseTemperature:#0.0}) + Aus({TurnOffDiff:#0.0}) = Sum({baseTemperature:#0.0}) Max({MaxPoolTemp:#0.0})");
85 | }
86 | }
87 |
88 | public void RecalculateSolarHeatingCleaning()
89 | {
90 | StartTrigger(TurnOnTrigger, SolarHeaterCleaningTime);
91 | StartTrigger(TurnOffTrigger, SolarHeaterCleaningTime.Add(new TimeSpan(0, 0, SolarHeaterCleaningDuration)));
92 | NextStart = TurnOnTrigger.TriggerTime;
93 | NextEnd = TurnOffTrigger.TriggerTime;
94 | }
95 |
96 | public override void RecalculateThings()
97 | {
98 | RecalculateSolarHeatingCleaning();
99 | }
100 |
101 | protected override void OnTimerTicked(object? state)
102 | {
103 | // Nothing has to be done
104 | }
105 |
106 | [JsonIgnore]
107 | protected Temperature? SolarPoolTemperature { get; set; }
108 |
109 | [JsonIgnore]
110 | protected Temperature? SolarPreLoopTemperature { get; set; }
111 |
112 | [JsonIgnore]
113 | protected Temperature? SolarHeaterTemperature { get; set; }
114 |
115 | [JsonIgnore]
116 | public TimeTrigger TurnOnTrigger { get; set; }
117 |
118 | [JsonIgnore]
119 | public TimeTrigger TurnOffTrigger { get; set; }
120 |
121 | [Reactive]
122 | [JsonProperty]
123 | public TimeSpan SolarHeaterCleaningTime { get; set; }
124 |
125 | [Reactive]
126 | [JsonProperty]
127 | public double TurnOnDiff { get; set; }
128 |
129 | [Reactive]
130 | [JsonProperty]
131 | public double TurnOffDiff { get; set; }
132 |
133 | [Reactive]
134 | [JsonProperty]
135 | public double MaxPoolTemp { get; set; }
136 |
137 | [Reactive]
138 | [JsonProperty]
139 | public int SolarHeaterCleaningDuration { get; set; }
140 |
141 | [Reactive]
142 | [JsonProperty]
143 | public DateTime NextStart { get; set; }
144 |
145 | [Reactive]
146 | [JsonProperty]
147 | public DateTime NextEnd { get; set; }
148 | }
--------------------------------------------------------------------------------
/ViewModels/Switch.cs:
--------------------------------------------------------------------------------
1 | using Newtonsoft.Json;
2 | using ReactiveUI;
3 | using ReactiveUI.Fody.Helpers;
4 | using System;
5 | using PoolControl.Hardware;
6 | using PoolControl.Helper;
7 |
8 |
9 | namespace PoolControl.ViewModels;
10 |
11 | ///
12 | /// Data for all Switches
13 | ///
14 | [JsonObject(MemberSerialization.OptOut)]
15 | public class Switch : ViewModelBase
16 | {
17 | public Switch()
18 | {
19 | Logger = Log.Logger?.ForContext() ?? throw new ArgumentNullException(nameof(Logger));
20 |
21 | this.WhenAnyValue(s => s.On).Subscribe(_ => SwitchRelay());
22 | }
23 |
24 | [Reactive]
25 | public string? Key { get; set; }
26 |
27 | [Reactive]
28 | public int RelayNumber { get; set; }
29 |
30 | [Reactive]
31 | public bool HighIsOn { get; set; }
32 |
33 | [Reactive]
34 | public bool On { get; set; }
35 |
36 | public void SwitchRelay()
37 | {
38 | Logger.Debug("Switch {Key} {On} changed", Key, On);
39 | if (RelayConfig.Instance != null)
40 | Gpio.Instance.DoSwitch(RelayConfig.Instance.GetGpioForRelayNumber(RelayNumber), On, HighIsOn);
41 | PublishMessage($"Switches/{Key}/On", On ? "1" : "0", 2, true, !string.IsNullOrEmpty(Key));
42 | }
43 |
44 | protected override void OnTimerTicked(object? state)
45 | {
46 | // Nothing has to be done
47 | }
48 | }
--------------------------------------------------------------------------------
/ViewModels/Temperature.cs:
--------------------------------------------------------------------------------
1 | using Newtonsoft.Json;
2 | using ReactiveUI.Fody.Helpers;
3 | using System;
4 | using PoolControl.Helper;
5 |
6 | namespace PoolControl.ViewModels;
7 |
8 | ///
9 | /// Data fpr all temperatures
10 | ///
11 | [JsonObject(MemberSerialization.OptIn)]
12 | public class Temperature : MeasurementModelBase
13 | {
14 | public Temperature()
15 | {
16 | Logger = Log.Logger?.ForContext() ?? throw new ArgumentNullException(nameof(Logger));
17 | }
18 |
19 | public override void PublishMessageValue()
20 | {
21 | PublishMessage($"Temperatures/{Key}/Temperature", InterfaceFormatDecimalPoint, !String.IsNullOrEmpty(Key), false);
22 | PublishMessage($"Temperatures/{Key}/TimeStamp", TimeStamp.ToString("yyyy-MM-ddTHH:mm:ss"), !String.IsNullOrEmpty(Key), false);
23 | }
24 |
25 | [Reactive]
26 | [JsonProperty]
27 | public string? Key { get; set; }
28 | }
--------------------------------------------------------------------------------
/ViewModels/ViewModelBase.cs:
--------------------------------------------------------------------------------
1 | using ReactiveUI;
2 | using ReactiveUI.Fody.Helpers;
3 | using Newtonsoft.Json;
4 | using System;
5 | using PoolControl.Communication;
6 | using Serilog;
7 | using System.Reflection;
8 | using System.Collections;
9 | using System.Threading;
10 | using PoolControl.Helper;
11 | using Log = PoolControl.Helper.Log;
12 | using PoolControl.Views;
13 | using Avalonia.Threading;
14 | using System.Threading.Tasks;
15 |
16 | namespace PoolControl.ViewModels;
17 |
18 | [JsonObject(MemberSerialization.OptIn)]
19 | public abstract class ViewModelBase : ReactiveObject, IDisposable
20 | {
21 | private bool _disposedValue;
22 |
23 | protected ILogger Logger { get; set; }
24 |
25 | protected ViewModelBase()
26 | {
27 | Logger = Log.Logger?.ForContext() ?? throw new ArgumentNullException(nameof(Logger));
28 | }
29 |
30 | private static void ShowNotification(string title, string message)
31 | {
32 | Dispatcher.UIThread.Post(() => { AsyncShowNotification(title, message); });
33 | }
34 |
35 | private static void AsyncShowNotification(string title, string message)
36 | {
37 | (App.MainWindow as MainWindow)?.ShowNotification(title, message);
38 | }
39 |
40 | [JsonIgnore]
41 | protected Timer? Timer { get; private set; }
42 |
43 | [Reactive]
44 | [JsonProperty]
45 | public int IntervalInSec { get; set; }
46 |
47 | [JsonIgnore]
48 | public int Interval => IntervalInSec * 1000;
49 |
50 | [Reactive]
51 | [JsonProperty]
52 | public string Name { get; set; } = "Name";
53 |
54 | [Reactive]
55 | [JsonProperty]
56 | public bool WinterMode { get; set; }
57 |
58 |
59 | [JsonIgnore]
60 | public string LocationName
61 | {
62 | get
63 | {
64 | var ret = "Nix";
65 | try
66 | {
67 | ret = (string)typeof(Resource).GetProperty(Name)?.GetValue(null)!;
68 | }
69 | catch (Exception)
70 | {
71 | // ignored
72 | }
73 |
74 | return ret;
75 | }
76 | }
77 |
78 | public void RestartTimerAndPublishNewInterval()
79 | {
80 | Timer = RestartTimer(Timer, OnTimerTicked, Interval);
81 |
82 | PublishMessageWithType(PoolControlHelper.GetPropertyName(() => IntervalInSec), IntervalInSec.ToString(), false);
83 | }
84 |
85 | protected abstract void OnTimerTicked(object? state);
86 |
87 | protected Timer? RestartTimer(Timer? timer, TimerCallback callback, int interval)
88 | {
89 | return RestartTimer(timer, callback, interval, interval);
90 | }
91 |
92 | protected Timer? RestartTimer(Timer? timer, TimerCallback callback, int dueTime, int interval)
93 | {
94 | if (timer == null && interval > 0)
95 | {
96 | var autoEvent = new AutoResetEvent(false);
97 | timer = new Timer(callback, autoEvent, dueTime, interval);
98 | }
99 | else
100 | {
101 | if (interval <= 0)
102 | {
103 | if (Timer != null)
104 | {
105 | timer?.Dispose();
106 | timer = null;
107 | Logger.Debug("Timer deleted");
108 | }
109 | }
110 | else
111 | {
112 | if (timer != null && timer.Change(dueTime, interval))
113 | {
114 | Logger.Debug("Timer set: dueTime {DueTime} interval {Interval}", dueTime, interval);
115 | }
116 | else
117 | {
118 | Logger.Debug("Timer set: dueTime {DueTime} interval {Interval} error", dueTime, interval);
119 | }
120 | }
121 | }
122 |
123 | return timer;
124 | }
125 |
126 | public void PublishMessage(string? propertyName, string? value, int qos, bool retain, bool reallySend, bool notifyOnReallySend = false)
127 | {
128 | if(!reallySend)
129 | {
130 | Logger.Warning("Property {PropertyName} should not be sent for value {Value}. MQTT Message will not be published", propertyName, value);
131 | return;
132 | }
133 | if (propertyName == null)
134 | {
135 | Logger.Warning("Property is null for value {Value}. MQTT Message will not be published", value);
136 | }
137 | else
138 | {
139 | _ = PoolMqttClient.Instance.publishMessage($"{PoolControlConfig.Instance.Settings?.BaseTopic.State}{propertyName}", value, qos, retain);
140 | if (notifyOnReallySend)
141 | {
142 | ShowNotification($"MQTT: {PoolControlConfig.Instance.Settings?.BaseTopic.State}", $"Property: {propertyName} Payload: {value}");
143 | }
144 | }
145 | }
146 |
147 | public void PublishMessage(string propertyName, string? value, int qos, bool retain, bool notifyOnReallySend = false)
148 | {
149 | PublishMessage(propertyName, value, qos, retain, true, notifyOnReallySend);
150 | }
151 |
152 | public void PublishMessage(string propertyName, string? value, bool notifyOnReallySend = false)
153 | {
154 | PublishMessage(propertyName, value, true, notifyOnReallySend);
155 | }
156 |
157 | public void PublishMessage(string propertyName, string? value, bool reallySend, bool notifyOnReallySend = false)
158 | {
159 | PublishMessage(propertyName, value, 0, false, reallySend, notifyOnReallySend);
160 | }
161 |
162 | public void PublishMessageWithType(string propertyName, string? value, bool notifyOnReallySend = false)
163 | {
164 | PublishMessage($"{this.GetType().Name}/{propertyName}", value, true, notifyOnReallySend);
165 | }
166 |
167 | protected virtual void Dispose(bool disposing)
168 | {
169 | if (_disposedValue) return;
170 | if (disposing)
171 | {
172 | // Dispose stuff
173 | }
174 |
175 | foreach (PropertyInfo pi in GetType().GetProperties())
176 | {
177 | if(pi.GetValue(this) is IEnumerable enumerable)
178 | {
179 | foreach(var item in enumerable)
180 | {
181 | if(item is IDisposable dp)
182 | {
183 | dp.Dispose();
184 | }
185 | }
186 | }
187 |
188 | if (pi.GetValue(this) is IDisposable disposable)
189 | {
190 | disposable.Dispose();
191 | }
192 | }
193 |
194 | _disposedValue = true;
195 | }
196 |
197 | public void Dispose()
198 | {
199 | // Dispose
200 | Dispose(disposing: true);
201 | GC.SuppressFinalize(this);
202 | }
203 | }
--------------------------------------------------------------------------------
/Views/MainView.axaml:
--------------------------------------------------------------------------------
1 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/Views/MainView.axaml.cs:
--------------------------------------------------------------------------------
1 | using Avalonia.Controls;
2 | using Avalonia.Markup.Xaml;
3 |
4 | namespace PoolControl.Views;
5 |
6 | public partial class MainView : UserControl
7 | {
8 | public MainView()
9 | {
10 | InitializeComponent();
11 | }
12 | }
--------------------------------------------------------------------------------
/Views/MainWindow.axaml:
--------------------------------------------------------------------------------
1 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/Views/MainWindow.axaml.cs:
--------------------------------------------------------------------------------
1 | using Avalonia.Controls;
2 | using Avalonia.Controls.Chrome;
3 | using Avalonia.Controls.Notifications;
4 | using Avalonia.Interactivity;
5 | using PoolControl.Helper;
6 |
7 | namespace PoolControl.Views;
8 |
9 | public partial class MainWindow : Window
10 | {
11 | private WindowNotificationManager? notificationManager;
12 | public MainWindow()
13 | {
14 | InitializeComponent();
15 | Loaded += MainWindow_Loaded;
16 | }
17 |
18 | private void MainWindow_Loaded(object? sender, RoutedEventArgs e)
19 | {
20 | notificationManager = new WindowNotificationManager(this)
21 | {
22 | Position = NotificationPosition.BottomRight,
23 | };
24 | }
25 |
26 | public void ShowNotification(string title, string message)
27 | {
28 | var notif = new Notification(title, message, NotificationType.Success, new (0, 0, 3));
29 | notificationManager?.Show(notif);
30 | }
31 | }
--------------------------------------------------------------------------------
/appsettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "Settings": {
3 | "PersistenceFile": "poolcontrolviewmodel.json",
4 | "PersistenceSaveIntervalInSec": 60,
5 | "BaseTopic": {
6 | "Command": "PoolControl/cmd/",
7 | "State": "PoolControl/state/"
8 | },
9 | "LWT": {
10 | "ConnectMessage": "Connected",
11 | "DisconnectMessage": "Disconnected",
12 | "Topic": "PoolControlControl/LWT"
13 | },
14 | "MQTT": {
15 | "Password": "letsdoit",
16 | "Port": 1883,
17 | "Server": "192.168.39.104",
18 | "User": "openhabian"
19 | }
20 | },
21 |
22 | "Serilog": {
23 | "Using": [ "Serilog.Sinks.File","Serilog.Sinks.Console","Serilog.Sinks.Syslog" ],
24 | "MinimumLevel": {
25 | "Default": "Debug",
26 | "Override": {
27 | "Microsoft": "Debug",
28 | "System": "Debug"
29 | }
30 | },
31 | "Enrich": [ "FromLogContext", "WithMachineName", "WithProcessId", "WithThreadId" ],
32 | "WriteTo": [
33 | {
34 | "Name": "Console",
35 | "Args": {
36 | "outputTemplate": "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} {Level:u3}][{ProcessId}][{ThreadId}] {Message:lj} {NewLine}{Exception}"
37 | }
38 | },
39 | {
40 | "Name": "File",
41 | "Args": {
42 | "path": "log.txt",
43 | "outputTemplate": "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} {Level:u3}][{ProcessId}][{ThreadId}] {Message:lj} {NewLine}{Exception}",
44 | "rollOnFileSizeLimit": true,
45 | "fileSizeLimitBytes": 419430400,
46 | "retainedFileCountLimit": 10,
47 | "rollingInterval": "Day"
48 | }
49 | },
50 | {
51 | "Name": "UdpSyslog",
52 | "Args": {
53 | "outputTemplate": "[{Timestamp:fff}] [{Level:u3}][{ProcessId}][{ThreadId}] {Message:lj} {Exception}",
54 | "host": "192.168.39.104",
55 | "port": 514,
56 | "format": "RFC3164",
57 | "secureProtocols": "SecureProtocols.None"
58 | }
59 | }
60 | ]
61 | }
62 | }
--------------------------------------------------------------------------------
/poolcontrolviewmodel.json:
--------------------------------------------------------------------------------
1 | {
2 | "Temperatures": {
3 | "Pool": {
4 | "Key": "Pool",
5 | "Value": 25.9,
6 | "Address": "28-0114536eebaa",
7 | "UnitSign": "°C",
8 | "ViewFormat": "#0.00",
9 | "InterfaceFormat": "#0.00",
10 | "TimeStamp": "2022-05-12T11:18:07.7582361+02:00",
11 | "IntervalInSec": 10,
12 | "Name": "Pool"
13 | },
14 | "SolarPreRun": {
15 | "Key": "SolarPreRun",
16 | "Value": 25.9,
17 | "Address": "28-041670557dff",
18 | "UnitSign": "°C",
19 | "ViewFormat": "#0.00",
20 | "InterfaceFormat": "#0.00",
21 | "TimeStamp": "2022-05-12T11:18:07.758184+02:00",
22 | "IntervalInSec": 10,
23 | "Name": "SolarPreRun"
24 | },
25 | "SolarHeater": {
26 | "Key": "SolarHeater",
27 | "Value": 33.1,
28 | "Address": "28-0315a43260ff",
29 | "UnitSign": "°C",
30 | "ViewFormat": "#0.00",
31 | "InterfaceFormat": "#0.00",
32 | "TimeStamp": "2022-05-12T11:18:07.7582103+02:00",
33 | "IntervalInSec": 10,
34 | "Name": "SolarHeater"
35 | },
36 | "Technikraum": {
37 | "Key": "Technikraum",
38 | "Value": 25.9,
39 | "Address": "28-0317609128ff",
40 | "UnitSign": "°C",
41 | "ViewFormat": "#0.00",
42 | "InterfaceFormat": "#0.00",
43 | "TimeStamp": "2022-05-12T11:17:27.7572016+02:00",
44 | "IntervalInSec": 60,
45 | "Name": "TechnicRoom"
46 | },
47 | "FrostChecker": {
48 | "Key": "FrostChecker",
49 | "Value": 25.9,
50 | "Address": "28-0416705896ff",
51 | "UnitSign": "°C",
52 | "ViewFormat": "#0.00",
53 | "InterfaceFormat": "#0.00",
54 | "TimeStamp": "2022-05-12T11:17:27.7571765+02:00",
55 | "IntervalInSec": 60,
56 | "Name": "FrostChecker"
57 | }
58 | },
59 | "Switches": {
60 | "FilterPump": {
61 | "Key": "FilterPump",
62 | "RelayNumber": 8,
63 | "HighIsOn": false,
64 | "On": false,
65 | "Name": "FilterPump"
66 | },
67 | "SolarHeater": {
68 | "Key": "SolarHeater",
69 | "RelayNumber": 7,
70 | "HighIsOn": false,
71 | "On": true,
72 | "Name": "SolarHeater"
73 | },
74 | "Ph": {
75 | "Key": "Ph",
76 | "RelayNumber": 6,
77 | "HighIsOn": false,
78 | "On": false,
79 | "Name": "PhPump"
80 | },
81 | "Redox": {
82 | "Key": "Redox",
83 | "RelayNumber": 5,
84 | "HighIsOn": false,
85 | "On": false,
86 | "Name": "RedoxSwitch"
87 | },
88 | "PoolLight": {
89 | "Key": "PoolLight",
90 | "RelayNumber": 4,
91 | "HighIsOn": false,
92 | "On": false,
93 | "Name": "PoolLight"
94 | },
95 | "Three": {
96 | "Key": "Three",
97 | "RelayNumber": 3,
98 | "HighIsOn": false,
99 | "On": false,
100 | "Name": "Three"
101 | },
102 | "Two": {
103 | "Key": "Two",
104 | "RelayNumber": 2,
105 | "HighIsOn": false,
106 | "On": false,
107 | "Name": "Two"
108 | },
109 | "One": {
110 | "Key": "One",
111 | "RelayNumber": 1,
112 | "HighIsOn": false,
113 | "On": false,
114 | "Name": "One"
115 | }
116 | },
117 | "RelayConfig": {
118 | "RelayToLogicLevelConverter": {
119 | "1": 1,
120 | "2": 2,
121 | "3": 3,
122 | "4": 4,
123 | "5": 5,
124 | "6": 6,
125 | "7": 7,
126 | "8": 8
127 | },
128 | "LogicLevelConverterToGpio": {
129 | "1": 19,
130 | "2": 13,
131 | "3": 5,
132 | "4": 22,
133 | "5": 27,
134 | "6": 23,
135 | "7": 20,
136 | "8": 21
137 | }
138 | },
139 | "FilterPump": {
140 | "StandardFilterRunTime": 180,
141 | "StartMorning": "08:00:00",
142 | "StartNoon": "14:00:00",
143 | "FilterOff": "20:00:00",
144 | "NextStart": "2022-05-12T14:00:00+02:00",
145 | "NextEnd": "2022-05-12T11:35:24+02:00",
146 | "Name": "FilterPump"
147 | },
148 | "SolarHeater": {
149 | "SolarHeaterCleaningTime": "21:30:00",
150 | "TurnOnDiff": 6.0,
151 | "TurnOffDiff": 3.0,
152 | "MaxPoolTemp": 29.5,
153 | "SolarHeaterCleaningDuration": 180,
154 | "NextStart": "2022-05-07T21:30:00+02:00",
155 | "NextEnd": "2022-05-07T21:33:00+02:00",
156 | "Name": "SolarHeater"
157 | },
158 | "Ph": {
159 | "MidCal": 0.0,
160 | "LowCal": 0.0,
161 | "HighCal": 0.0,
162 | "MaxValue": 7.3,
163 | "AcidInjectionDuration": 20,
164 | "AcidInjectionRecurringPeriod": 10,
165 | "Slope": null,
166 | "LedOn": true,
167 | "SensorsCalibrated": 0,
168 | "Voltage": 3.85,
169 | "Value": 6.9,
170 | "Address": "99",
171 | "UnitSign": "pH",
172 | "ViewFormat": "#0.00",
173 | "InterfaceFormat": "#0.000",
174 | "TimeStamp": "2022-05-12T11:18:08.0067131+02:00",
175 | "IntervalInSec": 5,
176 | "Name": "pHValue"
177 | },
178 | "Redox": {
179 | "On": 750,
180 | "Off": 840,
181 | "Cal": 0,
182 | "LedOn": true,
183 | "SensorsCalibrated": 0,
184 | "Voltage": 3.84,
185 | "Value": 745.0,
186 | "Address": "98",
187 | "UnitSign": "mV",
188 | "ViewFormat": "#0",
189 | "InterfaceFormat": "#0.0",
190 | "TimeStamp": "2022-05-12T11:17:28.1939391+02:00",
191 | "IntervalInSec": 60,
192 | "Name": "RedoxValue"
193 | },
194 | "Distance": {
195 | "NameL": "Volume",
196 | "UnitSignL": "L",
197 | "ViewFormatL": "#0",
198 | "InterfaceFormatL": "#0",
199 | "NumberOfMeasurements": 5,
200 | "Value": 0.0013728000000000017,
201 | "Address": "16/26",
202 | "UnitSign": "cm",
203 | "ViewFormat": "#0.00",
204 | "InterfaceFormat": "#0.00",
205 | "TimeStamp": "2022-05-12T11:17:28.301886+02:00",
206 | "IntervalInSec": 60,
207 | "Name": "Distance"
208 | },
209 | "Name": "Winter"
210 | }
--------------------------------------------------------------------------------
/w1_slave:
--------------------------------------------------------------------------------
1 | a0 00 4b 46 7f ff 0c 10 f2 : crc=f2 YES
2 | a0 00 4b 46 7f ff 0c 10 f2 t=10000
--------------------------------------------------------------------------------
/winpoolcontrolviewmodel.json:
--------------------------------------------------------------------------------
1 | {
2 | "Temperatures": {
3 | "Pool": {
4 | "Key": "Pool",
5 | "Value": 25.9,
6 | "Address": "28-0114536eebaa",
7 | "UnitSign": "°C",
8 | "ViewFormat": "#0.00",
9 | "InterfaceFormat": "#0.00",
10 | "TimeStamp": "2023-05-06T17:10:00.8460774+02:00",
11 | "IntervalInSec": 10,
12 | "Name": "Pool",
13 | "WinterMode": false
14 | },
15 | "SolarPreRun": {
16 | "Key": "SolarPreRun",
17 | "Value": 25.9,
18 | "Address": "28-041670557dff",
19 | "UnitSign": "°C",
20 | "ViewFormat": "#0.00",
21 | "InterfaceFormat": "#0.00",
22 | "TimeStamp": "2023-05-06T17:10:00.8776088+02:00",
23 | "IntervalInSec": 10,
24 | "Name": "SolarPreRun",
25 | "WinterMode": false
26 | },
27 | "SolarHeater": {
28 | "Key": "SolarHeater",
29 | "Value": 28.1,
30 | "Address": "28-0315a43260ff",
31 | "UnitSign": "°C",
32 | "ViewFormat": "#0.00",
33 | "InterfaceFormat": "#0.00",
34 | "TimeStamp": "2023-05-06T17:10:00.8935405+02:00",
35 | "IntervalInSec": 10,
36 | "Name": "SolarHeater",
37 | "WinterMode": false
38 | },
39 | "Technikraum": {
40 | "Key": "Technikraum",
41 | "Value": 25.9,
42 | "Address": "28-0317609128ff",
43 | "UnitSign": "°C",
44 | "ViewFormat": "#0.00",
45 | "InterfaceFormat": "#0.00",
46 | "TimeStamp": "2023-05-06T17:09:10.9065614+02:00",
47 | "IntervalInSec": 60,
48 | "Name": "TechnicRoom",
49 | "WinterMode": false
50 | },
51 | "FrostChecker": {
52 | "Key": "FrostChecker",
53 | "Value": 25.9,
54 | "Address": "28-0416705896ff",
55 | "UnitSign": "°C",
56 | "ViewFormat": "#0.00",
57 | "InterfaceFormat": "#0.00",
58 | "TimeStamp": "2023-05-06T17:09:10.9216874+02:00",
59 | "IntervalInSec": 60,
60 | "Name": "FrostChecker",
61 | "WinterMode": false
62 | }
63 | },
64 | "Switches": {
65 | "FilterPump": {
66 | "Key": "FilterPump",
67 | "RelayNumber": 8,
68 | "HighIsOn": false,
69 | "On": true,
70 | "IntervalInSec": 0,
71 | "Name": "FilterPump",
72 | "WinterMode": false
73 | },
74 | "SolarHeater": {
75 | "Key": "SolarHeater",
76 | "RelayNumber": 7,
77 | "HighIsOn": false,
78 | "On": false,
79 | "IntervalInSec": 0,
80 | "Name": "SolarHeater",
81 | "WinterMode": false
82 | },
83 | "Ph": {
84 | "Key": "Ph",
85 | "RelayNumber": 6,
86 | "HighIsOn": false,
87 | "On": false,
88 | "IntervalInSec": 0,
89 | "Name": "PhPump",
90 | "WinterMode": false
91 | },
92 | "Redox": {
93 | "Key": "Redox",
94 | "RelayNumber": 5,
95 | "HighIsOn": false,
96 | "On": true,
97 | "IntervalInSec": 0,
98 | "Name": "RedoxSwitch",
99 | "WinterMode": false
100 | },
101 | "Poollampe": {
102 | "Key": "Poollampe",
103 | "RelayNumber": 4,
104 | "HighIsOn": false,
105 | "On": false,
106 | "IntervalInSec": 0,
107 | "Name": "PoolLight",
108 | "WinterMode": false
109 | },
110 | "Three": {
111 | "Key": "Three",
112 | "RelayNumber": 3,
113 | "HighIsOn": false,
114 | "On": false,
115 | "IntervalInSec": 0,
116 | "Name": "Three",
117 | "WinterMode": false
118 | },
119 | "Two": {
120 | "Key": "Two",
121 | "RelayNumber": 2,
122 | "HighIsOn": false,
123 | "On": false,
124 | "IntervalInSec": 0,
125 | "Name": "Two",
126 | "WinterMode": false
127 | },
128 | "One": {
129 | "Key": "One",
130 | "RelayNumber": 1,
131 | "HighIsOn": false,
132 | "On": false,
133 | "IntervalInSec": 0,
134 | "Name": "One",
135 | "WinterMode": false
136 | }
137 | },
138 | "RelayConfig": {
139 | "RelayToLogicLevelConverter": {
140 | "1": 1,
141 | "2": 2,
142 | "3": 3,
143 | "4": 4,
144 | "5": 5,
145 | "6": 6,
146 | "7": 7,
147 | "8": 8
148 | },
149 | "LogicLevelConverterToGpio": {
150 | "1": 19,
151 | "2": 13,
152 | "3": 5,
153 | "4": 22,
154 | "5": 27,
155 | "6": 23,
156 | "7": 20,
157 | "8": 21
158 | }
159 | },
160 | "FilterPump": {
161 | "StandardFilterRunTime": 180,
162 | "StartMorning": "08:00:00",
163 | "StartNoon": "14:00:00",
164 | "FilterOff": "20:00:00",
165 | "NextStart": "2023-05-07T08:00:00+02:00",
166 | "NextEnd": "2023-05-06T17:35:24+02:00",
167 | "IntervalInSec": 0,
168 | "Name": "Name",
169 | "WinterMode": false
170 | },
171 | "SolarHeater": {
172 | "SolarHeaterCleaningTime": "21:30:00",
173 | "TurnOnDiff": 6.0,
174 | "TurnOffDiff": 3.0,
175 | "MaxPoolTemp": 29.5,
176 | "SolarHeaterCleaningDuration": 180,
177 | "NextStart": "2023-05-05T21:30:00+02:00",
178 | "NextEnd": "2023-05-05T21:33:00+02:00",
179 | "IntervalInSec": 0,
180 | "Name": "Name",
181 | "WinterMode": false
182 | },
183 | "Ph": {
184 | "MidCal": 0.0,
185 | "LowCal": 0.0,
186 | "HighCal": 0.0,
187 | "MaxValue": 7.3,
188 | "AcidInjectionDuration": 20,
189 | "AcidInjectionRecurringPeriod": 10,
190 | "Slope": null,
191 | "LedOn": true,
192 | "SensorsCalibrated": 0,
193 | "Voltage": 3.85,
194 | "Value": 7.399999999999999,
195 | "Address": "99",
196 | "UnitSign": "pH",
197 | "ViewFormat": "#0.00",
198 | "InterfaceFormat": "#0.000",
199 | "TimeStamp": "2023-05-06T17:09:11.3422343+02:00",
200 | "IntervalInSec": 60,
201 | "Name": "pHValue",
202 | "WinterMode": false
203 | },
204 | "Redox": {
205 | "On": 750,
206 | "Off": 840,
207 | "Cal": 0,
208 | "LedOn": true,
209 | "SensorsCalibrated": 0,
210 | "Voltage": 3.84,
211 | "Value": 805.0,
212 | "Address": "98",
213 | "UnitSign": "mV",
214 | "ViewFormat": "#0",
215 | "InterfaceFormat": "#0.0",
216 | "TimeStamp": "2023-05-06T17:09:11.4353267+02:00",
217 | "IntervalInSec": 60,
218 | "Name": "RedoxValue",
219 | "WinterMode": false
220 | },
221 | "Distance": {
222 | "NameL": "Volume",
223 | "UnitSignL": "L",
224 | "ViewFormatL": "#0",
225 | "InterfaceFormatL": "#0",
226 | "NumberOfMeasurements": 5,
227 | "Value": 0.021278400000000003,
228 | "Address": "16/26",
229 | "UnitSign": "cm",
230 | "ViewFormat": "#0.00",
231 | "InterfaceFormat": "#0.00",
232 | "TimeStamp": "2023-05-06T17:09:11.576952+02:00",
233 | "IntervalInSec": 60,
234 | "Name": "Distance",
235 | "WinterMode": false
236 | },
237 | "IntervalInSec": 0,
238 | "Name": "Winter",
239 | "WinterMode": false
240 | }
--------------------------------------------------------------------------------