<?xml version="1.0"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
	<id>http://217.79.180.177/mediawiki/api.php?action=feedcontributions&amp;feedformat=atom&amp;user=Uwe</id>
	<title>Aquarium-Control - User contributions [en]</title>
	<link rel="self" type="application/atom+xml" href="http://217.79.180.177/mediawiki/api.php?action=feedcontributions&amp;feedformat=atom&amp;user=Uwe"/>
	<link rel="alternate" type="text/html" href="http://217.79.180.177/mediawiki/index.php/Special:Contributions/Uwe"/>
	<updated>2026-06-04T07:02:38Z</updated>
	<subtitle>User contributions</subtitle>
	<generator>MediaWiki 1.40.0</generator>
	<entry>
		<id>http://217.79.180.177/mediawiki/index.php?title=Android_application&amp;diff=492</id>
		<title>Android application</title>
		<link rel="alternate" type="text/html" href="http://217.79.180.177/mediawiki/index.php?title=Android_application&amp;diff=492"/>
		<updated>2026-05-21T17:46:36Z</updated>

		<summary type="html">&lt;p&gt;Uwe: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;The app is available in the [https://play.google.com/store/apps/details?id=com.laimburggasse.aquariumcontroller Play store].&lt;br /&gt;
== Requirements ==&lt;br /&gt;
== Architecture ==&lt;br /&gt;
The app is written completely in Kotlin&lt;br /&gt;
* using Jetpack Compose&lt;br /&gt;
* using [https://github.com/evant/kotlin-inject Kotlin-inject] for dependency injection&lt;br /&gt;
* using [https://ktor.io Ktor] for server communication&lt;br /&gt;
* aiming to implement clean architecture&lt;br /&gt;
&lt;br /&gt;
=== Module structure ===&lt;br /&gt;
Besides the main app module, the app consists of the following library modules:&lt;br /&gt;
* balling&lt;br /&gt;
* common&lt;br /&gt;
* controller&lt;br /&gt;
* feed&lt;br /&gt;
* heating&lt;br /&gt;
* info&lt;br /&gt;
* summary&lt;br /&gt;
* refill&lt;br /&gt;
* schedule&lt;br /&gt;
* timedata&lt;br /&gt;
* ventilation&lt;br /&gt;
&lt;br /&gt;
The main &amp;lt;code&amp;gt;app&amp;lt;/code&amp;gt; module contains the AndroidManifest and the main activity (As of November 2025, the app only uses one activity).&lt;br /&gt;
The activity contains the navigation &amp;lt;code&amp;gt;NavHost&amp;lt;/code&amp;gt; in &amp;lt;code&amp;gt;onCreate&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
None of the other modules has a dependency to the main &amp;lt;code&amp;gt;app&amp;lt;/code&amp;gt; module.&lt;br /&gt;
&lt;br /&gt;
The &amp;lt;code&amp;gt;common&amp;lt;/code&amp;gt; module provides low-level functionalities and layout elements used in various places.&lt;br /&gt;
The following functionalities are located in &amp;lt;code&amp;gt;common&amp;lt;/code&amp;gt; module:&lt;br /&gt;
* &amp;lt;code&amp;gt;DataFetchResult&amp;lt;/code&amp;gt;: A wrapper class indicating the status of data fetching operation (&amp;lt;code&amp;gt;Success&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Error&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Loading&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Empty&amp;lt;/code&amp;gt;).&lt;br /&gt;
* &amp;lt;code&amp;gt;WebApiParams&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;WebApiPostRequest&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;WebApiPostRequestExecution&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;WebApiUrls&amp;lt;/code&amp;gt;: Classes and objects used for communication from the app to the server.&lt;br /&gt;
* &amp;lt;code&amp;gt;ControllerProfileDao&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;ControllerProfileDatabase&amp;lt;/code&amp;gt;: Classes which provide Room database functionality.&lt;br /&gt;
* Various classes used for dependency injection&lt;br /&gt;
* &amp;lt;code&amp;gt;SetValsSanityCheckResult&amp;lt;/code&amp;gt;: Class used for to communicate status of set values for &amp;lt;code&amp;gt;heating&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;ventilation&amp;lt;/code&amp;gt; module.&lt;br /&gt;
* &amp;lt;code&amp;gt;ControllerProfile&amp;lt;/code&amp;gt;: Class containing the profile data of the server (address, port, credentials, ...)&lt;br /&gt;
* &amp;lt;code&amp;gt;GlobalConstants&amp;lt;/code&amp;gt;: Mainly UI-related strings (non-context-related) and some minor functional constants&lt;br /&gt;
* Composable functions for the main drop down menu&lt;br /&gt;
* Composable functions for the theme&lt;br /&gt;
* Composable functions for hyperlinks, text edit fields, headline&lt;br /&gt;
* &amp;lt;code&amp;gt;ViewNavigationRoutes&amp;lt;/code&amp;gt;: Object containing the routing information processed in main &amp;lt;code&amp;gt;app&amp;lt;/code&amp;gt; module&lt;br /&gt;
* &amp;lt;code&amp;gt;ScreenshotTestHelper&amp;lt;/code&amp;gt;: Helper class for executing the instrumented snapshot testing using the emulator.&lt;br /&gt;
&lt;br /&gt;
=== Layer structure ===&lt;br /&gt;
The code is separated into the following layers:&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
[module]/&lt;br /&gt;
&lt;br /&gt;
├── src/main/kotlin/com/laimburggasse/aquariumcontrol/[module]/data/&lt;br /&gt;
&lt;br /&gt;
│   ├── remote/                   // Folder for data retrieval functionality&lt;br /&gt;
&lt;br /&gt;
│   │   └── dto/                  // Folder for Data Transfer Objects&lt;br /&gt;
&lt;br /&gt;
│   │       └── [module][name]Import.kt&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module][name]Reader.kt      // Implementation of GET server request&lt;br /&gt;
&lt;br /&gt;
│   ├── [module][name]RepositoryImpl.kt  // Repositories (implementation of interface)&lt;br /&gt;
&lt;br /&gt;
├── src/main/kotlin/com/laimburggasse/aquariumcontrol/[module]/domain/&lt;br /&gt;
&lt;br /&gt;
│   ├── model/                    // Folder for data definition classes&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module][name].kt.    // Classes for data definition&lt;br /&gt;
&lt;br /&gt;
│   ├── repository/               // Folder containing repository interface definitions&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module][name]Repository.kt.  // Interface defining the repository, used by view model&lt;br /&gt;
&lt;br /&gt;
├── src/main/kotlin/com/laimburggasse/aquariumcontrol/[module]/presentation/&lt;br /&gt;
&lt;br /&gt;
│   ├── entry/                    // First screen of feature&lt;br /&gt;
&lt;br /&gt;
│   ├── [components]/             // Shared code between different screens&lt;br /&gt;
&lt;br /&gt;
│   ├── [screen name]/            // Folder for each screen&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module][name]Screen.kt       // Composable showing the complete screen&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module][name]Composable.kt   // Composable showing elements inside the screen&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module][name]UiEvent.kt      // Sealed interface describing the communication from the composable to the view model&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module][name]UiState.kt      // Data class describing the content to be rendered by the composable&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module][name]ViewModel.kt    // Class implementing the view model&lt;br /&gt;
&lt;br /&gt;
│   ├── navigation/               // Folder containing class and functionality related to screen navigation&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module]NavigationEvent.kt  // One sealed class per module containing the navigation intent of the user&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module]Navigator.kt  // Module-specific navigation functionality&lt;br /&gt;
&lt;br /&gt;
│&lt;br /&gt;
└── build.gradle                  // Plugins, setting, dependencies&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Screen navigation ===&lt;br /&gt;
The screen navigation is controlled by a &amp;lt;code&amp;gt;NavHostController&amp;lt;/code&amp;gt; which is created in &amp;lt;code&amp;gt;MainActivity&amp;lt;/code&amp;gt; and remembered using &amp;lt;code&amp;gt;rememberNavController&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
==== Modular navigation ====&lt;br /&gt;
Each module adds its own navigation function to &amp;lt;code&amp;gt;NavHostController&amp;lt;/code&amp;gt;.&lt;br /&gt;
These extension functions are stored in &amp;lt;code&amp;gt;[module]/presentation/navigation/[module]Navigator.kt&amp;lt;/code&amp;gt;.&lt;br /&gt;
They encapsulate the routing logic for that specific module.&lt;br /&gt;
&lt;br /&gt;
==== Event-Driven Navigation ====&lt;br /&gt;
The functions receive as parameter a specific sealed class &amp;lt;code&amp;gt;[module]NavigationEvent&amp;lt;/code&amp;gt;.&lt;br /&gt;
These sealed classes contain objects for each navigation event inside the module.&lt;br /&gt;
&lt;br /&gt;
==== Implementation pattern ====&lt;br /&gt;
The navigation functions are used in the following locations:&lt;br /&gt;
* inside the lambda function passed to &amp;lt;code&amp;gt;NavHost&amp;lt;/code&amp;gt; in &amp;lt;code&amp;gt;MainActivity&amp;lt;/code&amp;gt; for each screen.&lt;br /&gt;
* inside composables themselves being passed as lambda function from within &amp;lt;code&amp;gt;MainActivity&amp;lt;/code&amp;gt;&lt;br /&gt;
:* The composable remain unaware of &amp;lt;code&amp;gt;NavHostController&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Routes and arguments ====&lt;br /&gt;
Source and destination of the routing is defined using strings from the object &amp;lt;code&amp;gt;ViewNavigationRoutes&amp;lt;/code&amp;gt; where data that needs to be transferred between screens is embedded in the strings.&lt;br /&gt;
&lt;br /&gt;
== Example workflow for adding API communication ==&lt;br /&gt;
=== Implementation ===&lt;br /&gt;
Create the model: &amp;lt;code&amp;gt;RefillState&amp;lt;/code&amp;gt; as &amp;lt;code&amp;gt;enum&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Add a reader &amp;lt;code&amp;gt;class&amp;lt;/code&amp;gt; &amp;lt;code&amp;gt;RefillStateReader&amp;lt;/code&amp;gt; which uses the injected interface (Retrofit)&lt;br /&gt;
&lt;br /&gt;
Update the &amp;lt;code&amp;gt;RefillStateApiService&amp;lt;/code&amp;gt;: Add a function to load the state via Retrofit&lt;br /&gt;
&lt;br /&gt;
Add the label for the JSON property in &amp;lt;code&amp;gt;WebApiParams&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Add the URL for the end point in &amp;lt;code&amp;gt;WebApiUrls&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Add a wrapper &amp;lt;code&amp;gt;RefillStateResponse&amp;lt;/code&amp;gt; for the model to allow Retrofit parsing the JSON&lt;br /&gt;
&lt;br /&gt;
Update the repository interface and the repository interface implementation:&lt;br /&gt;
* Add a flow that delivers a &amp;lt;code&amp;gt;DataFetchResult&amp;lt;/code&amp;gt; of the &amp;lt;code&amp;gt;RefillState&amp;lt;/code&amp;gt;&lt;br /&gt;
* Add a function that allows the view model to trigger a refresh&lt;br /&gt;
&lt;br /&gt;
Add a wrapper &amp;lt;code&amp;gt;class&amp;lt;/code&amp;gt; &amp;lt;code&amp;gt;RefillStateUiState&amp;lt;/code&amp;gt;&lt;br /&gt;
Update the view model&lt;br /&gt;
* Add a flow that delivers a &amp;lt;code&amp;gt;RefillStateUiState&amp;lt;/code&amp;gt; based on the &amp;lt;code&amp;gt;DataFetchResult&amp;lt;/code&amp;gt; from repository&lt;br /&gt;
* Handle the new UI event for refreshing&lt;br /&gt;
&lt;br /&gt;
Add composable &amp;lt;code&amp;gt;RefillStateComposable&amp;lt;/code&amp;gt; for displaying the &amp;lt;code&amp;gt;RefillState&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Update &amp;lt;code&amp;gt;RefillUiEvent&amp;lt;/code&amp;gt; with an object for the refresh&lt;br /&gt;
&lt;br /&gt;
Update &amp;lt;code&amp;gt;RefillScreen&amp;lt;/code&amp;gt;:&lt;br /&gt;
* additional parameter (&amp;lt;code&amp;gt;RefillStateUiState&amp;lt;/code&amp;gt;) in composable function and in previews&lt;br /&gt;
* Added &amp;lt;code&amp;gt;PullToRefreshBox&amp;lt;/code&amp;gt;&lt;br /&gt;
* Added parsing of &amp;lt;code&amp;gt;RefillUiState&amp;lt;/code&amp;gt; and display of either&lt;br /&gt;
:* &amp;lt;code&amp;gt;RefillStateComposable&amp;lt;/code&amp;gt; in case of success&lt;br /&gt;
:* Text-based error message in case of error&lt;br /&gt;
:* &amp;lt;code&amp;gt;CircularProgressIndicator&amp;lt;/code&amp;gt; in case of loading&lt;br /&gt;
&lt;br /&gt;
Inject &amp;lt;code&amp;gt;RefillUiState&amp;lt;/code&amp;gt; into &amp;lt;code&amp;gt;RefillScreen&amp;lt;/code&amp;gt; in the &amp;lt;code&amp;gt;MainActivity&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;RefillScreenTest&amp;lt;/code&amp;gt; as well as in previews of &amp;lt;code&amp;gt;RefillScreen&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
(Optional) Check if &amp;lt;code&amp;gt;RefillDataModule&amp;lt;/code&amp;gt; needs update (not required in this case)&lt;br /&gt;
&lt;br /&gt;
=== Testing ===&lt;br /&gt;
Update existing screenshot testing (compose screenshot testing and instrumented tests)&lt;br /&gt;
&lt;br /&gt;
Add compose screenshot testing for new composable&lt;br /&gt;
&lt;br /&gt;
== Build ==&lt;br /&gt;
Sync gradle and build in Android Studio.&lt;br /&gt;
Run &amp;lt;code&amp;gt;./gradlew :shared:assemble&amp;lt;/code&amp;gt; to assemble the shared KMP module.&lt;br /&gt;
&lt;br /&gt;
== Test strategy ==&lt;br /&gt;
The app uses different testing approaches:&lt;br /&gt;
* Linting&lt;br /&gt;
* Unit tests&lt;br /&gt;
* Compose preview screenshot testing&lt;br /&gt;
* Screenshot tests running as instrumented tests on emulator&lt;br /&gt;
&lt;br /&gt;
=== Linting ===&lt;br /&gt;
Run &amp;lt;code&amp;gt;./gradlew lint&amp;lt;/code&amp;gt;: This command shall yield no warnings.&lt;br /&gt;
Use the Android Studio code inspection for more detailed linting - some of the warnings cannot be resolved. &lt;br /&gt;
&lt;br /&gt;
=== Compose preview screenshot tests ===&lt;br /&gt;
These tests run comparably fast and device-independent.&lt;br /&gt;
&lt;br /&gt;
The compose preview screenshot tests cover portrait mode/landscape mode, light/dark mode (four variants).&lt;br /&gt;
&lt;br /&gt;
Limitations:&lt;br /&gt;
* Two tests within &amp;lt;code&amp;gt;feed&amp;lt;/code&amp;gt; module can only run in landscape mode (&amp;lt;code&amp;gt;FeedProfileScheduleScreenTest&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;FeedHistoryScreenTest&amp;lt;/code&amp;gt;) due to excessive heap memory utilisation.&lt;br /&gt;
* &amp;lt;code&amp;gt;Info&amp;lt;/code&amp;gt; screen is not tested due to &amp;lt;code&amp;gt;LibrariesContainer&amp;lt;/code&amp;gt; (dynamic content which cannot be mocked).&lt;br /&gt;
&lt;br /&gt;
The tests are located in &amp;lt;code&amp;gt;[module]/src/screenshotTest/kotlin/com/aquariumcontrol/[module]/&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
The references are located in &amp;lt;code&amp;gt;[module]/src/screenshotTestDebug/kotlin/com/aquariumcontrol/[module]/&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
The test reports are located in &amp;lt;code&amp;gt;build/reports/screenshotTest/preview/debug/&amp;lt;/code&amp;gt;. The test reports for the compose screenshot tests contain reference, actual and delta images.&lt;br /&gt;
&lt;br /&gt;
The images are generated with &amp;lt;code&amp;gt;./gradlew [module]:updateDebugScreenshotTest&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
The images are validated with &amp;lt;code&amp;gt;./gradlew [module]:validateDebugScreenshotTest&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Omitting the module will execute all tests.&lt;br /&gt;
&lt;br /&gt;
=== Instrumented tests ===&lt;br /&gt;
The tests run comparably long and are bound to an emulator device.&lt;br /&gt;
As of November 2025, a Pixel 5 API S is used for the instrumented test.&lt;br /&gt;
The instrumented tests contain&lt;br /&gt;
* screenshot testing for the complete screens (&amp;lt;code&amp;gt;ScreenNameTest&amp;lt;/code&amp;gt;)&lt;br /&gt;
* verification if the composable triggers the right event when tapping on buttons bound to navigation to another screen (&amp;lt;code&amp;gt;ScreenNameUITest&amp;lt;/code&amp;gt;)&lt;br /&gt;
&lt;br /&gt;
Limitations:&lt;br /&gt;
* &amp;lt;code&amp;gt;Info&amp;lt;/code&amp;gt; screen is not tested due to &amp;lt;code&amp;gt;LibrariesContainer&amp;lt;/code&amp;gt; (dynamic content which cannot be mocked and also does not allow execution of instrumented test).&lt;br /&gt;
&lt;br /&gt;
The test reports are located in &amp;lt;code&amp;gt;build/reports/androidTests/connected/debug/&amp;lt;/code&amp;gt;. The test reports for the compose screenshot tests do not contain any images.&lt;br /&gt;
&lt;br /&gt;
The tests are executed with &amp;lt;code&amp;gt;./gradlew [module]:connectedDebugAndroidTest&amp;lt;/code&amp;gt;.&lt;br /&gt;
Omitting the module will execute all tests.&lt;br /&gt;
&lt;br /&gt;
A switch called &amp;lt;code&amp;gt;recordMode&amp;lt;/code&amp;gt; inside each test class for screenshot testing determines if the image is generated &amp;lt;code&amp;gt;(true)&amp;lt;/code&amp;gt; or if the image is validated &amp;lt;code&amp;gt;(false)&amp;lt;/code&amp;gt;. The images are generated on the device &amp;lt;code&amp;gt;(/data/data/com.laimburggasse.aquariumcontrol.[module].test/files/screenshots_output/)&amp;lt;/code&amp;gt; and need to be downloaded to the repository.&lt;br /&gt;
&lt;br /&gt;
=== Unit tests ===&lt;br /&gt;
As of April 2026, unit tests are developed for:&lt;br /&gt;
* all view models&lt;br /&gt;
* all repositories&lt;br /&gt;
&lt;br /&gt;
The unit test cases for the view models make extensive use of the &amp;lt;code&amp;gt;MockK&amp;lt;/code&amp;gt; library.&lt;br /&gt;
&lt;br /&gt;
The unit test cases for the repositories use mock implementations of the &amp;lt;code&amp;gt;Retrofit&amp;lt;/code&amp;gt; interface.&lt;br /&gt;
&lt;br /&gt;
All unit test cases are executed using the command: &amp;lt;code&amp;gt;./gradlew :[module]:testDebugUnitTest&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The code coverage report can be created using: &amp;lt;code&amp;gt;./gradlew createDebugUnitTestCoverageReport&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The gradle task removes the injected code from the test coverage report: &amp;lt;code&amp;gt;./gradlew jacocoTestReport&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The report is stored in &amp;lt;code&amp;gt;[module]/build/reports/jacoco/jacocoTestReport/html&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Release procedure ==&lt;br /&gt;
# Update &amp;lt;code&amp;gt;versionCode&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;versionName&amp;lt;/code&amp;gt; in &amp;lt;code&amp;gt;build.gradle.kts&amp;lt;/code&amp;gt; of &amp;lt;code&amp;gt;app&amp;lt;/code&amp;gt; module.&lt;br /&gt;
# Create a release branch (&amp;lt;code&amp;gt;release/[version]&amp;lt;/code&amp;gt;), checkout, commit and push.&lt;br /&gt;
# Change to release build variant.&lt;br /&gt;
# Build&lt;br /&gt;
# Generate a signed App bundle&lt;br /&gt;
# Upload to play store&lt;/div&gt;</summary>
		<author><name>Uwe</name></author>
	</entry>
	<entry>
		<id>http://217.79.180.177/mediawiki/index.php?title=Android_application&amp;diff=491</id>
		<title>Android application</title>
		<link rel="alternate" type="text/html" href="http://217.79.180.177/mediawiki/index.php?title=Android_application&amp;diff=491"/>
		<updated>2026-05-20T20:06:32Z</updated>

		<summary type="html">&lt;p&gt;Uwe: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;The app is available in the [https://play.google.com/store/apps/details?id=com.laimburggasse.aquariumcontroller Play store].&lt;br /&gt;
== Requirements ==&lt;br /&gt;
== Architecture ==&lt;br /&gt;
The app is written completely in Kotlin&lt;br /&gt;
* using Jetpack Compose&lt;br /&gt;
* using [https://github.com/evant/kotlin-inject Kotlin-inject] for dependency injection&lt;br /&gt;
* using [https://ktor.io Ktor] for server communication&lt;br /&gt;
* aiming to implement clean architecture&lt;br /&gt;
&lt;br /&gt;
=== Module structure ===&lt;br /&gt;
Besides the main app module, the app consists of the following library modules:&lt;br /&gt;
* balling&lt;br /&gt;
* common&lt;br /&gt;
* controller&lt;br /&gt;
* feed&lt;br /&gt;
* heating&lt;br /&gt;
* info&lt;br /&gt;
* summary&lt;br /&gt;
* refill&lt;br /&gt;
* schedule&lt;br /&gt;
* timedata&lt;br /&gt;
* ventilation&lt;br /&gt;
&lt;br /&gt;
The main &amp;lt;code&amp;gt;app&amp;lt;/code&amp;gt; module contains the AndroidManifest and the main activity (As of November 2025, the app only uses one activity).&lt;br /&gt;
The activity contains the navigation &amp;lt;code&amp;gt;NavHost&amp;lt;/code&amp;gt; in &amp;lt;code&amp;gt;onCreate&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
None of the other modules has a dependency to the main &amp;lt;code&amp;gt;app&amp;lt;/code&amp;gt; module.&lt;br /&gt;
&lt;br /&gt;
The &amp;lt;code&amp;gt;common&amp;lt;/code&amp;gt; module provides low-level functionalities and layout elements used in various places.&lt;br /&gt;
The following functionalities are located in &amp;lt;code&amp;gt;common&amp;lt;/code&amp;gt; module:&lt;br /&gt;
* &amp;lt;code&amp;gt;DataFetchResult&amp;lt;/code&amp;gt;: A wrapper class indicating the status of data fetching operation (&amp;lt;code&amp;gt;Success&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Error&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Loading&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Empty&amp;lt;/code&amp;gt;).&lt;br /&gt;
* &amp;lt;code&amp;gt;WebApiParams&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;WebApiPostRequest&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;WebApiPostRequestExecution&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;WebApiUrls&amp;lt;/code&amp;gt;: Classes and objects used for communication from the app to the server.&lt;br /&gt;
* &amp;lt;code&amp;gt;ControllerProfileDao&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;ControllerProfileDatabase&amp;lt;/code&amp;gt;: Classes which provide Room database functionality.&lt;br /&gt;
* Various classes used for dependency injection&lt;br /&gt;
* &amp;lt;code&amp;gt;SetValsSanityCheckResult&amp;lt;/code&amp;gt;: Class used for to communicate status of set values for &amp;lt;code&amp;gt;heating&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;ventilation&amp;lt;/code&amp;gt; module.&lt;br /&gt;
* &amp;lt;code&amp;gt;ControllerProfile&amp;lt;/code&amp;gt;: Class containing the profile data of the server (address, port, credentials, ...)&lt;br /&gt;
* &amp;lt;code&amp;gt;GlobalConstants&amp;lt;/code&amp;gt;: Mainly UI-related strings (non-context-related) and some minor functional constants&lt;br /&gt;
* Composable functions for the main drop down menu&lt;br /&gt;
* Composable functions for the theme&lt;br /&gt;
* Composable functions for hyperlinks, text edit fields, headline&lt;br /&gt;
* &amp;lt;code&amp;gt;ViewNavigationRoutes&amp;lt;/code&amp;gt;: Object containing the routing information processed in main &amp;lt;code&amp;gt;app&amp;lt;/code&amp;gt; module&lt;br /&gt;
* &amp;lt;code&amp;gt;ScreenshotTestHelper&amp;lt;/code&amp;gt;: Helper class for executing the instrumented snapshot testing using the emulator.&lt;br /&gt;
&lt;br /&gt;
=== Layer structure ===&lt;br /&gt;
The code is separated into the following layers:&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
[module]/&lt;br /&gt;
&lt;br /&gt;
├── src/main/kotlin/com/laimburggasse/aquariumcontrol/[module]/data/&lt;br /&gt;
&lt;br /&gt;
│   ├── remote/                   // Folder for data retrieval functionality&lt;br /&gt;
&lt;br /&gt;
│   │   └── dto/                  // Folder for Data Transfer Objects&lt;br /&gt;
&lt;br /&gt;
│   │       └── [module][name]Import.kt&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module][name]Reader.kt      // Implementation of GET server request&lt;br /&gt;
&lt;br /&gt;
│   ├── [module][name]RepositoryImpl.kt  // Repositories (implementation of interface)&lt;br /&gt;
&lt;br /&gt;
├── src/main/kotlin/com/laimburggasse/aquariumcontrol/[module]/domain/&lt;br /&gt;
&lt;br /&gt;
│   ├── model/                    // Folder for data definition classes&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module][name].kt.    // Classes for data definition&lt;br /&gt;
&lt;br /&gt;
│   ├── repository/               // Folder containing repository interface definitions&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module][name]Repository.kt.  // Interface defining the repository, used by view model&lt;br /&gt;
&lt;br /&gt;
├── src/main/kotlin/com/laimburggasse/aquariumcontrol/[module]/presentation/&lt;br /&gt;
&lt;br /&gt;
│   ├── entry/                    // First screen of feature&lt;br /&gt;
&lt;br /&gt;
│   ├── [components]/             // Shared code between different screens&lt;br /&gt;
&lt;br /&gt;
│   ├── [screen name]/            // Folder for each screen&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module][name]Screen.kt       // Composable showing the complete screen&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module][name]Composable.kt   // Composable showing elements inside the screen&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module][name]UiEvent.kt      // Sealed interface describing the communication from the composable to the view model&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module][name]UiState.kt      // Data class describing the content to be rendered by the composable&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module][name]ViewModel.kt    // Class implementing the view model&lt;br /&gt;
&lt;br /&gt;
│   ├── navigation/               // Folder containing class and functionality related to screen navigation&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module]NavigationEvent.kt  // One sealed class per module containing the navigation intent of the user&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module]Navigator.kt  // Module-specific navigation functionality&lt;br /&gt;
&lt;br /&gt;
│&lt;br /&gt;
└── build.gradle                  // Plugins, setting, dependencies&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Screen navigation ===&lt;br /&gt;
The screen navigation is controlled by a &amp;lt;code&amp;gt;NavHostController&amp;lt;/code&amp;gt; which is created in &amp;lt;code&amp;gt;MainActivity&amp;lt;/code&amp;gt; and remembered using &amp;lt;code&amp;gt;rememberNavController&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
==== Modular navigation ====&lt;br /&gt;
Each module adds its own navigation function to &amp;lt;code&amp;gt;NavHostController&amp;lt;/code&amp;gt;.&lt;br /&gt;
These extension functions are stored in &amp;lt;code&amp;gt;[module]/presentation/navigation/[module]Navigator.kt&amp;lt;/code&amp;gt;.&lt;br /&gt;
They encapsulate the routing logic for that specific module.&lt;br /&gt;
&lt;br /&gt;
==== Event-Driven Navigation ====&lt;br /&gt;
The functions receive as parameter a specific sealed class &amp;lt;code&amp;gt;[module]NavigationEvent&amp;lt;/code&amp;gt;.&lt;br /&gt;
These sealed classes contain objects for each navigation event inside the module.&lt;br /&gt;
&lt;br /&gt;
==== Implementation pattern ====&lt;br /&gt;
The navigation functions are used in the following locations:&lt;br /&gt;
* inside the lambda function passed to &amp;lt;code&amp;gt;NavHost&amp;lt;/code&amp;gt; in &amp;lt;code&amp;gt;MainActivity&amp;lt;/code&amp;gt; for each screen.&lt;br /&gt;
* inside composables themselves being passed as lambda function from within &amp;lt;code&amp;gt;MainActivity&amp;lt;/code&amp;gt;&lt;br /&gt;
:* The composable remain unaware of &amp;lt;code&amp;gt;NavHostController&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Routes and arguments ====&lt;br /&gt;
Source and destination of the routing is defined using strings from the object &amp;lt;code&amp;gt;ViewNavigationRoutes&amp;lt;/code&amp;gt; where data that needs to be transferred between screens is embedded in the strings.&lt;br /&gt;
&lt;br /&gt;
== Example workflow for adding API communication ==&lt;br /&gt;
=== Implementation ===&lt;br /&gt;
Create the model: &amp;lt;code&amp;gt;RefillState&amp;lt;/code&amp;gt; as &amp;lt;code&amp;gt;enum&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Add a reader &amp;lt;code&amp;gt;class&amp;lt;/code&amp;gt; &amp;lt;code&amp;gt;RefillStateReader&amp;lt;/code&amp;gt; which uses the injected interface (Retrofit)&lt;br /&gt;
&lt;br /&gt;
Update the &amp;lt;code&amp;gt;RefillStateApiService&amp;lt;/code&amp;gt;: Add a function to load the state via Retrofit&lt;br /&gt;
&lt;br /&gt;
Add the label for the JSON property in &amp;lt;code&amp;gt;WebApiParams&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Add the URL for the end point in &amp;lt;code&amp;gt;WebApiUrls&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Add a wrapper &amp;lt;code&amp;gt;RefillStateResponse&amp;lt;/code&amp;gt; for the model to allow Retrofit parsing the JSON&lt;br /&gt;
&lt;br /&gt;
Update the repository interface and the repository interface implementation:&lt;br /&gt;
* Add a flow that delivers a &amp;lt;code&amp;gt;DataFetchResult&amp;lt;/code&amp;gt; of the &amp;lt;code&amp;gt;RefillState&amp;lt;/code&amp;gt;&lt;br /&gt;
* Add a function that allows the view model to trigger a refresh&lt;br /&gt;
&lt;br /&gt;
Add a wrapper &amp;lt;code&amp;gt;class&amp;lt;/code&amp;gt; &amp;lt;code&amp;gt;RefillStateUiState&amp;lt;/code&amp;gt;&lt;br /&gt;
Update the view model&lt;br /&gt;
* Add a flow that delivers a &amp;lt;code&amp;gt;RefillStateUiState&amp;lt;/code&amp;gt; based on the &amp;lt;code&amp;gt;DataFetchResult&amp;lt;/code&amp;gt; from repository&lt;br /&gt;
* Handle the new UI event for refreshing&lt;br /&gt;
&lt;br /&gt;
Add composable &amp;lt;code&amp;gt;RefillStateComposable&amp;lt;/code&amp;gt; for displaying the &amp;lt;code&amp;gt;RefillState&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Update &amp;lt;code&amp;gt;RefillUiEvent&amp;lt;/code&amp;gt; with an object for the refresh&lt;br /&gt;
&lt;br /&gt;
Update &amp;lt;code&amp;gt;RefillScreen&amp;lt;/code&amp;gt;:&lt;br /&gt;
* additional parameter (&amp;lt;code&amp;gt;RefillStateUiState&amp;lt;/code&amp;gt;) in composable function and in previews&lt;br /&gt;
* Added &amp;lt;code&amp;gt;PullToRefreshBox&amp;lt;/code&amp;gt;&lt;br /&gt;
* Added parsing of &amp;lt;code&amp;gt;RefillUiState&amp;lt;/code&amp;gt; and display of either&lt;br /&gt;
:* &amp;lt;code&amp;gt;RefillStateComposable&amp;lt;/code&amp;gt; in case of success&lt;br /&gt;
:* Text-based error message in case of error&lt;br /&gt;
:* &amp;lt;code&amp;gt;CircularProgressIndicator&amp;lt;/code&amp;gt; in case of loading&lt;br /&gt;
&lt;br /&gt;
Inject &amp;lt;code&amp;gt;RefillUiState&amp;lt;/code&amp;gt; into &amp;lt;code&amp;gt;RefillScreen&amp;lt;/code&amp;gt; in the &amp;lt;code&amp;gt;MainActivity&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;RefillScreenTest&amp;lt;/code&amp;gt; as well as in previews of &amp;lt;code&amp;gt;RefillScreen&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
(Optional) Check if &amp;lt;code&amp;gt;RefillDataModule&amp;lt;/code&amp;gt; needs update (not required in this case)&lt;br /&gt;
&lt;br /&gt;
=== Testing ===&lt;br /&gt;
Update existing screenshot testing (compose screenshot testing and instrumented tests)&lt;br /&gt;
&lt;br /&gt;
Add compose screenshot testing for new composable&lt;br /&gt;
&lt;br /&gt;
== Test strategy ==&lt;br /&gt;
The app uses different testing approaches:&lt;br /&gt;
* Linting&lt;br /&gt;
* Unit tests&lt;br /&gt;
* Compose preview screenshot testing&lt;br /&gt;
* Screenshot tests running as instrumented tests on emulator&lt;br /&gt;
&lt;br /&gt;
=== Linting ===&lt;br /&gt;
Run &amp;lt;code&amp;gt;./gradlew lint&amp;lt;/code&amp;gt;: This command shall yield no warnings.&lt;br /&gt;
Use the Android Studio code inspection for more detailed linting - some of the warnings cannot be resolved. &lt;br /&gt;
&lt;br /&gt;
=== Compose preview screenshot tests ===&lt;br /&gt;
These tests run comparably fast and device-independent.&lt;br /&gt;
&lt;br /&gt;
The compose preview screenshot tests cover portrait mode/landscape mode, light/dark mode (four variants).&lt;br /&gt;
&lt;br /&gt;
Limitations:&lt;br /&gt;
* Two tests within &amp;lt;code&amp;gt;feed&amp;lt;/code&amp;gt; module can only run in landscape mode (&amp;lt;code&amp;gt;FeedProfileScheduleScreenTest&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;FeedHistoryScreenTest&amp;lt;/code&amp;gt;) due to excessive heap memory utilisation.&lt;br /&gt;
* &amp;lt;code&amp;gt;Info&amp;lt;/code&amp;gt; screen is not tested due to &amp;lt;code&amp;gt;LibrariesContainer&amp;lt;/code&amp;gt; (dynamic content which cannot be mocked).&lt;br /&gt;
&lt;br /&gt;
The tests are located in &amp;lt;code&amp;gt;[module]/src/screenshotTest/kotlin/com/aquariumcontrol/[module]/&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
The references are located in &amp;lt;code&amp;gt;[module]/src/screenshotTestDebug/kotlin/com/aquariumcontrol/[module]/&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
The test reports are located in &amp;lt;code&amp;gt;build/reports/screenshotTest/preview/debug/&amp;lt;/code&amp;gt;. The test reports for the compose screenshot tests contain reference, actual and delta images.&lt;br /&gt;
&lt;br /&gt;
The images are generated with &amp;lt;code&amp;gt;./gradlew [module]:updateDebugScreenshotTest&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
The images are validated with &amp;lt;code&amp;gt;./gradlew [module]:validateDebugScreenshotTest&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Omitting the module will execute all tests.&lt;br /&gt;
&lt;br /&gt;
=== Instrumented tests ===&lt;br /&gt;
The tests run comparably long and are bound to an emulator device.&lt;br /&gt;
As of November 2025, a Pixel 5 API S is used for the instrumented test.&lt;br /&gt;
The instrumented tests contain&lt;br /&gt;
* screenshot testing for the complete screens (&amp;lt;code&amp;gt;ScreenNameTest&amp;lt;/code&amp;gt;)&lt;br /&gt;
* verification if the composable triggers the right event when tapping on buttons bound to navigation to another screen (&amp;lt;code&amp;gt;ScreenNameUITest&amp;lt;/code&amp;gt;)&lt;br /&gt;
&lt;br /&gt;
Limitations:&lt;br /&gt;
* &amp;lt;code&amp;gt;Info&amp;lt;/code&amp;gt; screen is not tested due to &amp;lt;code&amp;gt;LibrariesContainer&amp;lt;/code&amp;gt; (dynamic content which cannot be mocked and also does not allow execution of instrumented test).&lt;br /&gt;
&lt;br /&gt;
The test reports are located in &amp;lt;code&amp;gt;build/reports/androidTests/connected/debug/&amp;lt;/code&amp;gt;. The test reports for the compose screenshot tests do not contain any images.&lt;br /&gt;
&lt;br /&gt;
The tests are executed with &amp;lt;code&amp;gt;./gradlew [module]:connectedDebugAndroidTest&amp;lt;/code&amp;gt;.&lt;br /&gt;
Omitting the module will execute all tests.&lt;br /&gt;
&lt;br /&gt;
A switch called &amp;lt;code&amp;gt;recordMode&amp;lt;/code&amp;gt; inside each test class for screenshot testing determines if the image is generated &amp;lt;code&amp;gt;(true)&amp;lt;/code&amp;gt; or if the image is validated &amp;lt;code&amp;gt;(false)&amp;lt;/code&amp;gt;. The images are generated on the device &amp;lt;code&amp;gt;(/data/data/com.laimburggasse.aquariumcontrol.[module].test/files/screenshots_output/)&amp;lt;/code&amp;gt; and need to be downloaded to the repository.&lt;br /&gt;
&lt;br /&gt;
=== Unit tests ===&lt;br /&gt;
As of April 2026, unit tests are developed for:&lt;br /&gt;
* all view models&lt;br /&gt;
* all repositories&lt;br /&gt;
&lt;br /&gt;
The unit test cases for the view models make extensive use of the &amp;lt;code&amp;gt;MockK&amp;lt;/code&amp;gt; library.&lt;br /&gt;
&lt;br /&gt;
The unit test cases for the repositories use mock implementations of the &amp;lt;code&amp;gt;Retrofit&amp;lt;/code&amp;gt; interface.&lt;br /&gt;
&lt;br /&gt;
All unit test cases are executed using the command: &amp;lt;code&amp;gt;./gradlew :[module]:testDebugUnitTest&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The code coverage report can be created using: &amp;lt;code&amp;gt;./gradlew createDebugUnitTestCoverageReport&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The gradle task removes the injected code from the test coverage report: &amp;lt;code&amp;gt;./gradlew jacocoTestReport&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The report is stored in &amp;lt;code&amp;gt;[module]/build/reports/jacoco/jacocoTestReport/html&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Release procedure ==&lt;br /&gt;
# Update &amp;lt;code&amp;gt;versionCode&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;versionName&amp;lt;/code&amp;gt; in &amp;lt;code&amp;gt;build.gradle.kts&amp;lt;/code&amp;gt; of &amp;lt;code&amp;gt;app&amp;lt;/code&amp;gt; module.&lt;br /&gt;
# Create a release branch (&amp;lt;code&amp;gt;release/[version]&amp;lt;/code&amp;gt;), checkout, commit and push.&lt;br /&gt;
# Change to release build variant.&lt;br /&gt;
# Build&lt;br /&gt;
# Generate a signed App bundle&lt;br /&gt;
# Upload to play store&lt;/div&gt;</summary>
		<author><name>Uwe</name></author>
	</entry>
	<entry>
		<id>http://217.79.180.177/mediawiki/index.php?title=Android_application&amp;diff=490</id>
		<title>Android application</title>
		<link rel="alternate" type="text/html" href="http://217.79.180.177/mediawiki/index.php?title=Android_application&amp;diff=490"/>
		<updated>2026-05-20T12:19:14Z</updated>

		<summary type="html">&lt;p&gt;Uwe: /* Test strategy */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;The app is available in the [https://play.google.com/store/apps/details?id=com.laimburggasse.aquariumcontroller Play store].&lt;br /&gt;
== Requirements ==&lt;br /&gt;
== Architecture ==&lt;br /&gt;
The app is written completely in Kotlin using Jetpack Compose, Dagger/Hilt and aiming to implement a clean architecture.&lt;br /&gt;
=== Module structure ===&lt;br /&gt;
Besides the main app module, the app consists of the following library modules:&lt;br /&gt;
* balling&lt;br /&gt;
* common&lt;br /&gt;
* controller&lt;br /&gt;
* feed&lt;br /&gt;
* heating&lt;br /&gt;
* info&lt;br /&gt;
* overview&lt;br /&gt;
* refill&lt;br /&gt;
* schedule&lt;br /&gt;
* timedata&lt;br /&gt;
* ventilation&lt;br /&gt;
&lt;br /&gt;
The main &amp;lt;code&amp;gt;app&amp;lt;/code&amp;gt; module contains the AndroidManifest and the main activity (As of November 2025, the app only uses one activity).&lt;br /&gt;
The activity contains the navigation &amp;lt;code&amp;gt;NavHost&amp;lt;/code&amp;gt; in &amp;lt;code&amp;gt;onCreate&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
None of the other modules has a dependency to the main &amp;lt;code&amp;gt;app&amp;lt;/code&amp;gt; module.&lt;br /&gt;
&lt;br /&gt;
The &amp;lt;code&amp;gt;common&amp;lt;/code&amp;gt; module provides low-level functionalities and layout elements used in various places.&lt;br /&gt;
The following functionalities are located in &amp;lt;code&amp;gt;common&amp;lt;/code&amp;gt; module:&lt;br /&gt;
* &amp;lt;code&amp;gt;DataFetchResult&amp;lt;/code&amp;gt;: A wrapper class indicating the status of data fetching operation (&amp;lt;code&amp;gt;Success&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Error&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Loading&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Empty&amp;lt;/code&amp;gt;).&lt;br /&gt;
* &amp;lt;code&amp;gt;WebApiParams&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;WebApiPostRequest&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;WebApiPostRequestExecution&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;WebApiUrls&amp;lt;/code&amp;gt;: Classes and objects used for communication from the app to the server.&lt;br /&gt;
* &amp;lt;code&amp;gt;ControllerProfileDao&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;ControllerProfileDatabase&amp;lt;/code&amp;gt;: Classes which provide Room database functionality.&lt;br /&gt;
* Various classes used for dependency injection (Hilt)&lt;br /&gt;
* &amp;lt;code&amp;gt;SetValsSanityCheckResult&amp;lt;/code&amp;gt;: Class used for to communicate status of set values for &amp;lt;code&amp;gt;heating&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;ventilation&amp;lt;/code&amp;gt; module.&lt;br /&gt;
* &amp;lt;code&amp;gt;ControllerProfile&amp;lt;/code&amp;gt;: Class containing the profile data of the server (address, port, credentials, ...)&lt;br /&gt;
* &amp;lt;code&amp;gt;GlobalConstants&amp;lt;/code&amp;gt;: Mainly UI-related strings (non-context-related) and some minor functional constants&lt;br /&gt;
* Composable functions for the main drop down menu&lt;br /&gt;
* Composable functions for the theme&lt;br /&gt;
* Composable functions for hyperlinks, text edit fields, headline&lt;br /&gt;
* &amp;lt;code&amp;gt;ViewNavigationRoutes&amp;lt;/code&amp;gt;: Object containing the routing information processed in main &amp;lt;code&amp;gt;app&amp;lt;/code&amp;gt; module&lt;br /&gt;
* &amp;lt;code&amp;gt;ScreenshotTestHelper&amp;lt;/code&amp;gt;: Helper class for executing the instrumented snapshot testing using the emulator.&lt;br /&gt;
&lt;br /&gt;
=== Layer structure ===&lt;br /&gt;
The code is separated into the following layers:&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
[module]/&lt;br /&gt;
&lt;br /&gt;
├── src/main/kotlin/com/laimburggasse/aquariumcontrol/[module]/data/&lt;br /&gt;
&lt;br /&gt;
│   ├── remote/                   // Folder for data retrieval functionality&lt;br /&gt;
&lt;br /&gt;
│   │   └── dto/                  // Folder for Data Transfer Objects&lt;br /&gt;
&lt;br /&gt;
│   │       └── [module][name]Import.kt&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module][name]Reader.kt      // Implementation of GET server request&lt;br /&gt;
&lt;br /&gt;
│   ├── [module][name]RepositoryImpl.kt  // Repositories (implementation of interface)&lt;br /&gt;
&lt;br /&gt;
├── src/main/kotlin/com/laimburggasse/aquariumcontrol/[module]/domain/&lt;br /&gt;
&lt;br /&gt;
│   ├── model/                    // Folder for data definition classes&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module][name].kt.    // Classes for data definition&lt;br /&gt;
&lt;br /&gt;
│   ├── repository/               // Folder containing repository interface definitions&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module][name]Repository.kt.  // Interface defining the repository, used by view model&lt;br /&gt;
&lt;br /&gt;
├── src/main/kotlin/com/laimburggasse/aquariumcontrol/[module]/presentation/&lt;br /&gt;
&lt;br /&gt;
│   ├── entry/                    // First screen of feature&lt;br /&gt;
&lt;br /&gt;
│   ├── [components]/             // Shared code between different screens&lt;br /&gt;
&lt;br /&gt;
│   ├── [screen name]/            // Folder for each screen&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module][name]Screen.kt       // Composable showing the complete screen&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module][name]Composable.kt   // Composable showing elements inside the screen&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module][name]UiEvent.kt      // Sealed interface describing the communication from the composable to the view model&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module][name]UiState.kt      // Data class describing the content to be rendered by the composable&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module][name]ViewModel.kt    // Class implementing the view model&lt;br /&gt;
&lt;br /&gt;
│   ├── navigation/               // Folder containing class and functionality related to screen navigation&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module]NavigationEvent.kt  // One sealed class per module containing the navigation intent of the user&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module]Navigator.kt  // Module-specific navigation functionality&lt;br /&gt;
&lt;br /&gt;
│&lt;br /&gt;
└── build.gradle                  // Plugins, setting, dependencies&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Screen navigation ===&lt;br /&gt;
The screen navigation is controlled by a &amp;lt;code&amp;gt;NavHostController&amp;lt;/code&amp;gt; which is created in &amp;lt;code&amp;gt;MainActivity&amp;lt;/code&amp;gt; and remembered using &amp;lt;code&amp;gt;rememberNavController&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
==== Modular navigation ====&lt;br /&gt;
Each module adds its own navigation function to &amp;lt;code&amp;gt;NavHostController&amp;lt;/code&amp;gt;.&lt;br /&gt;
These extension functions are stored in &amp;lt;code&amp;gt;[module]/presentation/navigation/[module]Navigator.kt&amp;lt;/code&amp;gt;.&lt;br /&gt;
They encapsulate the routing logic for that specific module.&lt;br /&gt;
&lt;br /&gt;
==== Event-Driven Navigation ====&lt;br /&gt;
The functions receive as parameter a specific sealed class &amp;lt;code&amp;gt;[module]NavigationEvent&amp;lt;/code&amp;gt;.&lt;br /&gt;
These sealed classes contain objects for each navigation event inside the module.&lt;br /&gt;
&lt;br /&gt;
==== Implementation pattern ====&lt;br /&gt;
The navigation functions are used in the following locations:&lt;br /&gt;
* inside the lambda function passed to &amp;lt;code&amp;gt;NavHost&amp;lt;/code&amp;gt; in &amp;lt;code&amp;gt;MainActivity&amp;lt;/code&amp;gt; for each screen.&lt;br /&gt;
* inside composables themselves being passed as lambda function from within &amp;lt;code&amp;gt;MainActivity&amp;lt;/code&amp;gt;&lt;br /&gt;
:* The composable remain unaware of &amp;lt;code&amp;gt;NavHostController&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Routes and arguments ====&lt;br /&gt;
Source and destination of the routing is defined using strings from the object &amp;lt;code&amp;gt;ViewNavigationRoutes&amp;lt;/code&amp;gt; where data that needs to be transferred between screens is embedded in the strings.&lt;br /&gt;
&lt;br /&gt;
== Example workflow for adding API communication ==&lt;br /&gt;
=== Implementation ===&lt;br /&gt;
Create the model: &amp;lt;code&amp;gt;RefillState&amp;lt;/code&amp;gt; as &amp;lt;code&amp;gt;enum&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Add a reader &amp;lt;code&amp;gt;class&amp;lt;/code&amp;gt; &amp;lt;code&amp;gt;RefillStateReader&amp;lt;/code&amp;gt; which uses the injected interface (Retrofit)&lt;br /&gt;
&lt;br /&gt;
Update the &amp;lt;code&amp;gt;RefillStateApiService&amp;lt;/code&amp;gt;: Add a function to load the state via Retrofit&lt;br /&gt;
&lt;br /&gt;
Add the label for the JSON property in &amp;lt;code&amp;gt;WebApiParams&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Add the URL for the end point in &amp;lt;code&amp;gt;WebApiUrls&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Add a wrapper &amp;lt;code&amp;gt;RefillStateResponse&amp;lt;/code&amp;gt; for the model to allow Retrofit parsing the JSON&lt;br /&gt;
&lt;br /&gt;
Update the repository interface and the repository interface implementation:&lt;br /&gt;
* Add a flow that delivers a &amp;lt;code&amp;gt;DataFetchResult&amp;lt;/code&amp;gt; of the &amp;lt;code&amp;gt;RefillState&amp;lt;/code&amp;gt;&lt;br /&gt;
* Add a function that allows the view model to trigger a refresh&lt;br /&gt;
&lt;br /&gt;
Add a wrapper &amp;lt;code&amp;gt;class&amp;lt;/code&amp;gt; &amp;lt;code&amp;gt;RefillStateUiState&amp;lt;/code&amp;gt;&lt;br /&gt;
Update the view model&lt;br /&gt;
* Add a flow that delivers a &amp;lt;code&amp;gt;RefillStateUiState&amp;lt;/code&amp;gt; based on the &amp;lt;code&amp;gt;DataFetchResult&amp;lt;/code&amp;gt; from repository&lt;br /&gt;
* Handle the new UI event for refreshing&lt;br /&gt;
&lt;br /&gt;
Add composable &amp;lt;code&amp;gt;RefillStateComposable&amp;lt;/code&amp;gt; for displaying the &amp;lt;code&amp;gt;RefillState&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Update &amp;lt;code&amp;gt;RefillUiEvent&amp;lt;/code&amp;gt; with an object for the refresh&lt;br /&gt;
&lt;br /&gt;
Update &amp;lt;code&amp;gt;RefillScreen&amp;lt;/code&amp;gt;:&lt;br /&gt;
* additional parameter (&amp;lt;code&amp;gt;RefillStateUiState&amp;lt;/code&amp;gt;) in composable function and in previews&lt;br /&gt;
* Added &amp;lt;code&amp;gt;PullToRefreshBox&amp;lt;/code&amp;gt;&lt;br /&gt;
* Added parsing of &amp;lt;code&amp;gt;RefillUiState&amp;lt;/code&amp;gt; and display of either&lt;br /&gt;
:* &amp;lt;code&amp;gt;RefillStateComposable&amp;lt;/code&amp;gt; in case of success&lt;br /&gt;
:* Text-based error message in case of error&lt;br /&gt;
:* &amp;lt;code&amp;gt;CircularProgressIndicator&amp;lt;/code&amp;gt; in case of loading&lt;br /&gt;
&lt;br /&gt;
Inject &amp;lt;code&amp;gt;RefillUiState&amp;lt;/code&amp;gt; into &amp;lt;code&amp;gt;RefillScreen&amp;lt;/code&amp;gt; in the &amp;lt;code&amp;gt;MainActivity&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;RefillScreenTest&amp;lt;/code&amp;gt; as well as in previews of &amp;lt;code&amp;gt;RefillScreen&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
(Optional) Check if &amp;lt;code&amp;gt;RefillDataModule&amp;lt;/code&amp;gt; needs update (not required in this case)&lt;br /&gt;
&lt;br /&gt;
=== Testing ===&lt;br /&gt;
Update existing screenshot testing (compose screenshot testing and instrumented tests)&lt;br /&gt;
&lt;br /&gt;
Add compose screenshot testing for new composable&lt;br /&gt;
&lt;br /&gt;
== Test strategy ==&lt;br /&gt;
The app uses different testing approaches:&lt;br /&gt;
* Linting&lt;br /&gt;
* Unit tests&lt;br /&gt;
* Compose preview screenshot testing&lt;br /&gt;
* Screenshot tests running as instrumented tests on emulator&lt;br /&gt;
&lt;br /&gt;
=== Linting ===&lt;br /&gt;
Run &amp;lt;code&amp;gt;./gradlew lint&amp;lt;/code&amp;gt;: This command shall yield no warnings.&lt;br /&gt;
Use the Android Studio code inspection for more detailed linting - some of the warnings cannot be resolved. &lt;br /&gt;
&lt;br /&gt;
=== Compose preview screenshot tests ===&lt;br /&gt;
These tests run comparably fast and device-independent.&lt;br /&gt;
&lt;br /&gt;
The compose preview screenshot tests cover portrait mode/landscape mode, light/dark mode (four variants).&lt;br /&gt;
&lt;br /&gt;
Limitations:&lt;br /&gt;
* Two tests within &amp;lt;code&amp;gt;feed&amp;lt;/code&amp;gt; module can only run in landscape mode (&amp;lt;code&amp;gt;FeedProfileScheduleScreenTest&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;FeedHistoryScreenTest&amp;lt;/code&amp;gt;) due to excessive heap memory utilisation.&lt;br /&gt;
* &amp;lt;code&amp;gt;Info&amp;lt;/code&amp;gt; screen is not tested due to &amp;lt;code&amp;gt;LibrariesContainer&amp;lt;/code&amp;gt; (dynamic content which cannot be mocked).&lt;br /&gt;
&lt;br /&gt;
The tests are located in &amp;lt;code&amp;gt;[module]/src/screenshotTest/kotlin/com/aquariumcontrol/[module]/&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
The references are located in &amp;lt;code&amp;gt;[module]/src/screenshotTestDebug/kotlin/com/aquariumcontrol/[module]/&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
The test reports are located in &amp;lt;code&amp;gt;build/reports/screenshotTest/preview/debug/&amp;lt;/code&amp;gt;. The test reports for the compose screenshot tests contain reference, actual and delta images.&lt;br /&gt;
&lt;br /&gt;
The images are generated with &amp;lt;code&amp;gt;./gradlew [module]:updateDebugScreenshotTest&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
The images are validated with &amp;lt;code&amp;gt;./gradlew [module]:validateDebugScreenshotTest&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Omitting the module will execute all tests.&lt;br /&gt;
&lt;br /&gt;
=== Instrumented tests ===&lt;br /&gt;
The tests run comparably long and are bound to an emulator device.&lt;br /&gt;
As of November 2025, a Pixel 5 API S is used for the instrumented test.&lt;br /&gt;
The instrumented tests contain&lt;br /&gt;
* screenshot testing for the complete screens (&amp;lt;code&amp;gt;ScreenNameTest&amp;lt;/code&amp;gt;)&lt;br /&gt;
* verification if the composable triggers the right event when tapping on buttons bound to navigation to another screen (&amp;lt;code&amp;gt;ScreenNameUITest&amp;lt;/code&amp;gt;)&lt;br /&gt;
&lt;br /&gt;
Limitations:&lt;br /&gt;
* &amp;lt;code&amp;gt;Info&amp;lt;/code&amp;gt; screen is not tested due to &amp;lt;code&amp;gt;LibrariesContainer&amp;lt;/code&amp;gt; (dynamic content which cannot be mocked and also does not allow execution of instrumented test).&lt;br /&gt;
&lt;br /&gt;
The test reports are located in &amp;lt;code&amp;gt;build/reports/androidTests/connected/debug/&amp;lt;/code&amp;gt;. The test reports for the compose screenshot tests do not contain any images.&lt;br /&gt;
&lt;br /&gt;
The tests are executed with &amp;lt;code&amp;gt;./gradlew [module]:connectedDebugAndroidTest&amp;lt;/code&amp;gt;.&lt;br /&gt;
Omitting the module will execute all tests.&lt;br /&gt;
&lt;br /&gt;
A switch called &amp;lt;code&amp;gt;recordMode&amp;lt;/code&amp;gt; inside each test class for screenshot testing determines if the image is generated &amp;lt;code&amp;gt;(true)&amp;lt;/code&amp;gt; or if the image is validated &amp;lt;code&amp;gt;(false)&amp;lt;/code&amp;gt;. The images are generated on the device &amp;lt;code&amp;gt;(/data/data/com.laimburggasse.aquariumcontrol.[module].test/files/screenshots_output/)&amp;lt;/code&amp;gt; and need to be downloaded to the repository.&lt;br /&gt;
&lt;br /&gt;
=== Unit tests ===&lt;br /&gt;
As of April 2026, unit tests are developed for:&lt;br /&gt;
* all view models&lt;br /&gt;
* all repositories&lt;br /&gt;
&lt;br /&gt;
The unit test cases for the view models make extensive use of the &amp;lt;code&amp;gt;MockK&amp;lt;/code&amp;gt; library.&lt;br /&gt;
&lt;br /&gt;
The unit test cases for the repositories use mock implementations of the &amp;lt;code&amp;gt;Retrofit&amp;lt;/code&amp;gt; interface.&lt;br /&gt;
&lt;br /&gt;
All unit test cases are executed using the command: &amp;lt;code&amp;gt;./gradlew :[module]:testDebugUnitTest&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The code coverage report can be created using: &amp;lt;code&amp;gt;./gradlew createDebugUnitTestCoverageReport&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The gradle task removes the injected code from the test coverage report: &amp;lt;code&amp;gt;./gradlew jacocoTestReport&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The report is stored in &amp;lt;code&amp;gt;[module]/build/reports/jacoco/jacocoTestReport/html&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Release procedure ==&lt;br /&gt;
# Update &amp;lt;code&amp;gt;versionCode&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;versionName&amp;lt;/code&amp;gt; in &amp;lt;code&amp;gt;build.gradle.kts&amp;lt;/code&amp;gt; of &amp;lt;code&amp;gt;app&amp;lt;/code&amp;gt; module.&lt;br /&gt;
# Create a release branch (&amp;lt;code&amp;gt;release/[version]&amp;lt;/code&amp;gt;), checkout, commit and push.&lt;br /&gt;
# Change to release build variant.&lt;br /&gt;
# Build&lt;br /&gt;
# Generate a signed App bundle&lt;br /&gt;
# Upload to play store&lt;/div&gt;</summary>
		<author><name>Uwe</name></author>
	</entry>
	<entry>
		<id>http://217.79.180.177/mediawiki/index.php?title=App_for_Apple_mobile_devices&amp;diff=489</id>
		<title>App for Apple mobile devices</title>
		<link rel="alternate" type="text/html" href="http://217.79.180.177/mediawiki/index.php?title=App_for_Apple_mobile_devices&amp;diff=489"/>
		<updated>2026-05-10T11:39:33Z</updated>

		<summary type="html">&lt;p&gt;Uwe: /* Build */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Architecture =&lt;br /&gt;
Description follows.&lt;br /&gt;
&lt;br /&gt;
= Testing =&lt;br /&gt;
Description follows.&lt;br /&gt;
&lt;br /&gt;
= Release procedure =&lt;br /&gt;
== Versioning ==&lt;br /&gt;
Update the version number in General tab.&lt;br /&gt;
&lt;br /&gt;
== Branching ==&lt;br /&gt;
Create a branch for the release and check it out:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;git branch release/20260510_01&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;git checkout release/20260510_01&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Edit Scheme ==&lt;br /&gt;
Go to Product &amp;gt; Scheme &amp;gt; Edit Scheme&lt;br /&gt;
&lt;br /&gt;
Select Archive on the left.&lt;br /&gt;
&lt;br /&gt;
Ensure the Build Configuration is set to Release.&lt;br /&gt;
&lt;br /&gt;
== Signing &amp;amp; Capabilities ==&lt;br /&gt;
Ensure signing is activated and account is logged in.&lt;br /&gt;
&lt;br /&gt;
== Frameworks, Libraries, and Embedded Content ==&lt;br /&gt;
Remove PreviewSnapshots&lt;br /&gt;
&lt;br /&gt;
== Build ==&lt;br /&gt;
Execute the following steps inside Xcode:&lt;br /&gt;
&lt;br /&gt;
=== Setup the version number ===&lt;br /&gt;
[[File:XCode_setup_version_number.png|thumb|center|Setup the version number]]&lt;br /&gt;
&lt;br /&gt;
=== Change Build Configuration ===&lt;br /&gt;
[[File:XCode_Change_Build_Configuration.png|thumb|center|Change Build Configuration]]&lt;br /&gt;
&lt;br /&gt;
=== Login for signing ===&lt;br /&gt;
[[File:XCode_Login_for_signing.png|thumb|center|Login for signing]]&lt;br /&gt;
&lt;br /&gt;
=== Remove preview snapshot dependency ===&lt;br /&gt;
[[File:XCode_Removew_PreviewSnapshot.png|thumb|center|Remove preview snapshot dependency]]&lt;br /&gt;
&lt;br /&gt;
=== Change build target ===&lt;br /&gt;
[[File:XCode_Change_Build_Target.png|thumb|center|Change build target]]&lt;br /&gt;
&lt;br /&gt;
=== Start build ===&lt;br /&gt;
[[File:XCode_Start_Build.png|thumb|center|Start build]]&lt;br /&gt;
&lt;br /&gt;
=== After build ===&lt;br /&gt;
[[File:XCode_After_Build.png|thumb|center|After build]]&lt;br /&gt;
&lt;br /&gt;
=== Validate build ===&lt;br /&gt;
[[File:.png|thumb|center|Validate build]]&lt;br /&gt;
&lt;br /&gt;
=== Validated build ===&lt;br /&gt;
[[File:XCode_Validate_Build.png|thumb|center|Validated build]]&lt;br /&gt;
&lt;br /&gt;
=== Distribute App ===&lt;br /&gt;
[[File:XCode_Distribute_App.png|thumb|center|Distribute App]]&lt;br /&gt;
&lt;br /&gt;
=== Uploading App ===&lt;br /&gt;
[[File:XCode_Uploading_App.png|thumb|center|Uploading App]]&lt;br /&gt;
&lt;br /&gt;
=== Uploaded App ===&lt;br /&gt;
[[File:XCode_Uploaded_App.png|thumb|center|Uploaded App]]&lt;/div&gt;</summary>
		<author><name>Uwe</name></author>
	</entry>
	<entry>
		<id>http://217.79.180.177/mediawiki/index.php?title=App_for_Apple_mobile_devices&amp;diff=488</id>
		<title>App for Apple mobile devices</title>
		<link rel="alternate" type="text/html" href="http://217.79.180.177/mediawiki/index.php?title=App_for_Apple_mobile_devices&amp;diff=488"/>
		<updated>2026-05-10T11:34:16Z</updated>

		<summary type="html">&lt;p&gt;Uwe: /* Build */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Architecture =&lt;br /&gt;
Description follows.&lt;br /&gt;
&lt;br /&gt;
= Testing =&lt;br /&gt;
Description follows.&lt;br /&gt;
&lt;br /&gt;
= Release procedure =&lt;br /&gt;
== Versioning ==&lt;br /&gt;
Update the version number in General tab.&lt;br /&gt;
&lt;br /&gt;
== Branching ==&lt;br /&gt;
Create a branch for the release and check it out:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;git branch release/20260510_01&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;git checkout release/20260510_01&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Edit Scheme ==&lt;br /&gt;
Go to Product &amp;gt; Scheme &amp;gt; Edit Scheme&lt;br /&gt;
&lt;br /&gt;
Select Archive on the left.&lt;br /&gt;
&lt;br /&gt;
Ensure the Build Configuration is set to Release.&lt;br /&gt;
&lt;br /&gt;
== Signing &amp;amp; Capabilities ==&lt;br /&gt;
Ensure signing is activated and account is logged in.&lt;br /&gt;
&lt;br /&gt;
== Frameworks, Libraries, and Embedded Content ==&lt;br /&gt;
Remove PreviewSnapshots&lt;br /&gt;
&lt;br /&gt;
== Build ==&lt;br /&gt;
Execute the following steps inside Xcode:&lt;br /&gt;
&lt;br /&gt;
=== Setup the version number ===&lt;br /&gt;
[[File:XCode_setup_version_number.png|thumb|center|Setup the version number]]&lt;br /&gt;
&lt;br /&gt;
=== Change Build Configuration ===&lt;br /&gt;
[[File:XCode_Change_Build_Configuration.png|thumb|center|Change Build Configuration]]&lt;/div&gt;</summary>
		<author><name>Uwe</name></author>
	</entry>
	<entry>
		<id>http://217.79.180.177/mediawiki/index.php?title=App_for_Apple_mobile_devices&amp;diff=487</id>
		<title>App for Apple mobile devices</title>
		<link rel="alternate" type="text/html" href="http://217.79.180.177/mediawiki/index.php?title=App_for_Apple_mobile_devices&amp;diff=487"/>
		<updated>2026-05-10T11:32:09Z</updated>

		<summary type="html">&lt;p&gt;Uwe: /* Build */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Architecture =&lt;br /&gt;
Description follows.&lt;br /&gt;
&lt;br /&gt;
= Testing =&lt;br /&gt;
Description follows.&lt;br /&gt;
&lt;br /&gt;
= Release procedure =&lt;br /&gt;
== Versioning ==&lt;br /&gt;
Update the version number in General tab.&lt;br /&gt;
&lt;br /&gt;
== Branching ==&lt;br /&gt;
Create a branch for the release and check it out:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;git branch release/20260510_01&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;git checkout release/20260510_01&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Edit Scheme ==&lt;br /&gt;
Go to Product &amp;gt; Scheme &amp;gt; Edit Scheme&lt;br /&gt;
&lt;br /&gt;
Select Archive on the left.&lt;br /&gt;
&lt;br /&gt;
Ensure the Build Configuration is set to Release.&lt;br /&gt;
&lt;br /&gt;
== Signing &amp;amp; Capabilities ==&lt;br /&gt;
Ensure signing is activated and account is logged in.&lt;br /&gt;
&lt;br /&gt;
== Frameworks, Libraries, and Embedded Content ==&lt;br /&gt;
Remove PreviewSnapshots&lt;br /&gt;
&lt;br /&gt;
== Build ==&lt;br /&gt;
Execute the following steps inside Xcode:&lt;br /&gt;
&lt;br /&gt;
=== Setup the version number ===&lt;br /&gt;
[[File:XCode_setup_version_number.png|thumb|Setup the version number]]&lt;br /&gt;
=== Change Build Configuration ===&lt;br /&gt;
[[File:XCode_Change_Build_Configuration.png|thumb|Change Build Configuration]]&lt;/div&gt;</summary>
		<author><name>Uwe</name></author>
	</entry>
	<entry>
		<id>http://217.79.180.177/mediawiki/index.php?title=App_for_Apple_mobile_devices&amp;diff=486</id>
		<title>App for Apple mobile devices</title>
		<link rel="alternate" type="text/html" href="http://217.79.180.177/mediawiki/index.php?title=App_for_Apple_mobile_devices&amp;diff=486"/>
		<updated>2026-05-10T11:31:32Z</updated>

		<summary type="html">&lt;p&gt;Uwe: /* Build */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Architecture =&lt;br /&gt;
Description follows.&lt;br /&gt;
&lt;br /&gt;
= Testing =&lt;br /&gt;
Description follows.&lt;br /&gt;
&lt;br /&gt;
= Release procedure =&lt;br /&gt;
== Versioning ==&lt;br /&gt;
Update the version number in General tab.&lt;br /&gt;
&lt;br /&gt;
== Branching ==&lt;br /&gt;
Create a branch for the release and check it out:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;git branch release/20260510_01&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;git checkout release/20260510_01&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Edit Scheme ==&lt;br /&gt;
Go to Product &amp;gt; Scheme &amp;gt; Edit Scheme&lt;br /&gt;
&lt;br /&gt;
Select Archive on the left.&lt;br /&gt;
&lt;br /&gt;
Ensure the Build Configuration is set to Release.&lt;br /&gt;
&lt;br /&gt;
== Signing &amp;amp; Capabilities ==&lt;br /&gt;
Ensure signing is activated and account is logged in.&lt;br /&gt;
&lt;br /&gt;
== Frameworks, Libraries, and Embedded Content ==&lt;br /&gt;
Remove PreviewSnapshots&lt;br /&gt;
&lt;br /&gt;
== Build ==&lt;br /&gt;
Execute the following steps inside Xcode:&lt;br /&gt;
&lt;br /&gt;
* Setup the version number&lt;br /&gt;
[[File:XCode_setup_version_number.png|thumb|Setup the version number]]&lt;br /&gt;
* Change Build Configuration&lt;br /&gt;
[[File:XCode_Change_Build_Configuration.png|thumb|Change Build Configuration]]&lt;/div&gt;</summary>
		<author><name>Uwe</name></author>
	</entry>
	<entry>
		<id>http://217.79.180.177/mediawiki/index.php?title=App_for_Apple_mobile_devices&amp;diff=485</id>
		<title>App for Apple mobile devices</title>
		<link rel="alternate" type="text/html" href="http://217.79.180.177/mediawiki/index.php?title=App_for_Apple_mobile_devices&amp;diff=485"/>
		<updated>2026-05-10T11:29:54Z</updated>

		<summary type="html">&lt;p&gt;Uwe: /* Build */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Architecture =&lt;br /&gt;
Description follows.&lt;br /&gt;
&lt;br /&gt;
= Testing =&lt;br /&gt;
Description follows.&lt;br /&gt;
&lt;br /&gt;
= Release procedure =&lt;br /&gt;
== Versioning ==&lt;br /&gt;
Update the version number in General tab.&lt;br /&gt;
&lt;br /&gt;
== Branching ==&lt;br /&gt;
Create a branch for the release and check it out:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;git branch release/20260510_01&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;git checkout release/20260510_01&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Edit Scheme ==&lt;br /&gt;
Go to Product &amp;gt; Scheme &amp;gt; Edit Scheme&lt;br /&gt;
&lt;br /&gt;
Select Archive on the left.&lt;br /&gt;
&lt;br /&gt;
Ensure the Build Configuration is set to Release.&lt;br /&gt;
&lt;br /&gt;
== Signing &amp;amp; Capabilities ==&lt;br /&gt;
Ensure signing is activated and account is logged in.&lt;br /&gt;
&lt;br /&gt;
== Frameworks, Libraries, and Embedded Content ==&lt;br /&gt;
Remove PreviewSnapshots&lt;br /&gt;
&lt;br /&gt;
== Build ==&lt;br /&gt;
Execute the following steps inside Xcode:&lt;br /&gt;
&lt;br /&gt;
* Setup the version number&lt;/div&gt;</summary>
		<author><name>Uwe</name></author>
	</entry>
	<entry>
		<id>http://217.79.180.177/mediawiki/index.php?title=File:XCode_Uploaded_App.png&amp;diff=484</id>
		<title>File:XCode Uploaded App.png</title>
		<link rel="alternate" type="text/html" href="http://217.79.180.177/mediawiki/index.php?title=File:XCode_Uploaded_App.png&amp;diff=484"/>
		<updated>2026-05-10T11:28:42Z</updated>

		<summary type="html">&lt;p&gt;Uwe: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;/div&gt;</summary>
		<author><name>Uwe</name></author>
	</entry>
	<entry>
		<id>http://217.79.180.177/mediawiki/index.php?title=File:XCode_Uploading_App.png&amp;diff=483</id>
		<title>File:XCode Uploading App.png</title>
		<link rel="alternate" type="text/html" href="http://217.79.180.177/mediawiki/index.php?title=File:XCode_Uploading_App.png&amp;diff=483"/>
		<updated>2026-05-10T11:28:22Z</updated>

		<summary type="html">&lt;p&gt;Uwe: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;/div&gt;</summary>
		<author><name>Uwe</name></author>
	</entry>
	<entry>
		<id>http://217.79.180.177/mediawiki/index.php?title=File:XCode_Distribute_App.png&amp;diff=482</id>
		<title>File:XCode Distribute App.png</title>
		<link rel="alternate" type="text/html" href="http://217.79.180.177/mediawiki/index.php?title=File:XCode_Distribute_App.png&amp;diff=482"/>
		<updated>2026-05-10T11:28:04Z</updated>

		<summary type="html">&lt;p&gt;Uwe: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;/div&gt;</summary>
		<author><name>Uwe</name></author>
	</entry>
	<entry>
		<id>http://217.79.180.177/mediawiki/index.php?title=File:XCode_Validated_Build.png&amp;diff=481</id>
		<title>File:XCode Validated Build.png</title>
		<link rel="alternate" type="text/html" href="http://217.79.180.177/mediawiki/index.php?title=File:XCode_Validated_Build.png&amp;diff=481"/>
		<updated>2026-05-10T11:27:46Z</updated>

		<summary type="html">&lt;p&gt;Uwe: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;/div&gt;</summary>
		<author><name>Uwe</name></author>
	</entry>
	<entry>
		<id>http://217.79.180.177/mediawiki/index.php?title=File:XCode_Validate_Build.png&amp;diff=480</id>
		<title>File:XCode Validate Build.png</title>
		<link rel="alternate" type="text/html" href="http://217.79.180.177/mediawiki/index.php?title=File:XCode_Validate_Build.png&amp;diff=480"/>
		<updated>2026-05-10T11:27:09Z</updated>

		<summary type="html">&lt;p&gt;Uwe: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;/div&gt;</summary>
		<author><name>Uwe</name></author>
	</entry>
	<entry>
		<id>http://217.79.180.177/mediawiki/index.php?title=File:XCode_After_Build.png&amp;diff=479</id>
		<title>File:XCode After Build.png</title>
		<link rel="alternate" type="text/html" href="http://217.79.180.177/mediawiki/index.php?title=File:XCode_After_Build.png&amp;diff=479"/>
		<updated>2026-05-10T11:26:56Z</updated>

		<summary type="html">&lt;p&gt;Uwe: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;/div&gt;</summary>
		<author><name>Uwe</name></author>
	</entry>
	<entry>
		<id>http://217.79.180.177/mediawiki/index.php?title=File:XCode_Start_Build.png&amp;diff=478</id>
		<title>File:XCode Start Build.png</title>
		<link rel="alternate" type="text/html" href="http://217.79.180.177/mediawiki/index.php?title=File:XCode_Start_Build.png&amp;diff=478"/>
		<updated>2026-05-10T11:26:21Z</updated>

		<summary type="html">&lt;p&gt;Uwe: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;/div&gt;</summary>
		<author><name>Uwe</name></author>
	</entry>
	<entry>
		<id>http://217.79.180.177/mediawiki/index.php?title=File:XCode_Change_Build_Target.png&amp;diff=477</id>
		<title>File:XCode Change Build Target.png</title>
		<link rel="alternate" type="text/html" href="http://217.79.180.177/mediawiki/index.php?title=File:XCode_Change_Build_Target.png&amp;diff=477"/>
		<updated>2026-05-10T11:26:03Z</updated>

		<summary type="html">&lt;p&gt;Uwe: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;/div&gt;</summary>
		<author><name>Uwe</name></author>
	</entry>
	<entry>
		<id>http://217.79.180.177/mediawiki/index.php?title=File:XCode_Removew_PreviewSnapshot.png&amp;diff=476</id>
		<title>File:XCode Removew PreviewSnapshot.png</title>
		<link rel="alternate" type="text/html" href="http://217.79.180.177/mediawiki/index.php?title=File:XCode_Removew_PreviewSnapshot.png&amp;diff=476"/>
		<updated>2026-05-10T11:25:48Z</updated>

		<summary type="html">&lt;p&gt;Uwe: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;/div&gt;</summary>
		<author><name>Uwe</name></author>
	</entry>
	<entry>
		<id>http://217.79.180.177/mediawiki/index.php?title=File:XCode_Login_for_signing.png&amp;diff=475</id>
		<title>File:XCode Login for signing.png</title>
		<link rel="alternate" type="text/html" href="http://217.79.180.177/mediawiki/index.php?title=File:XCode_Login_for_signing.png&amp;diff=475"/>
		<updated>2026-05-10T11:25:31Z</updated>

		<summary type="html">&lt;p&gt;Uwe: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;/div&gt;</summary>
		<author><name>Uwe</name></author>
	</entry>
	<entry>
		<id>http://217.79.180.177/mediawiki/index.php?title=File:XCode_Change_Build_Configuration.png&amp;diff=474</id>
		<title>File:XCode Change Build Configuration.png</title>
		<link rel="alternate" type="text/html" href="http://217.79.180.177/mediawiki/index.php?title=File:XCode_Change_Build_Configuration.png&amp;diff=474"/>
		<updated>2026-05-10T11:25:16Z</updated>

		<summary type="html">&lt;p&gt;Uwe: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;/div&gt;</summary>
		<author><name>Uwe</name></author>
	</entry>
	<entry>
		<id>http://217.79.180.177/mediawiki/index.php?title=File:XCode_setup_version_number.png&amp;diff=473</id>
		<title>File:XCode setup version number.png</title>
		<link rel="alternate" type="text/html" href="http://217.79.180.177/mediawiki/index.php?title=File:XCode_setup_version_number.png&amp;diff=473"/>
		<updated>2026-05-10T11:24:57Z</updated>

		<summary type="html">&lt;p&gt;Uwe: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;/div&gt;</summary>
		<author><name>Uwe</name></author>
	</entry>
	<entry>
		<id>http://217.79.180.177/mediawiki/index.php?title=App_for_Apple_mobile_devices&amp;diff=472</id>
		<title>App for Apple mobile devices</title>
		<link rel="alternate" type="text/html" href="http://217.79.180.177/mediawiki/index.php?title=App_for_Apple_mobile_devices&amp;diff=472"/>
		<updated>2026-05-10T07:58:17Z</updated>

		<summary type="html">&lt;p&gt;Uwe: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Architecture =&lt;br /&gt;
Description follows.&lt;br /&gt;
&lt;br /&gt;
= Testing =&lt;br /&gt;
Description follows.&lt;br /&gt;
&lt;br /&gt;
= Release procedure =&lt;br /&gt;
== Versioning ==&lt;br /&gt;
Update the version number in General tab.&lt;br /&gt;
&lt;br /&gt;
== Branching ==&lt;br /&gt;
Create a branch for the release and check it out:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;git branch release/20260510_01&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;git checkout release/20260510_01&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Edit Scheme ==&lt;br /&gt;
Go to Product &amp;gt; Scheme &amp;gt; Edit Scheme&lt;br /&gt;
&lt;br /&gt;
Select Archive on the left.&lt;br /&gt;
&lt;br /&gt;
Ensure the Build Configuration is set to Release.&lt;br /&gt;
&lt;br /&gt;
== Signing &amp;amp; Capabilities ==&lt;br /&gt;
Ensure signing is activated and account is logged in.&lt;br /&gt;
&lt;br /&gt;
== Frameworks, Libraries, and Embedded Content ==&lt;br /&gt;
Remove PreviewSnapshots&lt;br /&gt;
&lt;br /&gt;
== Build ==&lt;br /&gt;
Go to Xcode Cloud and edit the workflow. Use the release branch previously created.&lt;/div&gt;</summary>
		<author><name>Uwe</name></author>
	</entry>
	<entry>
		<id>http://217.79.180.177/mediawiki/index.php?title=App_for_Apple_mobile_devices&amp;diff=471</id>
		<title>App for Apple mobile devices</title>
		<link rel="alternate" type="text/html" href="http://217.79.180.177/mediawiki/index.php?title=App_for_Apple_mobile_devices&amp;diff=471"/>
		<updated>2026-05-10T07:55:20Z</updated>

		<summary type="html">&lt;p&gt;Uwe: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Architecture =&lt;br /&gt;
Description follows.&lt;br /&gt;
&lt;br /&gt;
= Testing =&lt;br /&gt;
Description follows.&lt;br /&gt;
&lt;br /&gt;
= Release procedure =&lt;br /&gt;
== Versioning ==&lt;br /&gt;
Update the version number in General tab.&lt;br /&gt;
&lt;br /&gt;
== Branching ==&lt;br /&gt;
Create a branch for the release and check it out:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;git branch release/20260510_01&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;git checkout release/20260510_01&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Edit Scheme ==&lt;br /&gt;
Go to Product &amp;gt; Scheme &amp;gt; Edit Scheme&lt;br /&gt;
&lt;br /&gt;
Select Archive on the left.&lt;br /&gt;
&lt;br /&gt;
Ensure the Build Configuration is set to Release.&lt;br /&gt;
&lt;br /&gt;
== Signing &amp;amp; Capabilities ==&lt;br /&gt;
Ensure signing is activated and account is logged in.&lt;br /&gt;
&lt;br /&gt;
== Frameworks, Libraries, and Embedded Content ==&lt;br /&gt;
Remove PreviewSnapshots&lt;br /&gt;
&lt;br /&gt;
== Build ==&lt;br /&gt;
Go to report navigator, select the Cloud-tab and log in to your account.&lt;br /&gt;
&lt;br /&gt;
Select AquariumControl and press &amp;quot;Start Build&amp;quot;.&lt;/div&gt;</summary>
		<author><name>Uwe</name></author>
	</entry>
	<entry>
		<id>http://217.79.180.177/mediawiki/index.php?title=App_for_Apple_mobile_devices&amp;diff=470</id>
		<title>App for Apple mobile devices</title>
		<link rel="alternate" type="text/html" href="http://217.79.180.177/mediawiki/index.php?title=App_for_Apple_mobile_devices&amp;diff=470"/>
		<updated>2026-05-10T07:48:48Z</updated>

		<summary type="html">&lt;p&gt;Uwe: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Architecture =&lt;br /&gt;
Description follows.&lt;br /&gt;
&lt;br /&gt;
= Testing =&lt;br /&gt;
Description follows.&lt;br /&gt;
&lt;br /&gt;
= Release procedure =&lt;br /&gt;
== Versioning ==&lt;br /&gt;
Update the version number in General tab.&lt;br /&gt;
&lt;br /&gt;
== Branching ==&lt;br /&gt;
Create a branch for the release and check it out:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;git branch release/20260510_01&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;git checkout release/20260510_01&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Edit Scheme ==&lt;br /&gt;
Go to Product &amp;gt; Scheme &amp;gt; Edit Scheme&lt;br /&gt;
&lt;br /&gt;
Select Archive on the left.&lt;br /&gt;
&lt;br /&gt;
Ensure the Build Configuration is set to Release.&lt;br /&gt;
&lt;br /&gt;
== Signing &amp;amp; Capabilities ==&lt;br /&gt;
Ensure signing is activated and account is logged in.&lt;br /&gt;
&lt;br /&gt;
== Frameworks, Libraries, and Embedded Content ==&lt;br /&gt;
Remove PreviewSnapshots&lt;/div&gt;</summary>
		<author><name>Uwe</name></author>
	</entry>
	<entry>
		<id>http://217.79.180.177/mediawiki/index.php?title=App_for_Apple_mobile_devices&amp;diff=469</id>
		<title>App for Apple mobile devices</title>
		<link rel="alternate" type="text/html" href="http://217.79.180.177/mediawiki/index.php?title=App_for_Apple_mobile_devices&amp;diff=469"/>
		<updated>2026-05-10T07:46:09Z</updated>

		<summary type="html">&lt;p&gt;Uwe: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Architecture =&lt;br /&gt;
Description follows.&lt;br /&gt;
&lt;br /&gt;
= Testing =&lt;br /&gt;
Description follows.&lt;br /&gt;
&lt;br /&gt;
= Release procedure =&lt;br /&gt;
== Versioning ==&lt;br /&gt;
Update the version number in General tab.&lt;br /&gt;
&lt;br /&gt;
== Branching ==&lt;br /&gt;
Create a branch for the release and check it out:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;git branch release/20260510_01&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;git checkout release/20260510_01&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Edit Scheme ==&lt;br /&gt;
Go to Product &amp;gt; Scheme &amp;gt; Edit Scheme&lt;br /&gt;
&lt;br /&gt;
Select Archive on the left.&lt;br /&gt;
&lt;br /&gt;
Ensure the Build Configuration is set to Release.&lt;br /&gt;
&lt;br /&gt;
== Signing &amp;amp; Capabilities ==&lt;br /&gt;
Ensure signing is activated and account is logged in.&lt;/div&gt;</summary>
		<author><name>Uwe</name></author>
	</entry>
	<entry>
		<id>http://217.79.180.177/mediawiki/index.php?title=App_for_Apple_mobile_devices&amp;diff=468</id>
		<title>App for Apple mobile devices</title>
		<link rel="alternate" type="text/html" href="http://217.79.180.177/mediawiki/index.php?title=App_for_Apple_mobile_devices&amp;diff=468"/>
		<updated>2026-05-10T07:45:58Z</updated>

		<summary type="html">&lt;p&gt;Uwe: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Architecture =&lt;br /&gt;
Description follows.&lt;br /&gt;
&lt;br /&gt;
= Testing =&lt;br /&gt;
Description follows.&lt;br /&gt;
&lt;br /&gt;
= Release procedure =&lt;br /&gt;
== Versioning ==&lt;br /&gt;
Update the version number in General tab.&lt;br /&gt;
&lt;br /&gt;
== Branching ==&lt;br /&gt;
Create a branch for the release and check it out:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;git branch release/20260510_01&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;git checkout release/20260510_01&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Edit Scheme ==&lt;br /&gt;
Go to Product &amp;gt; Scheme &amp;gt; Edit Scheme&lt;br /&gt;
&lt;br /&gt;
Select Archive on the left.&lt;br /&gt;
&lt;br /&gt;
Ensure the Build Configuration is set to Release.&lt;br /&gt;
&lt;br /&gt;
== Signing &amp;amp; Capabilities&lt;br /&gt;
Ensure signing is activated and account is logged in.&lt;/div&gt;</summary>
		<author><name>Uwe</name></author>
	</entry>
	<entry>
		<id>http://217.79.180.177/mediawiki/index.php?title=App_for_Apple_mobile_devices&amp;diff=467</id>
		<title>App for Apple mobile devices</title>
		<link rel="alternate" type="text/html" href="http://217.79.180.177/mediawiki/index.php?title=App_for_Apple_mobile_devices&amp;diff=467"/>
		<updated>2026-05-10T07:41:27Z</updated>

		<summary type="html">&lt;p&gt;Uwe: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Architecture =&lt;br /&gt;
Description follows.&lt;br /&gt;
&lt;br /&gt;
= Testing =&lt;br /&gt;
Description follows.&lt;br /&gt;
&lt;br /&gt;
= Release procedure =&lt;br /&gt;
== Versioning ==&lt;br /&gt;
Update the version number in General tab.&lt;br /&gt;
&lt;br /&gt;
== Branching ==&lt;br /&gt;
Create a branch for the release and check it out:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;git branch release/20260510_01&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;git checkout release/20260510_01&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Edit Scheme ==&lt;br /&gt;
Go to Product &amp;gt; Scheme &amp;gt; Edit Scheme&lt;br /&gt;
&lt;br /&gt;
Select Archive on the left.&lt;br /&gt;
&lt;br /&gt;
Ensure the Build Configuration is set to Release.&lt;/div&gt;</summary>
		<author><name>Uwe</name></author>
	</entry>
	<entry>
		<id>http://217.79.180.177/mediawiki/index.php?title=App_for_Apple_mobile_devices&amp;diff=466</id>
		<title>App for Apple mobile devices</title>
		<link rel="alternate" type="text/html" href="http://217.79.180.177/mediawiki/index.php?title=App_for_Apple_mobile_devices&amp;diff=466"/>
		<updated>2026-05-10T07:39:43Z</updated>

		<summary type="html">&lt;p&gt;Uwe: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Architecture =&lt;br /&gt;
Description follows.&lt;br /&gt;
&lt;br /&gt;
= Testing =&lt;br /&gt;
Description follows.&lt;br /&gt;
&lt;br /&gt;
= Release procedure =&lt;br /&gt;
Update the version number in General tab.&lt;br /&gt;
&lt;br /&gt;
Create a branch for the release and check it out:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;git branch release/20260510_01&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;git checkout release/20260510_01&amp;lt;/code&amp;gt;&lt;/div&gt;</summary>
		<author><name>Uwe</name></author>
	</entry>
	<entry>
		<id>http://217.79.180.177/mediawiki/index.php?title=App_for_Apple_mobile_devices&amp;diff=465</id>
		<title>App for Apple mobile devices</title>
		<link rel="alternate" type="text/html" href="http://217.79.180.177/mediawiki/index.php?title=App_for_Apple_mobile_devices&amp;diff=465"/>
		<updated>2026-05-10T07:39:34Z</updated>

		<summary type="html">&lt;p&gt;Uwe: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Architecture =&lt;br /&gt;
Description follows.&lt;br /&gt;
&lt;br /&gt;
= Testing =&lt;br /&gt;
Description follows.&lt;br /&gt;
&lt;br /&gt;
= Release procedure =&lt;br /&gt;
Update the version number in General tab.&lt;br /&gt;
&lt;br /&gt;
Create a branch for the release and check it out:&lt;br /&gt;
&amp;lt;code&amp;gt;git branch release/20260510_01&amp;lt;/code&amp;gt;&lt;br /&gt;
&amp;lt;code&amp;gt;git checkout release/20260510_01&amp;lt;/code&amp;gt;&lt;/div&gt;</summary>
		<author><name>Uwe</name></author>
	</entry>
	<entry>
		<id>http://217.79.180.177/mediawiki/index.php?title=App_for_Apple_mobile_devices&amp;diff=464</id>
		<title>App for Apple mobile devices</title>
		<link rel="alternate" type="text/html" href="http://217.79.180.177/mediawiki/index.php?title=App_for_Apple_mobile_devices&amp;diff=464"/>
		<updated>2026-05-10T07:39:22Z</updated>

		<summary type="html">&lt;p&gt;Uwe: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Architecture =&lt;br /&gt;
Description follows.&lt;br /&gt;
&lt;br /&gt;
= Testing =&lt;br /&gt;
Description follows.&lt;br /&gt;
&lt;br /&gt;
= Release procedure =&lt;br /&gt;
Update the version number in General tab.&lt;br /&gt;
&lt;br /&gt;
Create a branch for the release and check it out:&lt;br /&gt;
&amp;lt;code&amp;gt;git branch release/20260510_01&lt;br /&gt;
git checkout release/20260510_01&amp;lt;/code&amp;gt;&lt;/div&gt;</summary>
		<author><name>Uwe</name></author>
	</entry>
	<entry>
		<id>http://217.79.180.177/mediawiki/index.php?title=App_for_Apple_mobile_devices&amp;diff=463</id>
		<title>App for Apple mobile devices</title>
		<link rel="alternate" type="text/html" href="http://217.79.180.177/mediawiki/index.php?title=App_for_Apple_mobile_devices&amp;diff=463"/>
		<updated>2026-05-09T18:18:05Z</updated>

		<summary type="html">&lt;p&gt;Uwe: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Architecture =&lt;br /&gt;
Description follows.&lt;br /&gt;
&lt;br /&gt;
= Testing =&lt;br /&gt;
Description follows.&lt;br /&gt;
&lt;br /&gt;
= Release procedure =&lt;/div&gt;</summary>
		<author><name>Uwe</name></author>
	</entry>
	<entry>
		<id>http://217.79.180.177/mediawiki/index.php?title=App_for_Apple_mobile_devices&amp;diff=462</id>
		<title>App for Apple mobile devices</title>
		<link rel="alternate" type="text/html" href="http://217.79.180.177/mediawiki/index.php?title=App_for_Apple_mobile_devices&amp;diff=462"/>
		<updated>2026-05-09T18:16:40Z</updated>

		<summary type="html">&lt;p&gt;Uwe: Created page with &amp;quot;= Architecture = Description follows.  = Testing = Description follows.&amp;quot;&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Architecture =&lt;br /&gt;
Description follows.&lt;br /&gt;
&lt;br /&gt;
= Testing =&lt;br /&gt;
Description follows.&lt;/div&gt;</summary>
		<author><name>Uwe</name></author>
	</entry>
	<entry>
		<id>http://217.79.180.177/mediawiki/index.php?title=Main_Page&amp;diff=461</id>
		<title>Main Page</title>
		<link rel="alternate" type="text/html" href="http://217.79.180.177/mediawiki/index.php?title=Main_Page&amp;diff=461"/>
		<updated>2026-05-09T18:15:30Z</updated>

		<summary type="html">&lt;p&gt;Uwe: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&amp;lt;strong&amp;gt;This is the Aquarium Control developer documentation.&amp;lt;/strong&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Aquarium-Control is a control system for salt-water aquariums.&lt;br /&gt;
&lt;br /&gt;
The main features include:&lt;br /&gt;
* [[Refill control for fresh water]]&lt;br /&gt;
* [[Data acquisition of water temperature, pH and conductivity|Data acquisition of temperature, pH, conductivity]]&lt;br /&gt;
* [[Data acquisition of ambient temperature and humidity|Data acquisition of ambient temperature and humidity]]&lt;br /&gt;
* Temperature control using ventilation fans and heater&lt;br /&gt;
* Automatic feeder&lt;br /&gt;
* Balling mineral dosing&lt;br /&gt;
&lt;br /&gt;
The components of the control system are assembled in one [[control cabinet]].&lt;br /&gt;
&lt;br /&gt;
Aquarium-Control consists of the following SW elements:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
!|Repository name&lt;br /&gt;
!|Description&lt;br /&gt;
!|Programming language&lt;br /&gt;
|-&lt;br /&gt;
|| [https://bitbucket.org/in-dubio/aquariumcontrol-android-mobile-app/ AquariumControl Android Mobile App]&lt;br /&gt;
|| [[Android application]]&lt;br /&gt;
|| Kotlin&lt;br /&gt;
|-&lt;br /&gt;
|| [https://bitbucket.org/in-dubio/aquariumcontrol-api AquariumControl API]&lt;br /&gt;
|| [[REST API]]&lt;br /&gt;
|| php&lt;br /&gt;
|-&lt;br /&gt;
|| [https://bitbucket.org/in-dubio/aquariumcontrol-controllino-relay-actuator AquariumControl Controllino Relay Actuator]&lt;br /&gt;
|| [[Arduino-based relay actuation]]&lt;br /&gt;
|| C&lt;br /&gt;
|-&lt;br /&gt;
|| [https://bitbucket.org/in-dubio/aquariumcontrol-database AquariumControl Database]&lt;br /&gt;
|| [[SQL database]] using MariaDB&lt;br /&gt;
|| SQL&lt;br /&gt;
|-&lt;br /&gt;
|| [https://bitbucket.org/in-dubio/aquariumcontrol-ios-mobile-app AquariumControl iOS Mobile App]&lt;br /&gt;
|| [[App for Apple mobile devices]] &lt;br /&gt;
|| Swift&lt;br /&gt;
|-&lt;br /&gt;
|| [https://bitbucket.org/in-dubio/aquariumcontrol-main-control AquariumControl Main Control]&lt;br /&gt;
|| [[Control application]], [[Terminal client]] and test server&lt;br /&gt;
|| Rust&lt;br /&gt;
|-&lt;br /&gt;
|| [https://bitbucket.org/in-dubio/aquariumcontrol-webpage AquariumControl Webpage]&lt;br /&gt;
|| [[Dynamic Webpage]] &lt;br /&gt;
|| Java Script&lt;br /&gt;
|-&lt;br /&gt;
|| [https://bitbucket.org/in-dubio/aquariumcontrol-deb AquariumControl Debian package]&lt;br /&gt;
|| [[Debian package]] &lt;br /&gt;
|| Bash&lt;br /&gt;
|-&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
The control application, the REST API, the webpage and the SQL database are designed to run on a [https://www.raspberrypi.org Raspberry Pi].&lt;/div&gt;</summary>
		<author><name>Uwe</name></author>
	</entry>
	<entry>
		<id>http://217.79.180.177/mediawiki/index.php?title=REST_API&amp;diff=460</id>
		<title>REST API</title>
		<link rel="alternate" type="text/html" href="http://217.79.180.177/mediawiki/index.php?title=REST_API&amp;diff=460"/>
		<updated>2026-04-28T18:45:42Z</updated>

		<summary type="html">&lt;p&gt;Uwe: /* Feed log */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Requirements ==&lt;br /&gt;
&lt;br /&gt;
=== General requirements ===&lt;br /&gt;
The API shall communicate with mobile apps and dynamic webpage.&lt;br /&gt;
&lt;br /&gt;
For each request, the API shall validate the credentials (user, password).&lt;br /&gt;
&lt;br /&gt;
The credentials of authorised individuals are stored in the table 'users'.&lt;br /&gt;
* The name is stored as clear text.&lt;br /&gt;
* A hash value of the password is stored in the table. &lt;br /&gt;
&lt;br /&gt;
The API shall compute the hash function of the password when validating the credentials.&lt;br /&gt;
&lt;br /&gt;
For testing purposes, the API shall provide an endpoint which calculates the hash function of a string given as parameter.&lt;br /&gt;
&lt;br /&gt;
The API shall provide the data for all subsequent queries in JSON format.&lt;br /&gt;
&lt;br /&gt;
Timestamps shall have the format: YYYY-MM-DD hh:mm:ss&lt;br /&gt;
&lt;br /&gt;
Placeholders in subsequent SQL queries are described in brackets: &amp;lt;code&amp;gt;[placeholder]&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Coding conventions ====&lt;br /&gt;
The URL action keys shall use snake_case.&lt;br /&gt;
&lt;br /&gt;
The URL parameters sent by the client shall use camelCase.&lt;br /&gt;
&lt;br /&gt;
The JSON keys (responses from the API) shall use camelCase.&lt;br /&gt;
&lt;br /&gt;
The database columns shall use snake_case.&lt;br /&gt;
&lt;br /&gt;
The php variables shall use camelCase.&lt;br /&gt;
&lt;br /&gt;
Filenames shall use all minor letters or kebab-case.&lt;br /&gt;
* Exception: Executables may use snake_case.&lt;br /&gt;
&lt;br /&gt;
=== Requirements for overview feature ===&lt;br /&gt;
&lt;br /&gt;
The API shall provide an endpoint for informing the client about the validity of the credentials.&lt;br /&gt;
&lt;br /&gt;
The API shall provide an endpoint communicating from the server to the client a set of floating point data read from a set of files:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
!|Signal name&lt;br /&gt;
!|Signal format&lt;br /&gt;
!|Signal source&lt;br /&gt;
!|API response&lt;br /&gt;
|-&lt;br /&gt;
||timestamp&lt;br /&gt;
||string&lt;br /&gt;
||/var/local/aquarium-ctrl/aquarium-ctrl-ts&lt;br /&gt;
||timestamp&lt;br /&gt;
|-&lt;br /&gt;
||water temperature&lt;br /&gt;
||floating point number&lt;br /&gt;
||/var/local/aquarium-ctrl/atlsscntfc-temp&lt;br /&gt;
||waterTemperature&lt;br /&gt;
|-&lt;br /&gt;
||filtered water temperature&lt;br /&gt;
||floating point number&lt;br /&gt;
||/var/local/aquarium-ctrl/atlsscntfc-tempfltrd&lt;br /&gt;
||filteredWaterTemperature&lt;br /&gt;
|-&lt;br /&gt;
||pH value&lt;br /&gt;
||floating point number&lt;br /&gt;
||/var/local/aquarium-ctrl/atlsscntfc-ph&lt;br /&gt;
||phValue&lt;br /&gt;
|-&lt;br /&gt;
||filtered pH value&lt;br /&gt;
||floating point number&lt;br /&gt;
||/var/local/aquarium-ctrl/atlsscntfc-phfltrd&lt;br /&gt;
||filteredPhValue&lt;br /&gt;
|-&lt;br /&gt;
||conductivity&lt;br /&gt;
||floating point number&lt;br /&gt;
||/var/local/aquarium-ctrl/atlsscntfc-conduc&lt;br /&gt;
||conductivity&lt;br /&gt;
|-&lt;br /&gt;
||filtered conductivity&lt;br /&gt;
||floating point number&lt;br /&gt;
||/var/local/aquarium-ctrl/atlsscntfc-conducfltrd&lt;br /&gt;
||filteredConductivity&lt;br /&gt;
|-&lt;br /&gt;
||filtered water temperature&lt;br /&gt;
||floating point number&lt;br /&gt;
||/var/local/aquarium-ctrl/atlsscntfc-conducfltrd&lt;br /&gt;
||filteredWaterTemperature&lt;br /&gt;
|-&lt;br /&gt;
||tank level switch position&lt;br /&gt;
||floating point number&lt;br /&gt;
||/var/local/aquarium-ctrl/tnklvlsswtch&lt;br /&gt;
||tankLevelSwitchPosition&lt;br /&gt;
|-&lt;br /&gt;
||surface ventilation status&lt;br /&gt;
||string&lt;br /&gt;
||/var/local/aquarium-ctrl/srfcvntltn&lt;br /&gt;
||surfaceVentilationStatus&lt;br /&gt;
|-&lt;br /&gt;
||ambient temperature&lt;br /&gt;
||floating point number&lt;br /&gt;
||/var/local/aquarium-ctrl/ambtemp&lt;br /&gt;
||ambientTemperature&lt;br /&gt;
|-&lt;br /&gt;
||ambient humidity&lt;br /&gt;
||floating point number&lt;br /&gt;
||/var/local/aquarium-ctrl/ambhum&lt;br /&gt;
||ambientHumidity&lt;br /&gt;
|-&lt;br /&gt;
||heating status&lt;br /&gt;
||string&lt;br /&gt;
||/var/local/aquarium-ctrl/htng&lt;br /&gt;
||heatingStatus&lt;br /&gt;
|-&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Requirements for Balling feature ===&lt;br /&gt;
&lt;br /&gt;
==== Balling dosing log ====&lt;br /&gt;
The API shall provide an endpoint communicating from the server to the client the Balling dosing events read from the tables &amp;lt;code&amp;gt;balling_log&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;balling_set_vals&amp;lt;/code&amp;gt; of either the last 24 hours or the last 7 days or of a specific date depending on parameter provided by the client.&lt;br /&gt;
The corresponding SQL query (for a period of one day) is:&lt;br /&gt;
&lt;br /&gt;
For periods:&lt;br /&gt;
&amp;lt;code&amp;gt;SELECT balling_log.Timestamp, balling_log.pumpid, balling_log.dosingvolume, balling_set_vals.label FROM balling_log LEFT JOIN balling_set_vals ON balling_log.pumpid=balling_set_vals.pumpid WHERE Timestamp &amp;gt; (NOW() - INTERVAL 1 DAY) ORDER BY Timestamp&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The query for a specific date is:&lt;br /&gt;
&amp;lt;code&amp;gt;SELECT b.timestamp, b.pump_id, b.dosing_volume, s.label FROM balling_log AS b LEFT JOIN balling_set_vals AS s ON b.pump_id = s.pump_id WHERE DATE(b.timestamp) = ? ORDER BY b.timestamp DESC;&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
!|Signal name&lt;br /&gt;
!|Signal format&lt;br /&gt;
!|Database column name&lt;br /&gt;
!|API response&lt;br /&gt;
|-&lt;br /&gt;
||timestamp&lt;br /&gt;
||string&lt;br /&gt;
||balling_log.Timestamp&lt;br /&gt;
||timestamp&lt;br /&gt;
|-&lt;br /&gt;
||pump id&lt;br /&gt;
||integer number&lt;br /&gt;
||balling_log.pumpid&lt;br /&gt;
||pumpId&lt;br /&gt;
|-&lt;br /&gt;
||dosing volume&lt;br /&gt;
||floating point number&lt;br /&gt;
||balling_log.dosingvolume&lt;br /&gt;
||dosingVolume&lt;br /&gt;
|-&lt;br /&gt;
||label&lt;br /&gt;
||string&lt;br /&gt;
||balling_set_vals.label&lt;br /&gt;
||label&lt;br /&gt;
|-&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
==== Balling set values ====&lt;br /&gt;
The API shall provide an endpoint communicating from the server to the client the Balling dosing set values read from the table &amp;lt;code&amp;gt;balling_set_vals&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
The corresponding SQL query is:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;SELECT pumpid, dosingvolume, label FROM balling_set_vals;&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
!|Signal name&lt;br /&gt;
!|Signal format&lt;br /&gt;
!|Database column name&lt;br /&gt;
|-&lt;br /&gt;
||pump id&lt;br /&gt;
||integer number&lt;br /&gt;
||balling_set_vals.pumpid&lt;br /&gt;
|-&lt;br /&gt;
||dosing volume&lt;br /&gt;
||floating point number&lt;br /&gt;
||balling_set_vals.dosing_volume&lt;br /&gt;
|-&lt;br /&gt;
||label&lt;br /&gt;
||string&lt;br /&gt;
||balling_set_vals.label&lt;br /&gt;
|-&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
The API shall provide an endpoint which allows the client to update the dosing volume of an existing dosing set value identified by the pump id.&lt;br /&gt;
The corresponding SQL query is:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;UPDATE balling_set_vals SET dosing_volume=&amp;quot;[dosingVolume]&amp;quot; WHERE pump_id=&amp;quot;[pumpId]&amp;quot;;&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Requirements for feed feature ===&lt;br /&gt;
==== Feed log ====&lt;br /&gt;
The API shall provide an endpoint communicating from the server to the client the feed events read from the tables &amp;lt;code&amp;gt;feed_log&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;feed_profiles&amp;lt;/code&amp;gt; of either the last 24 hours or the last 7 days or of a specific date depending on parameter provided by the client.&lt;br /&gt;
The corresponding SQL query (for a period of one day) is:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;SELECT fl.timestamp, fl.feeder_on_time, fp.profile_name, fl.profile_id FROM feed_log AS fl LEFT JOIN feed_profiles AS fp ON fl.profile_id = fp.profile_id WHERE timestamp &amp;gt; (NOW() - INTERVAL 1 DAY) ORDER BY timestamp;&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The query for a specific date is:&lt;br /&gt;
&amp;lt;code&amp;gt;SELECT fl.timestamp, fl.feeder_on_time, fl.profile_name, fl.profile_id FROM feed_log AS fl LEFT JOIN feed_profiles AS fp ON fl.profile_id = fp.profile_id WHERE DATE(fl.timestamp) = ? ORDER BY fl.timestamp DESC &amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
!|Signal name&lt;br /&gt;
!|Signal format&lt;br /&gt;
!|Database column name&lt;br /&gt;
|-&lt;br /&gt;
||timestamp&lt;br /&gt;
||string&lt;br /&gt;
||feed_log.timestamp&lt;br /&gt;
|-&lt;br /&gt;
||feeder on time&lt;br /&gt;
||floating point number&lt;br /&gt;
||feed_log.feeder_on_time&lt;br /&gt;
|-&lt;br /&gt;
||feed profile name&lt;br /&gt;
||string&lt;br /&gt;
||feed_profiles.profile_name&lt;br /&gt;
|-&lt;br /&gt;
||feed profile id&lt;br /&gt;
||integer numer&lt;br /&gt;
||feed_log.profile_id&lt;br /&gt;
|-&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
==== Feed profiles ====&lt;br /&gt;
A feed profile consists of general information (ID, name) and 10 groups of repetitive data where each group contains a &amp;lt;code&amp;gt;pause&amp;lt;/code&amp;gt; section and a &amp;lt;code&amp;gt;feed&amp;lt;/code&amp;gt; section.&lt;br /&gt;
&lt;br /&gt;
The API shall provide an endpoint communicating from the server to the client the feed profiles read from the table &amp;lt;code&amp;gt;feedprofiles&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
!|Signal name&lt;br /&gt;
!|Signal format&lt;br /&gt;
!|Database column name&lt;br /&gt;
!|API response&lt;br /&gt;
|-&lt;br /&gt;
||profile id&lt;br /&gt;
||integer number&lt;br /&gt;
||feed_profiles.profile_id&lt;br /&gt;
||profileId&lt;br /&gt;
|-&lt;br /&gt;
||profile name&lt;br /&gt;
||string&lt;br /&gt;
||feed_profiles.profile_name&lt;br /&gt;
||profileName&lt;br /&gt;
|-&lt;br /&gt;
||pause 01 duration&lt;br /&gt;
||integer&lt;br /&gt;
||feed_profiles.pause_01_duration&lt;br /&gt;
||pause01Duration&lt;br /&gt;
|-&lt;br /&gt;
||pause 01 skimmer target state&lt;br /&gt;
||boolean&lt;br /&gt;
||feed_profiles.pause_01_skimmer&lt;br /&gt;
||pause01Skimmer&lt;br /&gt;
|-&lt;br /&gt;
||pause 01 main pump #1 target state&lt;br /&gt;
||boolean&lt;br /&gt;
||feed_profiles.pause_01_main_pump1&lt;br /&gt;
||pause01MainPump1&lt;br /&gt;
|-&lt;br /&gt;
||pause 01 main pump #2 target state&lt;br /&gt;
||boolean&lt;br /&gt;
||feed_profiles.pause_01_main_pump2&lt;br /&gt;
||pause01MainPump2&lt;br /&gt;
|-&lt;br /&gt;
||pause 01 aux. pump #1 target state&lt;br /&gt;
||boolean&lt;br /&gt;
||feed_profiles.pause_01_aux_pump1&lt;br /&gt;
||pause01AuxPump1&lt;br /&gt;
|-&lt;br /&gt;
||pause 01 aux. pump #2 target state&lt;br /&gt;
||boolean&lt;br /&gt;
||feed_profiles.pause_01_aux_pump2&lt;br /&gt;
||pause01AuxPump2&lt;br /&gt;
|-&lt;br /&gt;
||feed 01 duration&lt;br /&gt;
||integer&lt;br /&gt;
||feed_profiles.feed_01_duration&lt;br /&gt;
||feed01Duration&lt;br /&gt;
|-&lt;br /&gt;
||feed 01 skimmer target state&lt;br /&gt;
||boolean&lt;br /&gt;
||feed_profiles.feed_01_skimmer&lt;br /&gt;
||feed01Skimmer&lt;br /&gt;
|-&lt;br /&gt;
||feed 01 main pump #1 target state&lt;br /&gt;
||boolean&lt;br /&gt;
||feed_profiles.feed_01_main_pump1&lt;br /&gt;
||feed01MainPump1&lt;br /&gt;
|-&lt;br /&gt;
||feed 01 main pump #2 target state&lt;br /&gt;
||boolean&lt;br /&gt;
||feed_profiles.feed_01_main_pump2&lt;br /&gt;
||feed01MainPump2&lt;br /&gt;
|-&lt;br /&gt;
||feed 01 aux. pump #1 target state&lt;br /&gt;
||boolean&lt;br /&gt;
||feed_profiles.feed_01_aux_pump1&lt;br /&gt;
||feed01AuxPump1&lt;br /&gt;
|-&lt;br /&gt;
||feed 01 aux. pump #2 target state&lt;br /&gt;
||boolean&lt;br /&gt;
||feed_profiles.feed_01_aux_pump2&lt;br /&gt;
||feed01AuxPump2&lt;br /&gt;
|-&lt;br /&gt;
||...&lt;br /&gt;
||...&lt;br /&gt;
||...&lt;br /&gt;
|-&lt;br /&gt;
||pause 10 duration&lt;br /&gt;
||integer&lt;br /&gt;
||feed_profiles.pause_10_duration&lt;br /&gt;
||pause10Duration&lt;br /&gt;
|-&lt;br /&gt;
||pause 10 skimmer target state&lt;br /&gt;
||boolean&lt;br /&gt;
||feed_profiles.pause_10_skimmer&lt;br /&gt;
||pause10Skimmer&lt;br /&gt;
|-&lt;br /&gt;
||pause 10 main pump #1 target state&lt;br /&gt;
||boolean&lt;br /&gt;
||feed_profiles.pause_10_main_pump1&lt;br /&gt;
||pause10MainPump1&lt;br /&gt;
|-&lt;br /&gt;
||pause 10 main pump #2 target state&lt;br /&gt;
||boolean&lt;br /&gt;
||feed_profiles.pause_10_main_pump2&lt;br /&gt;
||pause10MainPump2&lt;br /&gt;
|-&lt;br /&gt;
||pause 10 aux. pump #1 target state&lt;br /&gt;
||boolean&lt;br /&gt;
||feed_profiles.pause_10_aux_pump1&lt;br /&gt;
||pause10AuxPump1&lt;br /&gt;
|-&lt;br /&gt;
||pause 10 aux. pump #2 target state&lt;br /&gt;
||boolean&lt;br /&gt;
||feed_profiles.pause_10_aux_pump2&lt;br /&gt;
||pause10AuxPump2&lt;br /&gt;
|-&lt;br /&gt;
||feed 10 duration&lt;br /&gt;
||integer&lt;br /&gt;
||feed_profiles.feed_10_duration&lt;br /&gt;
||feed10Duration&lt;br /&gt;
|-&lt;br /&gt;
||feed 10 skimmer target state&lt;br /&gt;
||boolean&lt;br /&gt;
||feed_profiles.feed_10_skimmer&lt;br /&gt;
||feed10Skimmer&lt;br /&gt;
|-&lt;br /&gt;
||feed 10 main pump #1 target state&lt;br /&gt;
||boolean&lt;br /&gt;
||feed_profiles.feed_10_main_pump1&lt;br /&gt;
||feed10MainPump1&lt;br /&gt;
|-&lt;br /&gt;
||feed 10 main pump #2 target state&lt;br /&gt;
||boolean&lt;br /&gt;
||feed_profiles.feed_10_main_pump2&lt;br /&gt;
||feed10MainPump2&lt;br /&gt;
|-&lt;br /&gt;
||feed 10 aux. pump #1 target state&lt;br /&gt;
||boolean&lt;br /&gt;
||feed_profiles.feed_10_aux_pump1&lt;br /&gt;
||feed10AuxPump1&lt;br /&gt;
|-&lt;br /&gt;
||feed 10 aux. pump #2 target state&lt;br /&gt;
||boolean&lt;br /&gt;
||feed_profiles.feed_10_aux_pump2&lt;br /&gt;
||feed10AuxPump2&lt;br /&gt;
|-&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
The API shall provide an endpoint which allows the client to update an existing feed profile identified by the profile id.&lt;br /&gt;
&lt;br /&gt;
The API shall provide an endpoint which allows the client to create a new profile.&lt;br /&gt;
&lt;br /&gt;
The client shall only specify the name of the new profile.&lt;br /&gt;
&lt;br /&gt;
If the feed profile already exists, the endpoint shall provide an error code and not overwrite any existing data in the database.&lt;br /&gt;
&lt;br /&gt;
The related SQL query is:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;INSERT INTO feed_profiles(profile_name) VALUES(&amp;quot;[profileName]&amp;quot;);&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The API shall provide an endpoint which allows the client to execute an existing profile.&lt;br /&gt;
&lt;br /&gt;
The client shall only specify the ID of the feed profile.&lt;br /&gt;
* The API shall check if the profile identified by the ID exists in the database and output an error if the profile already exists.&lt;br /&gt;
&lt;br /&gt;
If the profile exists, then the API shall execute a shell script:&lt;br /&gt;
&amp;lt;code&amp;gt;shell_exec(&amp;quot;/usr/local/bin/aquarium_client feed execute [profileId]&amp;quot;);&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The API shall provide an endpoint which allows the client to remove an existing profile.&lt;br /&gt;
&lt;br /&gt;
The client shall only specify the ID of the feed profile.&lt;br /&gt;
&lt;br /&gt;
The related SQL query is:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;DELETE FROM feed_profiles WHERE profile_id=&amp;quot;[profileId]&amp;quot;;&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Feed schedules ====&lt;br /&gt;
The API shall provide an endpoint communicating from the server to the client the feed schedule entries read from the tables &amp;lt;code&amp;gt;feed_schedule&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;feed_profiles&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
The related SQL query is:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;SELECT fs.timestamp, fs.profile_id, fp.profile_name, fs.is_weekly, fs.is_daily FROM feed_schedule AS fs LEFT JOIN feed_profiles AS fp ON fs.profile_id = fp.profile_id;&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
!|Signal name&lt;br /&gt;
!|Signal format&lt;br /&gt;
!|Database column name&lt;br /&gt;
|-&lt;br /&gt;
||timestamp&lt;br /&gt;
||string&lt;br /&gt;
||feed_schedule.timestamp&lt;br /&gt;
|-&lt;br /&gt;
||profile id&lt;br /&gt;
||integer number&lt;br /&gt;
||feed_profiles.profile_id&lt;br /&gt;
|-&lt;br /&gt;
||profile name&lt;br /&gt;
||string&lt;br /&gt;
||feed_profiles.profile_name&lt;br /&gt;
|-&lt;br /&gt;
||weekly repetition indicator&lt;br /&gt;
||boolean&lt;br /&gt;
||feed_schedule.is_weekly&lt;br /&gt;
|-&lt;br /&gt;
||daily repetition indicator&lt;br /&gt;
||boolean&lt;br /&gt;
||feed_schedule.is_daily&lt;br /&gt;
|-&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
The API shall provide an endpoint which allows the client to update an existing feed schedule entry identified by the timestamp.&lt;br /&gt;
&lt;br /&gt;
Note: ''Depending on the database layout, an &amp;lt;code&amp;gt;UPDATE&amp;lt;/code&amp;gt; operation may not be applicable. In this case, a combined transaction of &amp;lt;code&amp;gt;DELETE&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;INSERT&amp;lt;/code&amp;gt; using rollback in case of failure shall be applied.''&lt;br /&gt;
&lt;br /&gt;
The API shall provide an endpoint which allows the client to insert a feed schedule entry.&lt;br /&gt;
* If the profile id of the feed schedule entry requested from the client does not exist, the API shall output an error message.&lt;br /&gt;
* If the feed schedule already contains an entry with a timestamp identical to the one requested from the client, the API shall output an error message.&lt;br /&gt;
&lt;br /&gt;
The related SQL query is:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;INSERT INTO feed_schedule(timestamp, profile_id, is_weekly, is_daily) VALUES(&amp;quot;[scheduleTimestamp]&amp;quot;, &amp;quot;[profileId]&amp;quot;, [scheduleRepeatWeekly], [scheduleRepeatDaily]);&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The API shall provide an endpoint which allows the client to delete an existing feed schedule entry identified by its timestamp.&lt;br /&gt;
&lt;br /&gt;
The related SQL query is:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;DELETE FROM feed_schedule WHERE timestamp=&amp;quot;[scheduleTimestamp]&amp;quot;;&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Requirements for heating feature ===&lt;br /&gt;
==== Heating set values ====&lt;br /&gt;
The API shall provide an endpoint communicating from the server to the client the heating set values read from the table &amp;lt;code&amp;gt;heating_set_vals&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
The corresponding SQL query is:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;SELECT heating_switch_off_temp, heating_switch_on_temp FROM heating_set_vals;&amp;quot;&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
!|Signal name&lt;br /&gt;
!|Signal format&lt;br /&gt;
!|Database column name&lt;br /&gt;
|-&lt;br /&gt;
||heating switch off temperature&lt;br /&gt;
||floating point number&lt;br /&gt;
||heating_set_vals.heating_switch_off_temp&lt;br /&gt;
|-&lt;br /&gt;
||heating switch on temperature&lt;br /&gt;
||floating point number&lt;br /&gt;
||heating_set_vals.heating_switch_on_temp&lt;br /&gt;
|-&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
The API shall provide an endpoint which allows the client to update both heating set values.&lt;br /&gt;
The corresponding SQL query is:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;UPDATE heating_set_vals SET heating_switch_on_temp=[heatingSwitchOnTemp], heating_switch_off_temp=[heatingSwitchOffTemp];&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Note: ''The database shall contain only one entry in the table. In case there are multiple entries, then the query will overwrite the data of all entries. This is intentional.''&lt;br /&gt;
&lt;br /&gt;
==== Heating statistical data ====&lt;br /&gt;
The API shall provide an endpoint communicating from the server to the client the heating statistical data read from the table &amp;lt;code&amp;gt;heating_stats&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
The corresponding SQL query is:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;SELECT date, energy, ambient_temp_average, water_temp_average, heating_control_runtime FROM heating_stats;&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
!|Signal name&lt;br /&gt;
!|Signal format&lt;br /&gt;
!|Database column name&lt;br /&gt;
|-&lt;br /&gt;
||date&lt;br /&gt;
||string&lt;br /&gt;
||heating_stats.date&lt;br /&gt;
|-&lt;br /&gt;
||daily energy consumption&lt;br /&gt;
||floating point number&lt;br /&gt;
||heating_stats.energy&lt;br /&gt;
|-&lt;br /&gt;
||daily average of ambient temperature&lt;br /&gt;
||floating point number&lt;br /&gt;
||heating_stats.ambient_temp_average&lt;br /&gt;
|-&lt;br /&gt;
||daily average of water temperature&lt;br /&gt;
||floating point number&lt;br /&gt;
||heating_stats.water_temp_average&lt;br /&gt;
|-&lt;br /&gt;
||heating control runtime&lt;br /&gt;
||integer number&lt;br /&gt;
||heating_stats.heating_control_runtime&lt;br /&gt;
|-&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Requirements for refill feature ===&lt;br /&gt;
==== Refill log ====&lt;br /&gt;
The API shall provide an endpoint communicating from the server to the client the refill events read from the table &amp;lt;code&amp;gt;refill_log&amp;lt;/code&amp;gt; of either the last 24 hours or the last 7 days or of a specific date depending on parameter provided by the client.&lt;br /&gt;
&lt;br /&gt;
The corresponding SQL query (for a period of one day) is:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;SELECT timestamp, duration, volume, error_code FROM refill_log WHERE timestamp &amp;gt; (NOW() - INTERVAL 1 DAY)&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The query for specific date is:&lt;br /&gt;
&amp;lt;code&amp;gt;SELECT timestamp, duration, volume, error_code FROM refill_log WHERE DATE(timestamp) = ? ORDER BY timestamp DESC&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
!|Signal name&lt;br /&gt;
!|Signal format&lt;br /&gt;
!|Database column name&lt;br /&gt;
|-&lt;br /&gt;
||timestamp&lt;br /&gt;
||string&lt;br /&gt;
||refill_log.timestamp&lt;br /&gt;
|-&lt;br /&gt;
||duration&lt;br /&gt;
||floating point number&lt;br /&gt;
||refill_log.duration&lt;br /&gt;
|-&lt;br /&gt;
||volume&lt;br /&gt;
||floating point number&lt;br /&gt;
||refill_log.volume&lt;br /&gt;
|-&lt;br /&gt;
||error code&lt;br /&gt;
||integer&lt;br /&gt;
||refill_log.error_code&lt;br /&gt;
|-&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
==== Refill controller state ====&lt;br /&gt;
The API shall provide an endpoint communicating from the server to the client the state of the refill control read from a file:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
!|Signal name&lt;br /&gt;
!|Signal format&lt;br /&gt;
!|Signal source&lt;br /&gt;
|-&lt;br /&gt;
||refill control state&lt;br /&gt;
||string&lt;br /&gt;
||/var/local/aquarium-ctrl/refillctrl&lt;br /&gt;
|-&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
The API shall provide an endpoint which allows the client to change the refill control state by executing:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;/usr/local/bin/aquarium_client refill [command]&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
!|operation&lt;br /&gt;
!|command&lt;br /&gt;
|-&lt;br /&gt;
||reset error state&lt;br /&gt;
||reset&lt;br /&gt;
|-&lt;br /&gt;
||(re-)start&lt;br /&gt;
||start&lt;br /&gt;
|-&lt;br /&gt;
||stop&lt;br /&gt;
||stop&lt;br /&gt;
|-&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Requirements for actuator schedule feature ===&lt;br /&gt;
The API shall provide an endpoint communicating from the server to the client the actuator schedule read from the table &amp;lt;code&amp;gt;schedule&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
The corresponding SQL query is:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;SELECT schedule_type, start_time, stop_time, is_active FROM schedule;&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
!|Signal name&lt;br /&gt;
!|Signal format&lt;br /&gt;
!|Database column name&lt;br /&gt;
|-&lt;br /&gt;
||schedule type&lt;br /&gt;
||string&lt;br /&gt;
||schedule.schedule_type&lt;br /&gt;
|-&lt;br /&gt;
||start time&lt;br /&gt;
||string&lt;br /&gt;
||schedule.start_time&lt;br /&gt;
|-&lt;br /&gt;
||stop time&lt;br /&gt;
||string&lt;br /&gt;
||schedule.stop_time&lt;br /&gt;
|-&lt;br /&gt;
||active indicator&lt;br /&gt;
||boolean&lt;br /&gt;
||schedule.is_active&lt;br /&gt;
|-&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
The API shall provide an endpoint that allows the client to update all actuator schedule entries.&lt;br /&gt;
The client provides the following time values as string using the format &amp;quot;hh:mm&amp;quot;:&lt;br /&gt;
* ballingRangeStartTime&lt;br /&gt;
* ballingRangeFinishTime&lt;br /&gt;
* refillRangeStartTime&lt;br /&gt;
* refillRangeFinishTime&lt;br /&gt;
* ventilationRangeStartTime&lt;br /&gt;
* ventilationRangeFinishTime&lt;br /&gt;
* heatingRangeStartTime&lt;br /&gt;
* heatingRangeFinishTime&lt;br /&gt;
&lt;br /&gt;
The client provides the following values as integer:&lt;br /&gt;
* ballingRangeIsActive&lt;br /&gt;
* refillRangeIsActive&lt;br /&gt;
* ventilationRangeIsActive&lt;br /&gt;
* heatingRangeIsActive&lt;br /&gt;
&lt;br /&gt;
The API shall check if all values were provided by the client.&lt;br /&gt;
If all values are provided, by the client, the API shall execute a set of database commands in one transaction:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
UPDATE schedule SET start_time=&amp;quot;[ballingRangeStartTime]&amp;quot;, stop_time=&amp;quot;[ballingRangeFinishTime]&amp;quot;, is_active=[ballingRangeIsActive] WHERE schedule_type=&amp;quot;balling&amp;quot;;&lt;br /&gt;
&lt;br /&gt;
UPDATE schedule SET start_time=&amp;quot;[refillRangeStartTime]&amp;quot;, stop_time=&amp;quot;[refillRangeFinishTime]&amp;quot;, is_active=[refillRangeIsActive] WHERE schedule_type=&amp;quot;refill&amp;quot;;&lt;br /&gt;
&lt;br /&gt;
UPDATE schedule SET start_time=&amp;quot;[ventilationRangeStartTime]&amp;quot;, stop_time=&amp;quot;[ventilationRangeFinishTime]&amp;quot;, is_active=[ventilationRangeIsActive] WHERE schedule_type=&amp;quot;ventilation&amp;quot;;&lt;br /&gt;
&lt;br /&gt;
UPDATE schedule SET start_time=&amp;quot;[heatingRangeStartTime]&amp;quot;, stop_time=&amp;quot;[heatingRangeFinishTime]&amp;quot;, is_active=[heatingRangeIsActive] WHERE schedule_type=&amp;quot;heating&amp;quot;;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Requirements for time data feature ===&lt;br /&gt;
The API shall provide an endpoint communicating from the server to the client the time data of the last 24 hours read from the table &amp;lt;code&amp;gt;data&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
The related SQL query is:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;SELECT timestamp, water_temperature, water_temperature_filtered, ph_value, pH_value_filtered, conductivity, conductivity_filtered, refill_in_progress, tank_level_switch_position, tank_level_switch_invalid, tank_level_switch_position_stabilized, surface_ventilation_status, ambient_temperature, ambient_humidity, heater_status FROM data WHERE (timestamp &amp;gt; (CURRENT_TIMESTAMP() - INTERVAL 1 DAY));&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
!|Signal name&lt;br /&gt;
!|Signal format&lt;br /&gt;
!|Database column name&lt;br /&gt;
!|API response&lt;br /&gt;
|-&lt;br /&gt;
||timestamp&lt;br /&gt;
||string&lt;br /&gt;
||data.timestamp&lt;br /&gt;
||timestamp&lt;br /&gt;
|-&lt;br /&gt;
||water temperature&lt;br /&gt;
||floating point number&lt;br /&gt;
||data.water_temperature&lt;br /&gt;
||waterTemperature&lt;br /&gt;
|-&lt;br /&gt;
||filtered water temperature&lt;br /&gt;
||floating point number&lt;br /&gt;
||data.water_temperature_filtered&lt;br /&gt;
||waterTemperatureFiltered&lt;br /&gt;
|-&lt;br /&gt;
||pH value&lt;br /&gt;
||floating point number&lt;br /&gt;
||data.ph_value&lt;br /&gt;
||phValue&lt;br /&gt;
|-&lt;br /&gt;
||filtered pH value&lt;br /&gt;
||floating point number&lt;br /&gt;
||data.ph_value_filtered&lt;br /&gt;
||filteredPhValue&lt;br /&gt;
|-&lt;br /&gt;
||conductivity&lt;br /&gt;
||floating point number&lt;br /&gt;
||data.conductivity&lt;br /&gt;
||conductivity&lt;br /&gt;
|-&lt;br /&gt;
||filtered conductivity&lt;br /&gt;
||floating point number&lt;br /&gt;
||data.conductivity_filtered&lt;br /&gt;
||filteredConductivity&lt;br /&gt;
|-&lt;br /&gt;
||refill in progress&lt;br /&gt;
||boolean&lt;br /&gt;
||data.refill_in_progress&lt;br /&gt;
||refillInProgress&lt;br /&gt;
|-&lt;br /&gt;
||tank level switch position&lt;br /&gt;
||boolean&lt;br /&gt;
||data.tank_level_switch_position&lt;br /&gt;
||tankLevelSwitchPosition&lt;br /&gt;
|-&lt;br /&gt;
||tank level switch validity indicator&lt;br /&gt;
||boolean&lt;br /&gt;
||data.tank_level_switch_invalid&lt;br /&gt;
||tankLevelSwitchInvalid&lt;br /&gt;
|-&lt;br /&gt;
||tank level switch position stabilized&lt;br /&gt;
||boolean&lt;br /&gt;
||data.tank_level_switch_position_stabilized&lt;br /&gt;
||tankLevelSwitchPositionStabilized&lt;br /&gt;
|-&lt;br /&gt;
||surface ventilation status&lt;br /&gt;
||boolean&lt;br /&gt;
||data.surface_ventilation_status&lt;br /&gt;
||surfaceVentilationStatus&lt;br /&gt;
|-&lt;br /&gt;
||ambient temperature&lt;br /&gt;
||floating point number&lt;br /&gt;
||data.ambient_temperature&lt;br /&gt;
||ambientTemperature&lt;br /&gt;
|-&lt;br /&gt;
||ambient humidity&lt;br /&gt;
||floating point number&lt;br /&gt;
||data.ambient_humidity&lt;br /&gt;
||ambientHumidity&lt;br /&gt;
|-&lt;br /&gt;
||heater status&lt;br /&gt;
||boolean&lt;br /&gt;
||data.heater_status&lt;br /&gt;
||heaterStatus&lt;br /&gt;
|-&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Requirements for ventilation feature ===&lt;br /&gt;
==== Ventilation set values ====&lt;br /&gt;
The API shall provide an endpoint communicating from the server to the client the ventilation set values read from the table &amp;lt;code&amp;gt;ventilation_set_vals&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
The corresponding SQL query is:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;SELECT ventilation_switch_off_temp, ventilation_switch_on_temp FROM ventilation_set_vals;&amp;quot;&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
!|Signal name&lt;br /&gt;
!|Signal format&lt;br /&gt;
!|Database column name&lt;br /&gt;
|-&lt;br /&gt;
||ventilation switch off temperature&lt;br /&gt;
||floating point number&lt;br /&gt;
||ventilation_set_vals.ventilation_switch_off_temp&lt;br /&gt;
|-&lt;br /&gt;
||ventilation switch on temperature&lt;br /&gt;
||floating point number&lt;br /&gt;
||ventilation_set_vals.ventilation_switch_on_temp&lt;br /&gt;
|-&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
The API shall provide an endpoint which allows the client to update both ventilation set values.&lt;br /&gt;
The corresponding SQL query is:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;UPDATE ventilation_set_vals SET ventilation_switch_on_temp=[ventilationSwitchOnTemp], ventilation_switch_off_temp=[ventilationSwitchOffTemp];&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Note: ''The database shall contain only one entry in the table. In case there are multiple entries, then the query will overwrite the data of all entries. This is intentional.''&lt;br /&gt;
&lt;br /&gt;
== Architecture ==&lt;br /&gt;
The API is distributed over the following files:&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
!|File name&lt;br /&gt;
!|Content description&lt;br /&gt;
|-&lt;br /&gt;
||api.php&lt;br /&gt;
||Main API functionality&lt;br /&gt;
|-&lt;br /&gt;
||db.php&lt;br /&gt;
||Adapter for connecting to SQL database&lt;br /&gt;
|-&lt;br /&gt;
||functions.php&lt;br /&gt;
||helper functionality repeatedly used throughout the API&lt;br /&gt;
|-&lt;br /&gt;
||test_reset_db.php&lt;br /&gt;
||functionality to reset and initialise the database with mock data: Do not deploy this file in productive environment!&lt;br /&gt;
|-&lt;br /&gt;
||test.php&lt;br /&gt;
||functionality to test access to database for development purposed. Do not deploy this file in productive environment!&lt;br /&gt;
|-&lt;br /&gt;
||test_generate_mock_data.php&lt;br /&gt;
||functionality to create mock data for front end testing. Do not deploy this file in productive environment!&lt;br /&gt;
|-&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Implementation ==&lt;br /&gt;
The behaviour of the &amp;lt;code&amp;gt;api.php&amp;lt;/code&amp;gt; is controlled by the first &amp;lt;code&amp;gt;action&amp;lt;/code&amp;gt; argument provided via &amp;lt;code&amp;gt;GET&amp;lt;/code&amp;gt; method which can assume the following values:&lt;br /&gt;
=== check_auth ===&lt;br /&gt;
The API responds with a JSON object containing the following data:&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
!|Label&lt;br /&gt;
!|Value&lt;br /&gt;
|-&lt;br /&gt;
||status&lt;br /&gt;
||message&lt;br /&gt;
|-&lt;br /&gt;
||authorized&lt;br /&gt;
||Credentials valid&lt;br /&gt;
|-&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== test_hash ===&lt;br /&gt;
The API responds with a JSON object containing the following data:&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
!|Label&lt;br /&gt;
!|Value&lt;br /&gt;
|-&lt;br /&gt;
||input&lt;br /&gt;
||hash&lt;br /&gt;
|-&lt;br /&gt;
||[Input value provided]&lt;br /&gt;
||[Hash calculated from the input value]&lt;br /&gt;
|-&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== load_overview_signals ===&lt;br /&gt;
When using this parameter value, the API provides the overview data in JSON format as per [[REST_API#Requirements_for_overview_feature|above requirement]].&lt;br /&gt;
&lt;br /&gt;
=== load_balling_log ===&lt;br /&gt;
When using this parameter value, the API provides the balling dosing log as per [[REST_API#Balling_dosing_log|above requirements]].&lt;br /&gt;
&lt;br /&gt;
The second parameter &amp;lt;code&amp;gt;period&amp;lt;/code&amp;gt; determines the period: &amp;lt;code&amp;gt;7d&amp;lt;/code&amp;gt; for the last 7 days, otherwise only the last 24 hours.&lt;br /&gt;
&lt;br /&gt;
=== load_balling_set_vals ===&lt;br /&gt;
When using this parameter value, the API provides the Balling mineral dosing set values of all configured pumps in JSON format as per [[REST_API#Balling_set_values|above requirement]].&lt;br /&gt;
&lt;br /&gt;
=== update_balling_set_vals ===&lt;br /&gt;
When using this parameter value, the API checks if the required &amp;lt;code&amp;gt;POST&amp;lt;/code&amp;gt; parameters were provided:&lt;br /&gt;
* dosingVolume&lt;br /&gt;
* pumpId&lt;br /&gt;
&lt;br /&gt;
If the parameters are provided, the API will issue the SQL statement to update the dosing volume for the dedicated pump as per [[REST_API#Balling_set_values|above requirements]].&lt;br /&gt;
&lt;br /&gt;
=== load_feed_log ===&lt;br /&gt;
When using this parameter value, the API provides the balling dosing log as per [[REST_API#Feed_log|above requirements]].&lt;br /&gt;
&lt;br /&gt;
The second parameter &amp;lt;code&amp;gt;period&amp;lt;/code&amp;gt; determines the period: &amp;lt;code&amp;gt;7d&amp;lt;/code&amp;gt; for the last 7 days, otherwise only the last 24 hours.&lt;br /&gt;
&lt;br /&gt;
=== load_feed_profiles ===&lt;br /&gt;
When using this parameter value, the API provides all existing feed profile in JSON format as per [[REST_API#Feed_profiles|above requirement]].&lt;br /&gt;
&lt;br /&gt;
=== update_feed_profile ===&lt;br /&gt;
When using this parameter value, the API will check if the &amp;lt;code&amp;gt;profileId&amp;lt;/code&amp;gt; is provided as POST parameter.&lt;br /&gt;
&lt;br /&gt;
All other parameters provided will be type checked if they are integer or string types.&lt;br /&gt;
&lt;br /&gt;
The API will issue the SQL statement to update the specific feed profile identified by &amp;lt;code&amp;gt;profileId&amp;lt;/code&amp;gt; as per [[REST_API#Feed_profiles|above requirement]].&lt;br /&gt;
&lt;br /&gt;
=== create_feed_profile ===&lt;br /&gt;
When using this parameter value, the query will do the following steps [[REST_API#create_feed_profile|as per above requirement]]:&lt;br /&gt;
* check if the parameter &amp;lt;code&amp;gt;profileName&amp;lt;/code&amp;gt; is provided via &amp;lt;code&amp;gt;POST&amp;lt;/code&amp;gt; method.&lt;br /&gt;
* issue an SQL statement to check if a profile with that name already exists in the database, issue an error response and abort further execution if indeed there already is such a profile&lt;br /&gt;
* issue and SQL statement to insert an empty feed profile into the data base using &amp;lt;code&amp;gt;profileName&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== execute_feed_profile ===&lt;br /&gt;
When using this parameter value, the API will check for the additional parameter &amp;lt;code&amp;gt;profileId&amp;lt;/code&amp;gt; provided via &amp;lt;code&amp;gt;GET&amp;lt;/code&amp;gt; method.&lt;br /&gt;
If the parameter is provided, the API will issue an SQL statement to check if the database contains a matching feed profile, issue an error message and abort execution if there is no such profile.&lt;br /&gt;
If the profile exists, the API will execute the external program &amp;lt;code&amp;gt;aquarium_client&amp;lt;/code&amp;gt; using the keywords &amp;lt;code&amp;gt;feed&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;execute&amp;lt;/code&amp;gt; and the feed profile ID as parameter as per [[REST_API#Feed_profiles|above requirement]].&lt;br /&gt;
&lt;br /&gt;
=== delete_feed_profile ===&lt;br /&gt;
When using this parameter value, the API checks if the parameter &amp;lt;code&amp;gt;profileId&amp;lt;/code&amp;gt; is provided via &amp;lt;code&amp;gt;GET&amp;lt;/code&amp;gt; method. If the parameter is provided, the API will issue an SQL statement to delete the matching feed profile from the database as per [[REST_API#Feed_profiles|above requirements]].&lt;br /&gt;
&lt;br /&gt;
=== load_feed_schedule ===&lt;br /&gt;
When using this parameter value, the API will load the feed schedule entries as per [[REST_API#Feed_schedules|above requirement]].&lt;br /&gt;
&lt;br /&gt;
The SQL query uses a &amp;lt;code&amp;gt;LEFT JOIN&amp;lt;/code&amp;gt; where the profile name is read from the &amp;lt;code&amp;gt;feed_profile&amp;lt;/code&amp;gt; table identified by the &amp;lt;code&amp;gt;profile_id&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== create_feed_schedule_entry ===&lt;br /&gt;
When using this parameter value, the API will check if the following parameters were provided via &amp;lt;code&amp;gt;POST&amp;lt;/code&amp;gt; method:&lt;br /&gt;
* &amp;lt;code&amp;gt;profileId&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;timestamp&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;scheduleIsWeekly&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;scheduleIsDaily&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Note: Functionality for &amp;lt;code&amp;gt;scheduleIsWeekly&amp;lt;/code&amp;gt; is not implemented as of December 2025.&lt;br /&gt;
&lt;br /&gt;
When the additional parameters are provided, the API will issue an SQL statement to check if a matching feed schedule entry already exists and abort further execution if that is the case.&lt;br /&gt;
&lt;br /&gt;
Otherwise, the API will issue a further SQL statement to insert the feed schedule entry into the database as per [[REST_API#Feed_schedules|above requirement]].&lt;br /&gt;
&lt;br /&gt;
=== update_feed_schedule_entry ===&lt;br /&gt;
When using this parameter value, the API will check if the following parameters were provided via &amp;lt;code&amp;gt;POST&amp;lt;/code&amp;gt; method and abort execution if this is not the case:&lt;br /&gt;
* &amp;lt;code&amp;gt;timestamp&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;profileId&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;isWeekly&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;isDaily&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
When the parameters are provided, the API will execute one SQL transaction containing two statements:&lt;br /&gt;
* The first statement will delete the feed schedule entry identified by the timestamp.&lt;br /&gt;
* The second statement will insert a new feed schedule entry using the additionally provided parameter.&lt;br /&gt;
&lt;br /&gt;
This will effectively update an existing feed schedule entry as per [[REST_API#Feed_schedules|above requirement]].&lt;br /&gt;
&lt;br /&gt;
If the transaction throws an exception (when one of the SQL statements fails), the API will trigger the rollback of the transaction.&lt;br /&gt;
&lt;br /&gt;
=== delete_feed_schedule_entry ===&lt;br /&gt;
When using this parameter value, the API will check if the &amp;lt;code&amp;gt;timestamp&amp;lt;/code&amp;gt; was provided using &amp;lt;code&amp;gt;POST&amp;lt;/code&amp;gt; method and abort execution if this is not the case.&lt;br /&gt;
&lt;br /&gt;
When the additional timestamp parameter is provided, the API will issue an SQL statement to delete the corresponding feed schedule entry as per [[REST_API#Feed_schedules|above requirement]].&lt;br /&gt;
&lt;br /&gt;
=== load_heating_set_vals ===&lt;br /&gt;
When using this parameter value, the API will load the heating control set values as per [[REST_API#Heating_set_values|above requirement]].&lt;br /&gt;
&lt;br /&gt;
=== update_heating_set_vals ===&lt;br /&gt;
When using this parameter value, the API will check if the following parameters were provided via &amp;lt;code&amp;gt;POST&amp;lt;/code&amp;gt; method and will abort execution if this is not the case:&lt;br /&gt;
* heatingSwitchOnTemp&lt;br /&gt;
* heatingSwitchOffTemp&lt;br /&gt;
&lt;br /&gt;
When the parameters are provided, the API will issue an SQL statement update the heating control set values as per [[REST_API#Heating_set_values|above requirement]].&lt;br /&gt;
&lt;br /&gt;
=== load_heating_stats ===&lt;br /&gt;
When using this parameter value, the API will load the heating statistical data as per [[REST_API#Heating_statistical_data|above requirement]].&lt;br /&gt;
&lt;br /&gt;
=== load_refill_log ===&lt;br /&gt;
When using this parameter value, the API provides the refill log as per [[REST_API#Refill_log|above requirements]].&lt;br /&gt;
&lt;br /&gt;
The second parameter &amp;lt;code&amp;gt;period&amp;lt;/code&amp;gt; determines the period: &amp;lt;code&amp;gt;7d&amp;lt;/code&amp;gt; for the last 7 days, otherwise only the last 24 hours.&lt;br /&gt;
&lt;br /&gt;
=== load_refill_state ===&lt;br /&gt;
When this parameter value is provided, the API will load the refill control state information from &amp;lt;code&amp;gt;/var/local/aquarium-ctrl/refillctrl&amp;lt;/code&amp;gt; as per [[REST_API#Refill_controller_state|above requirement]].&lt;br /&gt;
&lt;br /&gt;
=== set_refill_state ===&lt;br /&gt;
When using this parameter value, the API will check if an additional parameter named &amp;lt;code&amp;gt;command&amp;lt;/code&amp;gt; was provided via &amp;lt;code&amp;gt;GET&amp;lt;/code&amp;gt; method.&lt;br /&gt;
&lt;br /&gt;
The API will check if the parameter has a valid value. Valid values are:&lt;br /&gt;
* reset&lt;br /&gt;
* start&lt;br /&gt;
* stop&lt;br /&gt;
&lt;br /&gt;
When the additional parameter is provided with a valid value, the API will execute the command &amp;lt;code&amp;gt;/usr/local/bin/aquarium_client refill [command]&amp;lt;/code&amp;gt;. This will update the refill control state as per [[REST_API#Refill_controller_state]] above requirement.&lt;br /&gt;
&lt;br /&gt;
When no valid value is provided, the API will abort execution.&lt;br /&gt;
&lt;br /&gt;
=== load_actuator_schedule ===&lt;br /&gt;
When using this parameter value, the API will load the actuator schedule as per [[REST_API#Requirements_for_actuator_schedule_feature|above requirement]].&lt;br /&gt;
&lt;br /&gt;
For testing purposes, the schedule entries are provided in alphabetical order.&lt;br /&gt;
&lt;br /&gt;
=== update_actuator_schedule ===&lt;br /&gt;
When using this parameter value, the API will check if the following parameters were provided via &amp;lt;code&amp;gt;POST&amp;lt;/code&amp;gt; method and abort execution if this is not the case:&lt;br /&gt;
* ballingRangeStartTime&lt;br /&gt;
* ballingRangeFinishTime&lt;br /&gt;
* ballingRangeIsActive&lt;br /&gt;
* refillRangeStartTime&lt;br /&gt;
* refillRangeFinishTime&lt;br /&gt;
* refillRangeIsActive,&lt;br /&gt;
* ventilationRangeStartTime&lt;br /&gt;
* ventilationRangeFinishTime&lt;br /&gt;
* ventilationRangeIsActive&lt;br /&gt;
* heatingRangeStartTime&lt;br /&gt;
* heatingRangeFinishTime&lt;br /&gt;
* heatingRangeIsActive&lt;br /&gt;
&lt;br /&gt;
When all parameters are provided, the API will initiate a transaction consisting of four SQL statements for each actuator schedule.&lt;br /&gt;
&lt;br /&gt;
The SQL statement will update the actuator schedule as per above [[REST_API#Requirements_for_actuator_schedule_feature|requirement]].&lt;br /&gt;
&lt;br /&gt;
=== load_time_data ===&lt;br /&gt;
When using this parameter value, the API will load the time data as per [[REST_API#Requirements_for_time_data_feature|above requirement]].&lt;br /&gt;
&lt;br /&gt;
=== load_ventilation_set_vals ===&lt;br /&gt;
When using this parameter value, the API will load the ventilation control set values as per [[REST_API#Ventilation_set_values|above requirement]].&lt;br /&gt;
&lt;br /&gt;
=== update_ventilation_set_vals ===&lt;br /&gt;
When using this parameter value, the API will check if the following parameters were provided via &amp;lt;code&amp;gt;POST&amp;lt;/code&amp;gt; method and will abort execution if this is not the case:&lt;br /&gt;
* ventilationSwitchOnTemp&lt;br /&gt;
* ventilationSwitchOffTemp&lt;br /&gt;
&lt;br /&gt;
When the parameters are provided, the API will issue an SQL statement update the ventilation control set values as per [[REST_API#Ventilation_set_values|above requirement]].&lt;br /&gt;
&lt;br /&gt;
== Testing ==&lt;br /&gt;
Postman is used for testing of the API.&lt;br /&gt;
&lt;br /&gt;
The requests aggregated in collections.&lt;br /&gt;
&lt;br /&gt;
Each collection (except for the General collection) starts with a request to reset the database (truncating all tables and adding mock data).&lt;br /&gt;
&lt;br /&gt;
The server-based script for resetting the database cannot be run in parallel - limiting the ability to run all tests in parallel.&lt;br /&gt;
&lt;br /&gt;
Test execution is manual per collection.&lt;br /&gt;
&lt;br /&gt;
Reference values are stored in the Variables section of each collection.&lt;br /&gt;
&lt;br /&gt;
=== Feed collection ===&lt;br /&gt;
The collection executes the following requests:&lt;br /&gt;
* Reset database (GET)&lt;br /&gt;
* Load feed log (GET)&lt;br /&gt;
* Initial Load feed profiles (GET)&lt;br /&gt;
* Update feed profile (POST)&lt;br /&gt;
* Secondary Load feed profiles (GET): This request will verify if the previous request could modify the database successfully.&lt;br /&gt;
* Initial Create feed profile (POST)&lt;br /&gt;
* Secondary Create feed profile (POST): This request will verify if trying to create a new feed profile with an already existing name is rejected.&lt;br /&gt;
* Execute feed profile (GET)&lt;br /&gt;
* Create feed schedule entry (POST)&lt;br /&gt;
* Load feed schedule (GET)&lt;br /&gt;
* Update feed schedule entry (POST)&lt;br /&gt;
* Delete feed schedule entry (POST)&lt;br /&gt;
* Delete feed profile (GET)&lt;br /&gt;
&lt;br /&gt;
=== General collection ===&lt;br /&gt;
The collection executes the following requests:&lt;br /&gt;
* Hash test (GET)&lt;br /&gt;
* Check authorisation (GET)&lt;br /&gt;
&lt;br /&gt;
=== Balling collection ===&lt;br /&gt;
The collection executes the following requests:&lt;br /&gt;
* Reset database (GET)&lt;br /&gt;
* Load Balling log (GET)&lt;br /&gt;
* Initial Load Balling set values (GET)&lt;br /&gt;
* Update Balling set values (POST)&lt;br /&gt;
* Secondary Load Balling set values (GET): This request will verify if the previous request could successfully modify the database.&lt;br /&gt;
&lt;br /&gt;
=== Overview collection ===&lt;br /&gt;
The collection executes the following requests:&lt;br /&gt;
* Reset database (GET)&lt;br /&gt;
* Load overview signals (GET)&lt;br /&gt;
&lt;br /&gt;
=== Refill collection ===&lt;br /&gt;
The collection executes the following requests:&lt;br /&gt;
* Reset database (GET)&lt;br /&gt;
* Load refill log (GET)&lt;br /&gt;
* Load refill control state (GET)&lt;br /&gt;
* Set refill control state (GET)&lt;br /&gt;
&lt;br /&gt;
=== Actuator schedule collection ===&lt;br /&gt;
The collection executes the following requests:&lt;br /&gt;
* Reset database (GET)&lt;br /&gt;
* Initial Load actuator schedule (GET)&lt;br /&gt;
* Update actuator schedule (POST)&lt;br /&gt;
* Secondary Load actuator schedule (GET): This request will verify if the previous request could successfully modify the database.&lt;br /&gt;
&lt;br /&gt;
=== Time data collection ===&lt;br /&gt;
The collection executes the following requests:&lt;br /&gt;
* Reset database (GET)&lt;br /&gt;
* Load time data (GET)&lt;br /&gt;
&lt;br /&gt;
=== Ventilation collection ===&lt;br /&gt;
The collection executes the following requests:&lt;br /&gt;
* Reset database (GET)&lt;br /&gt;
* Initial Load ventilation set values (GET)&lt;br /&gt;
* Update ventilation set values (POST)&lt;br /&gt;
* Secondary Load ventilation set values (GET): This request will check if the previous request could successfully modify the database.&lt;br /&gt;
&lt;br /&gt;
=== Heating collection ===&lt;br /&gt;
The collection executes the following requests:&lt;br /&gt;
* Reset database&lt;br /&gt;
* Initial Load heating set values (GET)&lt;br /&gt;
* Update heating set values (POST)&lt;br /&gt;
* Secondary Load heating set values (GET): This request will verify if the previous request could successfully modify the database.&lt;br /&gt;
* Load heating statistics (GET)&lt;/div&gt;</summary>
		<author><name>Uwe</name></author>
	</entry>
	<entry>
		<id>http://217.79.180.177/mediawiki/index.php?title=REST_API&amp;diff=459</id>
		<title>REST API</title>
		<link rel="alternate" type="text/html" href="http://217.79.180.177/mediawiki/index.php?title=REST_API&amp;diff=459"/>
		<updated>2026-04-28T18:45:21Z</updated>

		<summary type="html">&lt;p&gt;Uwe: /* Refill log */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Requirements ==&lt;br /&gt;
&lt;br /&gt;
=== General requirements ===&lt;br /&gt;
The API shall communicate with mobile apps and dynamic webpage.&lt;br /&gt;
&lt;br /&gt;
For each request, the API shall validate the credentials (user, password).&lt;br /&gt;
&lt;br /&gt;
The credentials of authorised individuals are stored in the table 'users'.&lt;br /&gt;
* The name is stored as clear text.&lt;br /&gt;
* A hash value of the password is stored in the table. &lt;br /&gt;
&lt;br /&gt;
The API shall compute the hash function of the password when validating the credentials.&lt;br /&gt;
&lt;br /&gt;
For testing purposes, the API shall provide an endpoint which calculates the hash function of a string given as parameter.&lt;br /&gt;
&lt;br /&gt;
The API shall provide the data for all subsequent queries in JSON format.&lt;br /&gt;
&lt;br /&gt;
Timestamps shall have the format: YYYY-MM-DD hh:mm:ss&lt;br /&gt;
&lt;br /&gt;
Placeholders in subsequent SQL queries are described in brackets: &amp;lt;code&amp;gt;[placeholder]&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Coding conventions ====&lt;br /&gt;
The URL action keys shall use snake_case.&lt;br /&gt;
&lt;br /&gt;
The URL parameters sent by the client shall use camelCase.&lt;br /&gt;
&lt;br /&gt;
The JSON keys (responses from the API) shall use camelCase.&lt;br /&gt;
&lt;br /&gt;
The database columns shall use snake_case.&lt;br /&gt;
&lt;br /&gt;
The php variables shall use camelCase.&lt;br /&gt;
&lt;br /&gt;
Filenames shall use all minor letters or kebab-case.&lt;br /&gt;
* Exception: Executables may use snake_case.&lt;br /&gt;
&lt;br /&gt;
=== Requirements for overview feature ===&lt;br /&gt;
&lt;br /&gt;
The API shall provide an endpoint for informing the client about the validity of the credentials.&lt;br /&gt;
&lt;br /&gt;
The API shall provide an endpoint communicating from the server to the client a set of floating point data read from a set of files:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
!|Signal name&lt;br /&gt;
!|Signal format&lt;br /&gt;
!|Signal source&lt;br /&gt;
!|API response&lt;br /&gt;
|-&lt;br /&gt;
||timestamp&lt;br /&gt;
||string&lt;br /&gt;
||/var/local/aquarium-ctrl/aquarium-ctrl-ts&lt;br /&gt;
||timestamp&lt;br /&gt;
|-&lt;br /&gt;
||water temperature&lt;br /&gt;
||floating point number&lt;br /&gt;
||/var/local/aquarium-ctrl/atlsscntfc-temp&lt;br /&gt;
||waterTemperature&lt;br /&gt;
|-&lt;br /&gt;
||filtered water temperature&lt;br /&gt;
||floating point number&lt;br /&gt;
||/var/local/aquarium-ctrl/atlsscntfc-tempfltrd&lt;br /&gt;
||filteredWaterTemperature&lt;br /&gt;
|-&lt;br /&gt;
||pH value&lt;br /&gt;
||floating point number&lt;br /&gt;
||/var/local/aquarium-ctrl/atlsscntfc-ph&lt;br /&gt;
||phValue&lt;br /&gt;
|-&lt;br /&gt;
||filtered pH value&lt;br /&gt;
||floating point number&lt;br /&gt;
||/var/local/aquarium-ctrl/atlsscntfc-phfltrd&lt;br /&gt;
||filteredPhValue&lt;br /&gt;
|-&lt;br /&gt;
||conductivity&lt;br /&gt;
||floating point number&lt;br /&gt;
||/var/local/aquarium-ctrl/atlsscntfc-conduc&lt;br /&gt;
||conductivity&lt;br /&gt;
|-&lt;br /&gt;
||filtered conductivity&lt;br /&gt;
||floating point number&lt;br /&gt;
||/var/local/aquarium-ctrl/atlsscntfc-conducfltrd&lt;br /&gt;
||filteredConductivity&lt;br /&gt;
|-&lt;br /&gt;
||filtered water temperature&lt;br /&gt;
||floating point number&lt;br /&gt;
||/var/local/aquarium-ctrl/atlsscntfc-conducfltrd&lt;br /&gt;
||filteredWaterTemperature&lt;br /&gt;
|-&lt;br /&gt;
||tank level switch position&lt;br /&gt;
||floating point number&lt;br /&gt;
||/var/local/aquarium-ctrl/tnklvlsswtch&lt;br /&gt;
||tankLevelSwitchPosition&lt;br /&gt;
|-&lt;br /&gt;
||surface ventilation status&lt;br /&gt;
||string&lt;br /&gt;
||/var/local/aquarium-ctrl/srfcvntltn&lt;br /&gt;
||surfaceVentilationStatus&lt;br /&gt;
|-&lt;br /&gt;
||ambient temperature&lt;br /&gt;
||floating point number&lt;br /&gt;
||/var/local/aquarium-ctrl/ambtemp&lt;br /&gt;
||ambientTemperature&lt;br /&gt;
|-&lt;br /&gt;
||ambient humidity&lt;br /&gt;
||floating point number&lt;br /&gt;
||/var/local/aquarium-ctrl/ambhum&lt;br /&gt;
||ambientHumidity&lt;br /&gt;
|-&lt;br /&gt;
||heating status&lt;br /&gt;
||string&lt;br /&gt;
||/var/local/aquarium-ctrl/htng&lt;br /&gt;
||heatingStatus&lt;br /&gt;
|-&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Requirements for Balling feature ===&lt;br /&gt;
&lt;br /&gt;
==== Balling dosing log ====&lt;br /&gt;
The API shall provide an endpoint communicating from the server to the client the Balling dosing events read from the tables &amp;lt;code&amp;gt;balling_log&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;balling_set_vals&amp;lt;/code&amp;gt; of either the last 24 hours or the last 7 days or of a specific date depending on parameter provided by the client.&lt;br /&gt;
The corresponding SQL query (for a period of one day) is:&lt;br /&gt;
&lt;br /&gt;
For periods:&lt;br /&gt;
&amp;lt;code&amp;gt;SELECT balling_log.Timestamp, balling_log.pumpid, balling_log.dosingvolume, balling_set_vals.label FROM balling_log LEFT JOIN balling_set_vals ON balling_log.pumpid=balling_set_vals.pumpid WHERE Timestamp &amp;gt; (NOW() - INTERVAL 1 DAY) ORDER BY Timestamp&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The query for a specific date is:&lt;br /&gt;
&amp;lt;code&amp;gt;SELECT b.timestamp, b.pump_id, b.dosing_volume, s.label FROM balling_log AS b LEFT JOIN balling_set_vals AS s ON b.pump_id = s.pump_id WHERE DATE(b.timestamp) = ? ORDER BY b.timestamp DESC;&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
!|Signal name&lt;br /&gt;
!|Signal format&lt;br /&gt;
!|Database column name&lt;br /&gt;
!|API response&lt;br /&gt;
|-&lt;br /&gt;
||timestamp&lt;br /&gt;
||string&lt;br /&gt;
||balling_log.Timestamp&lt;br /&gt;
||timestamp&lt;br /&gt;
|-&lt;br /&gt;
||pump id&lt;br /&gt;
||integer number&lt;br /&gt;
||balling_log.pumpid&lt;br /&gt;
||pumpId&lt;br /&gt;
|-&lt;br /&gt;
||dosing volume&lt;br /&gt;
||floating point number&lt;br /&gt;
||balling_log.dosingvolume&lt;br /&gt;
||dosingVolume&lt;br /&gt;
|-&lt;br /&gt;
||label&lt;br /&gt;
||string&lt;br /&gt;
||balling_set_vals.label&lt;br /&gt;
||label&lt;br /&gt;
|-&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
==== Balling set values ====&lt;br /&gt;
The API shall provide an endpoint communicating from the server to the client the Balling dosing set values read from the table &amp;lt;code&amp;gt;balling_set_vals&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
The corresponding SQL query is:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;SELECT pumpid, dosingvolume, label FROM balling_set_vals;&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
!|Signal name&lt;br /&gt;
!|Signal format&lt;br /&gt;
!|Database column name&lt;br /&gt;
|-&lt;br /&gt;
||pump id&lt;br /&gt;
||integer number&lt;br /&gt;
||balling_set_vals.pumpid&lt;br /&gt;
|-&lt;br /&gt;
||dosing volume&lt;br /&gt;
||floating point number&lt;br /&gt;
||balling_set_vals.dosing_volume&lt;br /&gt;
|-&lt;br /&gt;
||label&lt;br /&gt;
||string&lt;br /&gt;
||balling_set_vals.label&lt;br /&gt;
|-&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
The API shall provide an endpoint which allows the client to update the dosing volume of an existing dosing set value identified by the pump id.&lt;br /&gt;
The corresponding SQL query is:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;UPDATE balling_set_vals SET dosing_volume=&amp;quot;[dosingVolume]&amp;quot; WHERE pump_id=&amp;quot;[pumpId]&amp;quot;;&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Requirements for feed feature ===&lt;br /&gt;
==== Feed log ====&lt;br /&gt;
The API shall provide an endpoint communicating from the server to the client the feed events read from the tables &amp;lt;code&amp;gt;feed_log&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;feed_profiles&amp;lt;/code&amp;gt; of either the last 24 hours or the last 7 days depending on parameter provided by the client.&lt;br /&gt;
The corresponding SQL query (for a period of one day) is:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;SELECT fl.timestamp, fl.feeder_on_time, fp.profile_name, fl.profile_id FROM feed_log AS fl LEFT JOIN feed_profiles AS fp ON fl.profile_id = fp.profile_id WHERE timestamp &amp;gt; (NOW() - INTERVAL 1 DAY) ORDER BY timestamp;&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The query for a specific date is:&lt;br /&gt;
&amp;lt;code&amp;gt;SELECT fl.timestamp, fl.feeder_on_time, fl.profile_name, fl.profile_id FROM feed_log AS fl LEFT JOIN feed_profiles AS fp ON fl.profile_id = fp.profile_id WHERE DATE(fl.timestamp) = ? ORDER BY fl.timestamp DESC &amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
!|Signal name&lt;br /&gt;
!|Signal format&lt;br /&gt;
!|Database column name&lt;br /&gt;
|-&lt;br /&gt;
||timestamp&lt;br /&gt;
||string&lt;br /&gt;
||feed_log.timestamp&lt;br /&gt;
|-&lt;br /&gt;
||feeder on time&lt;br /&gt;
||floating point number&lt;br /&gt;
||feed_log.feeder_on_time&lt;br /&gt;
|-&lt;br /&gt;
||feed profile name&lt;br /&gt;
||string&lt;br /&gt;
||feed_profiles.profile_name&lt;br /&gt;
|-&lt;br /&gt;
||feed profile id&lt;br /&gt;
||integer numer&lt;br /&gt;
||feed_log.profile_id&lt;br /&gt;
|-&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
==== Feed profiles ====&lt;br /&gt;
A feed profile consists of general information (ID, name) and 10 groups of repetitive data where each group contains a &amp;lt;code&amp;gt;pause&amp;lt;/code&amp;gt; section and a &amp;lt;code&amp;gt;feed&amp;lt;/code&amp;gt; section.&lt;br /&gt;
&lt;br /&gt;
The API shall provide an endpoint communicating from the server to the client the feed profiles read from the table &amp;lt;code&amp;gt;feedprofiles&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
!|Signal name&lt;br /&gt;
!|Signal format&lt;br /&gt;
!|Database column name&lt;br /&gt;
!|API response&lt;br /&gt;
|-&lt;br /&gt;
||profile id&lt;br /&gt;
||integer number&lt;br /&gt;
||feed_profiles.profile_id&lt;br /&gt;
||profileId&lt;br /&gt;
|-&lt;br /&gt;
||profile name&lt;br /&gt;
||string&lt;br /&gt;
||feed_profiles.profile_name&lt;br /&gt;
||profileName&lt;br /&gt;
|-&lt;br /&gt;
||pause 01 duration&lt;br /&gt;
||integer&lt;br /&gt;
||feed_profiles.pause_01_duration&lt;br /&gt;
||pause01Duration&lt;br /&gt;
|-&lt;br /&gt;
||pause 01 skimmer target state&lt;br /&gt;
||boolean&lt;br /&gt;
||feed_profiles.pause_01_skimmer&lt;br /&gt;
||pause01Skimmer&lt;br /&gt;
|-&lt;br /&gt;
||pause 01 main pump #1 target state&lt;br /&gt;
||boolean&lt;br /&gt;
||feed_profiles.pause_01_main_pump1&lt;br /&gt;
||pause01MainPump1&lt;br /&gt;
|-&lt;br /&gt;
||pause 01 main pump #2 target state&lt;br /&gt;
||boolean&lt;br /&gt;
||feed_profiles.pause_01_main_pump2&lt;br /&gt;
||pause01MainPump2&lt;br /&gt;
|-&lt;br /&gt;
||pause 01 aux. pump #1 target state&lt;br /&gt;
||boolean&lt;br /&gt;
||feed_profiles.pause_01_aux_pump1&lt;br /&gt;
||pause01AuxPump1&lt;br /&gt;
|-&lt;br /&gt;
||pause 01 aux. pump #2 target state&lt;br /&gt;
||boolean&lt;br /&gt;
||feed_profiles.pause_01_aux_pump2&lt;br /&gt;
||pause01AuxPump2&lt;br /&gt;
|-&lt;br /&gt;
||feed 01 duration&lt;br /&gt;
||integer&lt;br /&gt;
||feed_profiles.feed_01_duration&lt;br /&gt;
||feed01Duration&lt;br /&gt;
|-&lt;br /&gt;
||feed 01 skimmer target state&lt;br /&gt;
||boolean&lt;br /&gt;
||feed_profiles.feed_01_skimmer&lt;br /&gt;
||feed01Skimmer&lt;br /&gt;
|-&lt;br /&gt;
||feed 01 main pump #1 target state&lt;br /&gt;
||boolean&lt;br /&gt;
||feed_profiles.feed_01_main_pump1&lt;br /&gt;
||feed01MainPump1&lt;br /&gt;
|-&lt;br /&gt;
||feed 01 main pump #2 target state&lt;br /&gt;
||boolean&lt;br /&gt;
||feed_profiles.feed_01_main_pump2&lt;br /&gt;
||feed01MainPump2&lt;br /&gt;
|-&lt;br /&gt;
||feed 01 aux. pump #1 target state&lt;br /&gt;
||boolean&lt;br /&gt;
||feed_profiles.feed_01_aux_pump1&lt;br /&gt;
||feed01AuxPump1&lt;br /&gt;
|-&lt;br /&gt;
||feed 01 aux. pump #2 target state&lt;br /&gt;
||boolean&lt;br /&gt;
||feed_profiles.feed_01_aux_pump2&lt;br /&gt;
||feed01AuxPump2&lt;br /&gt;
|-&lt;br /&gt;
||...&lt;br /&gt;
||...&lt;br /&gt;
||...&lt;br /&gt;
|-&lt;br /&gt;
||pause 10 duration&lt;br /&gt;
||integer&lt;br /&gt;
||feed_profiles.pause_10_duration&lt;br /&gt;
||pause10Duration&lt;br /&gt;
|-&lt;br /&gt;
||pause 10 skimmer target state&lt;br /&gt;
||boolean&lt;br /&gt;
||feed_profiles.pause_10_skimmer&lt;br /&gt;
||pause10Skimmer&lt;br /&gt;
|-&lt;br /&gt;
||pause 10 main pump #1 target state&lt;br /&gt;
||boolean&lt;br /&gt;
||feed_profiles.pause_10_main_pump1&lt;br /&gt;
||pause10MainPump1&lt;br /&gt;
|-&lt;br /&gt;
||pause 10 main pump #2 target state&lt;br /&gt;
||boolean&lt;br /&gt;
||feed_profiles.pause_10_main_pump2&lt;br /&gt;
||pause10MainPump2&lt;br /&gt;
|-&lt;br /&gt;
||pause 10 aux. pump #1 target state&lt;br /&gt;
||boolean&lt;br /&gt;
||feed_profiles.pause_10_aux_pump1&lt;br /&gt;
||pause10AuxPump1&lt;br /&gt;
|-&lt;br /&gt;
||pause 10 aux. pump #2 target state&lt;br /&gt;
||boolean&lt;br /&gt;
||feed_profiles.pause_10_aux_pump2&lt;br /&gt;
||pause10AuxPump2&lt;br /&gt;
|-&lt;br /&gt;
||feed 10 duration&lt;br /&gt;
||integer&lt;br /&gt;
||feed_profiles.feed_10_duration&lt;br /&gt;
||feed10Duration&lt;br /&gt;
|-&lt;br /&gt;
||feed 10 skimmer target state&lt;br /&gt;
||boolean&lt;br /&gt;
||feed_profiles.feed_10_skimmer&lt;br /&gt;
||feed10Skimmer&lt;br /&gt;
|-&lt;br /&gt;
||feed 10 main pump #1 target state&lt;br /&gt;
||boolean&lt;br /&gt;
||feed_profiles.feed_10_main_pump1&lt;br /&gt;
||feed10MainPump1&lt;br /&gt;
|-&lt;br /&gt;
||feed 10 main pump #2 target state&lt;br /&gt;
||boolean&lt;br /&gt;
||feed_profiles.feed_10_main_pump2&lt;br /&gt;
||feed10MainPump2&lt;br /&gt;
|-&lt;br /&gt;
||feed 10 aux. pump #1 target state&lt;br /&gt;
||boolean&lt;br /&gt;
||feed_profiles.feed_10_aux_pump1&lt;br /&gt;
||feed10AuxPump1&lt;br /&gt;
|-&lt;br /&gt;
||feed 10 aux. pump #2 target state&lt;br /&gt;
||boolean&lt;br /&gt;
||feed_profiles.feed_10_aux_pump2&lt;br /&gt;
||feed10AuxPump2&lt;br /&gt;
|-&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
The API shall provide an endpoint which allows the client to update an existing feed profile identified by the profile id.&lt;br /&gt;
&lt;br /&gt;
The API shall provide an endpoint which allows the client to create a new profile.&lt;br /&gt;
&lt;br /&gt;
The client shall only specify the name of the new profile.&lt;br /&gt;
&lt;br /&gt;
If the feed profile already exists, the endpoint shall provide an error code and not overwrite any existing data in the database.&lt;br /&gt;
&lt;br /&gt;
The related SQL query is:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;INSERT INTO feed_profiles(profile_name) VALUES(&amp;quot;[profileName]&amp;quot;);&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The API shall provide an endpoint which allows the client to execute an existing profile.&lt;br /&gt;
&lt;br /&gt;
The client shall only specify the ID of the feed profile.&lt;br /&gt;
* The API shall check if the profile identified by the ID exists in the database and output an error if the profile already exists.&lt;br /&gt;
&lt;br /&gt;
If the profile exists, then the API shall execute a shell script:&lt;br /&gt;
&amp;lt;code&amp;gt;shell_exec(&amp;quot;/usr/local/bin/aquarium_client feed execute [profileId]&amp;quot;);&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The API shall provide an endpoint which allows the client to remove an existing profile.&lt;br /&gt;
&lt;br /&gt;
The client shall only specify the ID of the feed profile.&lt;br /&gt;
&lt;br /&gt;
The related SQL query is:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;DELETE FROM feed_profiles WHERE profile_id=&amp;quot;[profileId]&amp;quot;;&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Feed schedules ====&lt;br /&gt;
The API shall provide an endpoint communicating from the server to the client the feed schedule entries read from the tables &amp;lt;code&amp;gt;feed_schedule&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;feed_profiles&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
The related SQL query is:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;SELECT fs.timestamp, fs.profile_id, fp.profile_name, fs.is_weekly, fs.is_daily FROM feed_schedule AS fs LEFT JOIN feed_profiles AS fp ON fs.profile_id = fp.profile_id;&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
!|Signal name&lt;br /&gt;
!|Signal format&lt;br /&gt;
!|Database column name&lt;br /&gt;
|-&lt;br /&gt;
||timestamp&lt;br /&gt;
||string&lt;br /&gt;
||feed_schedule.timestamp&lt;br /&gt;
|-&lt;br /&gt;
||profile id&lt;br /&gt;
||integer number&lt;br /&gt;
||feed_profiles.profile_id&lt;br /&gt;
|-&lt;br /&gt;
||profile name&lt;br /&gt;
||string&lt;br /&gt;
||feed_profiles.profile_name&lt;br /&gt;
|-&lt;br /&gt;
||weekly repetition indicator&lt;br /&gt;
||boolean&lt;br /&gt;
||feed_schedule.is_weekly&lt;br /&gt;
|-&lt;br /&gt;
||daily repetition indicator&lt;br /&gt;
||boolean&lt;br /&gt;
||feed_schedule.is_daily&lt;br /&gt;
|-&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
The API shall provide an endpoint which allows the client to update an existing feed schedule entry identified by the timestamp.&lt;br /&gt;
&lt;br /&gt;
Note: ''Depending on the database layout, an &amp;lt;code&amp;gt;UPDATE&amp;lt;/code&amp;gt; operation may not be applicable. In this case, a combined transaction of &amp;lt;code&amp;gt;DELETE&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;INSERT&amp;lt;/code&amp;gt; using rollback in case of failure shall be applied.''&lt;br /&gt;
&lt;br /&gt;
The API shall provide an endpoint which allows the client to insert a feed schedule entry.&lt;br /&gt;
* If the profile id of the feed schedule entry requested from the client does not exist, the API shall output an error message.&lt;br /&gt;
* If the feed schedule already contains an entry with a timestamp identical to the one requested from the client, the API shall output an error message.&lt;br /&gt;
&lt;br /&gt;
The related SQL query is:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;INSERT INTO feed_schedule(timestamp, profile_id, is_weekly, is_daily) VALUES(&amp;quot;[scheduleTimestamp]&amp;quot;, &amp;quot;[profileId]&amp;quot;, [scheduleRepeatWeekly], [scheduleRepeatDaily]);&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The API shall provide an endpoint which allows the client to delete an existing feed schedule entry identified by its timestamp.&lt;br /&gt;
&lt;br /&gt;
The related SQL query is:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;DELETE FROM feed_schedule WHERE timestamp=&amp;quot;[scheduleTimestamp]&amp;quot;;&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Requirements for heating feature ===&lt;br /&gt;
==== Heating set values ====&lt;br /&gt;
The API shall provide an endpoint communicating from the server to the client the heating set values read from the table &amp;lt;code&amp;gt;heating_set_vals&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
The corresponding SQL query is:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;SELECT heating_switch_off_temp, heating_switch_on_temp FROM heating_set_vals;&amp;quot;&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
!|Signal name&lt;br /&gt;
!|Signal format&lt;br /&gt;
!|Database column name&lt;br /&gt;
|-&lt;br /&gt;
||heating switch off temperature&lt;br /&gt;
||floating point number&lt;br /&gt;
||heating_set_vals.heating_switch_off_temp&lt;br /&gt;
|-&lt;br /&gt;
||heating switch on temperature&lt;br /&gt;
||floating point number&lt;br /&gt;
||heating_set_vals.heating_switch_on_temp&lt;br /&gt;
|-&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
The API shall provide an endpoint which allows the client to update both heating set values.&lt;br /&gt;
The corresponding SQL query is:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;UPDATE heating_set_vals SET heating_switch_on_temp=[heatingSwitchOnTemp], heating_switch_off_temp=[heatingSwitchOffTemp];&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Note: ''The database shall contain only one entry in the table. In case there are multiple entries, then the query will overwrite the data of all entries. This is intentional.''&lt;br /&gt;
&lt;br /&gt;
==== Heating statistical data ====&lt;br /&gt;
The API shall provide an endpoint communicating from the server to the client the heating statistical data read from the table &amp;lt;code&amp;gt;heating_stats&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
The corresponding SQL query is:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;SELECT date, energy, ambient_temp_average, water_temp_average, heating_control_runtime FROM heating_stats;&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
!|Signal name&lt;br /&gt;
!|Signal format&lt;br /&gt;
!|Database column name&lt;br /&gt;
|-&lt;br /&gt;
||date&lt;br /&gt;
||string&lt;br /&gt;
||heating_stats.date&lt;br /&gt;
|-&lt;br /&gt;
||daily energy consumption&lt;br /&gt;
||floating point number&lt;br /&gt;
||heating_stats.energy&lt;br /&gt;
|-&lt;br /&gt;
||daily average of ambient temperature&lt;br /&gt;
||floating point number&lt;br /&gt;
||heating_stats.ambient_temp_average&lt;br /&gt;
|-&lt;br /&gt;
||daily average of water temperature&lt;br /&gt;
||floating point number&lt;br /&gt;
||heating_stats.water_temp_average&lt;br /&gt;
|-&lt;br /&gt;
||heating control runtime&lt;br /&gt;
||integer number&lt;br /&gt;
||heating_stats.heating_control_runtime&lt;br /&gt;
|-&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Requirements for refill feature ===&lt;br /&gt;
==== Refill log ====&lt;br /&gt;
The API shall provide an endpoint communicating from the server to the client the refill events read from the table &amp;lt;code&amp;gt;refill_log&amp;lt;/code&amp;gt; of either the last 24 hours or the last 7 days or of a specific date depending on parameter provided by the client.&lt;br /&gt;
&lt;br /&gt;
The corresponding SQL query (for a period of one day) is:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;SELECT timestamp, duration, volume, error_code FROM refill_log WHERE timestamp &amp;gt; (NOW() - INTERVAL 1 DAY)&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The query for specific date is:&lt;br /&gt;
&amp;lt;code&amp;gt;SELECT timestamp, duration, volume, error_code FROM refill_log WHERE DATE(timestamp) = ? ORDER BY timestamp DESC&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
!|Signal name&lt;br /&gt;
!|Signal format&lt;br /&gt;
!|Database column name&lt;br /&gt;
|-&lt;br /&gt;
||timestamp&lt;br /&gt;
||string&lt;br /&gt;
||refill_log.timestamp&lt;br /&gt;
|-&lt;br /&gt;
||duration&lt;br /&gt;
||floating point number&lt;br /&gt;
||refill_log.duration&lt;br /&gt;
|-&lt;br /&gt;
||volume&lt;br /&gt;
||floating point number&lt;br /&gt;
||refill_log.volume&lt;br /&gt;
|-&lt;br /&gt;
||error code&lt;br /&gt;
||integer&lt;br /&gt;
||refill_log.error_code&lt;br /&gt;
|-&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
==== Refill controller state ====&lt;br /&gt;
The API shall provide an endpoint communicating from the server to the client the state of the refill control read from a file:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
!|Signal name&lt;br /&gt;
!|Signal format&lt;br /&gt;
!|Signal source&lt;br /&gt;
|-&lt;br /&gt;
||refill control state&lt;br /&gt;
||string&lt;br /&gt;
||/var/local/aquarium-ctrl/refillctrl&lt;br /&gt;
|-&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
The API shall provide an endpoint which allows the client to change the refill control state by executing:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;/usr/local/bin/aquarium_client refill [command]&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
!|operation&lt;br /&gt;
!|command&lt;br /&gt;
|-&lt;br /&gt;
||reset error state&lt;br /&gt;
||reset&lt;br /&gt;
|-&lt;br /&gt;
||(re-)start&lt;br /&gt;
||start&lt;br /&gt;
|-&lt;br /&gt;
||stop&lt;br /&gt;
||stop&lt;br /&gt;
|-&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Requirements for actuator schedule feature ===&lt;br /&gt;
The API shall provide an endpoint communicating from the server to the client the actuator schedule read from the table &amp;lt;code&amp;gt;schedule&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
The corresponding SQL query is:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;SELECT schedule_type, start_time, stop_time, is_active FROM schedule;&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
!|Signal name&lt;br /&gt;
!|Signal format&lt;br /&gt;
!|Database column name&lt;br /&gt;
|-&lt;br /&gt;
||schedule type&lt;br /&gt;
||string&lt;br /&gt;
||schedule.schedule_type&lt;br /&gt;
|-&lt;br /&gt;
||start time&lt;br /&gt;
||string&lt;br /&gt;
||schedule.start_time&lt;br /&gt;
|-&lt;br /&gt;
||stop time&lt;br /&gt;
||string&lt;br /&gt;
||schedule.stop_time&lt;br /&gt;
|-&lt;br /&gt;
||active indicator&lt;br /&gt;
||boolean&lt;br /&gt;
||schedule.is_active&lt;br /&gt;
|-&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
The API shall provide an endpoint that allows the client to update all actuator schedule entries.&lt;br /&gt;
The client provides the following time values as string using the format &amp;quot;hh:mm&amp;quot;:&lt;br /&gt;
* ballingRangeStartTime&lt;br /&gt;
* ballingRangeFinishTime&lt;br /&gt;
* refillRangeStartTime&lt;br /&gt;
* refillRangeFinishTime&lt;br /&gt;
* ventilationRangeStartTime&lt;br /&gt;
* ventilationRangeFinishTime&lt;br /&gt;
* heatingRangeStartTime&lt;br /&gt;
* heatingRangeFinishTime&lt;br /&gt;
&lt;br /&gt;
The client provides the following values as integer:&lt;br /&gt;
* ballingRangeIsActive&lt;br /&gt;
* refillRangeIsActive&lt;br /&gt;
* ventilationRangeIsActive&lt;br /&gt;
* heatingRangeIsActive&lt;br /&gt;
&lt;br /&gt;
The API shall check if all values were provided by the client.&lt;br /&gt;
If all values are provided, by the client, the API shall execute a set of database commands in one transaction:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
UPDATE schedule SET start_time=&amp;quot;[ballingRangeStartTime]&amp;quot;, stop_time=&amp;quot;[ballingRangeFinishTime]&amp;quot;, is_active=[ballingRangeIsActive] WHERE schedule_type=&amp;quot;balling&amp;quot;;&lt;br /&gt;
&lt;br /&gt;
UPDATE schedule SET start_time=&amp;quot;[refillRangeStartTime]&amp;quot;, stop_time=&amp;quot;[refillRangeFinishTime]&amp;quot;, is_active=[refillRangeIsActive] WHERE schedule_type=&amp;quot;refill&amp;quot;;&lt;br /&gt;
&lt;br /&gt;
UPDATE schedule SET start_time=&amp;quot;[ventilationRangeStartTime]&amp;quot;, stop_time=&amp;quot;[ventilationRangeFinishTime]&amp;quot;, is_active=[ventilationRangeIsActive] WHERE schedule_type=&amp;quot;ventilation&amp;quot;;&lt;br /&gt;
&lt;br /&gt;
UPDATE schedule SET start_time=&amp;quot;[heatingRangeStartTime]&amp;quot;, stop_time=&amp;quot;[heatingRangeFinishTime]&amp;quot;, is_active=[heatingRangeIsActive] WHERE schedule_type=&amp;quot;heating&amp;quot;;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Requirements for time data feature ===&lt;br /&gt;
The API shall provide an endpoint communicating from the server to the client the time data of the last 24 hours read from the table &amp;lt;code&amp;gt;data&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
The related SQL query is:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;SELECT timestamp, water_temperature, water_temperature_filtered, ph_value, pH_value_filtered, conductivity, conductivity_filtered, refill_in_progress, tank_level_switch_position, tank_level_switch_invalid, tank_level_switch_position_stabilized, surface_ventilation_status, ambient_temperature, ambient_humidity, heater_status FROM data WHERE (timestamp &amp;gt; (CURRENT_TIMESTAMP() - INTERVAL 1 DAY));&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
!|Signal name&lt;br /&gt;
!|Signal format&lt;br /&gt;
!|Database column name&lt;br /&gt;
!|API response&lt;br /&gt;
|-&lt;br /&gt;
||timestamp&lt;br /&gt;
||string&lt;br /&gt;
||data.timestamp&lt;br /&gt;
||timestamp&lt;br /&gt;
|-&lt;br /&gt;
||water temperature&lt;br /&gt;
||floating point number&lt;br /&gt;
||data.water_temperature&lt;br /&gt;
||waterTemperature&lt;br /&gt;
|-&lt;br /&gt;
||filtered water temperature&lt;br /&gt;
||floating point number&lt;br /&gt;
||data.water_temperature_filtered&lt;br /&gt;
||waterTemperatureFiltered&lt;br /&gt;
|-&lt;br /&gt;
||pH value&lt;br /&gt;
||floating point number&lt;br /&gt;
||data.ph_value&lt;br /&gt;
||phValue&lt;br /&gt;
|-&lt;br /&gt;
||filtered pH value&lt;br /&gt;
||floating point number&lt;br /&gt;
||data.ph_value_filtered&lt;br /&gt;
||filteredPhValue&lt;br /&gt;
|-&lt;br /&gt;
||conductivity&lt;br /&gt;
||floating point number&lt;br /&gt;
||data.conductivity&lt;br /&gt;
||conductivity&lt;br /&gt;
|-&lt;br /&gt;
||filtered conductivity&lt;br /&gt;
||floating point number&lt;br /&gt;
||data.conductivity_filtered&lt;br /&gt;
||filteredConductivity&lt;br /&gt;
|-&lt;br /&gt;
||refill in progress&lt;br /&gt;
||boolean&lt;br /&gt;
||data.refill_in_progress&lt;br /&gt;
||refillInProgress&lt;br /&gt;
|-&lt;br /&gt;
||tank level switch position&lt;br /&gt;
||boolean&lt;br /&gt;
||data.tank_level_switch_position&lt;br /&gt;
||tankLevelSwitchPosition&lt;br /&gt;
|-&lt;br /&gt;
||tank level switch validity indicator&lt;br /&gt;
||boolean&lt;br /&gt;
||data.tank_level_switch_invalid&lt;br /&gt;
||tankLevelSwitchInvalid&lt;br /&gt;
|-&lt;br /&gt;
||tank level switch position stabilized&lt;br /&gt;
||boolean&lt;br /&gt;
||data.tank_level_switch_position_stabilized&lt;br /&gt;
||tankLevelSwitchPositionStabilized&lt;br /&gt;
|-&lt;br /&gt;
||surface ventilation status&lt;br /&gt;
||boolean&lt;br /&gt;
||data.surface_ventilation_status&lt;br /&gt;
||surfaceVentilationStatus&lt;br /&gt;
|-&lt;br /&gt;
||ambient temperature&lt;br /&gt;
||floating point number&lt;br /&gt;
||data.ambient_temperature&lt;br /&gt;
||ambientTemperature&lt;br /&gt;
|-&lt;br /&gt;
||ambient humidity&lt;br /&gt;
||floating point number&lt;br /&gt;
||data.ambient_humidity&lt;br /&gt;
||ambientHumidity&lt;br /&gt;
|-&lt;br /&gt;
||heater status&lt;br /&gt;
||boolean&lt;br /&gt;
||data.heater_status&lt;br /&gt;
||heaterStatus&lt;br /&gt;
|-&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Requirements for ventilation feature ===&lt;br /&gt;
==== Ventilation set values ====&lt;br /&gt;
The API shall provide an endpoint communicating from the server to the client the ventilation set values read from the table &amp;lt;code&amp;gt;ventilation_set_vals&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
The corresponding SQL query is:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;SELECT ventilation_switch_off_temp, ventilation_switch_on_temp FROM ventilation_set_vals;&amp;quot;&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
!|Signal name&lt;br /&gt;
!|Signal format&lt;br /&gt;
!|Database column name&lt;br /&gt;
|-&lt;br /&gt;
||ventilation switch off temperature&lt;br /&gt;
||floating point number&lt;br /&gt;
||ventilation_set_vals.ventilation_switch_off_temp&lt;br /&gt;
|-&lt;br /&gt;
||ventilation switch on temperature&lt;br /&gt;
||floating point number&lt;br /&gt;
||ventilation_set_vals.ventilation_switch_on_temp&lt;br /&gt;
|-&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
The API shall provide an endpoint which allows the client to update both ventilation set values.&lt;br /&gt;
The corresponding SQL query is:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;UPDATE ventilation_set_vals SET ventilation_switch_on_temp=[ventilationSwitchOnTemp], ventilation_switch_off_temp=[ventilationSwitchOffTemp];&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Note: ''The database shall contain only one entry in the table. In case there are multiple entries, then the query will overwrite the data of all entries. This is intentional.''&lt;br /&gt;
&lt;br /&gt;
== Architecture ==&lt;br /&gt;
The API is distributed over the following files:&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
!|File name&lt;br /&gt;
!|Content description&lt;br /&gt;
|-&lt;br /&gt;
||api.php&lt;br /&gt;
||Main API functionality&lt;br /&gt;
|-&lt;br /&gt;
||db.php&lt;br /&gt;
||Adapter for connecting to SQL database&lt;br /&gt;
|-&lt;br /&gt;
||functions.php&lt;br /&gt;
||helper functionality repeatedly used throughout the API&lt;br /&gt;
|-&lt;br /&gt;
||test_reset_db.php&lt;br /&gt;
||functionality to reset and initialise the database with mock data: Do not deploy this file in productive environment!&lt;br /&gt;
|-&lt;br /&gt;
||test.php&lt;br /&gt;
||functionality to test access to database for development purposed. Do not deploy this file in productive environment!&lt;br /&gt;
|-&lt;br /&gt;
||test_generate_mock_data.php&lt;br /&gt;
||functionality to create mock data for front end testing. Do not deploy this file in productive environment!&lt;br /&gt;
|-&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Implementation ==&lt;br /&gt;
The behaviour of the &amp;lt;code&amp;gt;api.php&amp;lt;/code&amp;gt; is controlled by the first &amp;lt;code&amp;gt;action&amp;lt;/code&amp;gt; argument provided via &amp;lt;code&amp;gt;GET&amp;lt;/code&amp;gt; method which can assume the following values:&lt;br /&gt;
=== check_auth ===&lt;br /&gt;
The API responds with a JSON object containing the following data:&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
!|Label&lt;br /&gt;
!|Value&lt;br /&gt;
|-&lt;br /&gt;
||status&lt;br /&gt;
||message&lt;br /&gt;
|-&lt;br /&gt;
||authorized&lt;br /&gt;
||Credentials valid&lt;br /&gt;
|-&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== test_hash ===&lt;br /&gt;
The API responds with a JSON object containing the following data:&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
!|Label&lt;br /&gt;
!|Value&lt;br /&gt;
|-&lt;br /&gt;
||input&lt;br /&gt;
||hash&lt;br /&gt;
|-&lt;br /&gt;
||[Input value provided]&lt;br /&gt;
||[Hash calculated from the input value]&lt;br /&gt;
|-&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== load_overview_signals ===&lt;br /&gt;
When using this parameter value, the API provides the overview data in JSON format as per [[REST_API#Requirements_for_overview_feature|above requirement]].&lt;br /&gt;
&lt;br /&gt;
=== load_balling_log ===&lt;br /&gt;
When using this parameter value, the API provides the balling dosing log as per [[REST_API#Balling_dosing_log|above requirements]].&lt;br /&gt;
&lt;br /&gt;
The second parameter &amp;lt;code&amp;gt;period&amp;lt;/code&amp;gt; determines the period: &amp;lt;code&amp;gt;7d&amp;lt;/code&amp;gt; for the last 7 days, otherwise only the last 24 hours.&lt;br /&gt;
&lt;br /&gt;
=== load_balling_set_vals ===&lt;br /&gt;
When using this parameter value, the API provides the Balling mineral dosing set values of all configured pumps in JSON format as per [[REST_API#Balling_set_values|above requirement]].&lt;br /&gt;
&lt;br /&gt;
=== update_balling_set_vals ===&lt;br /&gt;
When using this parameter value, the API checks if the required &amp;lt;code&amp;gt;POST&amp;lt;/code&amp;gt; parameters were provided:&lt;br /&gt;
* dosingVolume&lt;br /&gt;
* pumpId&lt;br /&gt;
&lt;br /&gt;
If the parameters are provided, the API will issue the SQL statement to update the dosing volume for the dedicated pump as per [[REST_API#Balling_set_values|above requirements]].&lt;br /&gt;
&lt;br /&gt;
=== load_feed_log ===&lt;br /&gt;
When using this parameter value, the API provides the balling dosing log as per [[REST_API#Feed_log|above requirements]].&lt;br /&gt;
&lt;br /&gt;
The second parameter &amp;lt;code&amp;gt;period&amp;lt;/code&amp;gt; determines the period: &amp;lt;code&amp;gt;7d&amp;lt;/code&amp;gt; for the last 7 days, otherwise only the last 24 hours.&lt;br /&gt;
&lt;br /&gt;
=== load_feed_profiles ===&lt;br /&gt;
When using this parameter value, the API provides all existing feed profile in JSON format as per [[REST_API#Feed_profiles|above requirement]].&lt;br /&gt;
&lt;br /&gt;
=== update_feed_profile ===&lt;br /&gt;
When using this parameter value, the API will check if the &amp;lt;code&amp;gt;profileId&amp;lt;/code&amp;gt; is provided as POST parameter.&lt;br /&gt;
&lt;br /&gt;
All other parameters provided will be type checked if they are integer or string types.&lt;br /&gt;
&lt;br /&gt;
The API will issue the SQL statement to update the specific feed profile identified by &amp;lt;code&amp;gt;profileId&amp;lt;/code&amp;gt; as per [[REST_API#Feed_profiles|above requirement]].&lt;br /&gt;
&lt;br /&gt;
=== create_feed_profile ===&lt;br /&gt;
When using this parameter value, the query will do the following steps [[REST_API#create_feed_profile|as per above requirement]]:&lt;br /&gt;
* check if the parameter &amp;lt;code&amp;gt;profileName&amp;lt;/code&amp;gt; is provided via &amp;lt;code&amp;gt;POST&amp;lt;/code&amp;gt; method.&lt;br /&gt;
* issue an SQL statement to check if a profile with that name already exists in the database, issue an error response and abort further execution if indeed there already is such a profile&lt;br /&gt;
* issue and SQL statement to insert an empty feed profile into the data base using &amp;lt;code&amp;gt;profileName&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== execute_feed_profile ===&lt;br /&gt;
When using this parameter value, the API will check for the additional parameter &amp;lt;code&amp;gt;profileId&amp;lt;/code&amp;gt; provided via &amp;lt;code&amp;gt;GET&amp;lt;/code&amp;gt; method.&lt;br /&gt;
If the parameter is provided, the API will issue an SQL statement to check if the database contains a matching feed profile, issue an error message and abort execution if there is no such profile.&lt;br /&gt;
If the profile exists, the API will execute the external program &amp;lt;code&amp;gt;aquarium_client&amp;lt;/code&amp;gt; using the keywords &amp;lt;code&amp;gt;feed&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;execute&amp;lt;/code&amp;gt; and the feed profile ID as parameter as per [[REST_API#Feed_profiles|above requirement]].&lt;br /&gt;
&lt;br /&gt;
=== delete_feed_profile ===&lt;br /&gt;
When using this parameter value, the API checks if the parameter &amp;lt;code&amp;gt;profileId&amp;lt;/code&amp;gt; is provided via &amp;lt;code&amp;gt;GET&amp;lt;/code&amp;gt; method. If the parameter is provided, the API will issue an SQL statement to delete the matching feed profile from the database as per [[REST_API#Feed_profiles|above requirements]].&lt;br /&gt;
&lt;br /&gt;
=== load_feed_schedule ===&lt;br /&gt;
When using this parameter value, the API will load the feed schedule entries as per [[REST_API#Feed_schedules|above requirement]].&lt;br /&gt;
&lt;br /&gt;
The SQL query uses a &amp;lt;code&amp;gt;LEFT JOIN&amp;lt;/code&amp;gt; where the profile name is read from the &amp;lt;code&amp;gt;feed_profile&amp;lt;/code&amp;gt; table identified by the &amp;lt;code&amp;gt;profile_id&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== create_feed_schedule_entry ===&lt;br /&gt;
When using this parameter value, the API will check if the following parameters were provided via &amp;lt;code&amp;gt;POST&amp;lt;/code&amp;gt; method:&lt;br /&gt;
* &amp;lt;code&amp;gt;profileId&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;timestamp&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;scheduleIsWeekly&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;scheduleIsDaily&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Note: Functionality for &amp;lt;code&amp;gt;scheduleIsWeekly&amp;lt;/code&amp;gt; is not implemented as of December 2025.&lt;br /&gt;
&lt;br /&gt;
When the additional parameters are provided, the API will issue an SQL statement to check if a matching feed schedule entry already exists and abort further execution if that is the case.&lt;br /&gt;
&lt;br /&gt;
Otherwise, the API will issue a further SQL statement to insert the feed schedule entry into the database as per [[REST_API#Feed_schedules|above requirement]].&lt;br /&gt;
&lt;br /&gt;
=== update_feed_schedule_entry ===&lt;br /&gt;
When using this parameter value, the API will check if the following parameters were provided via &amp;lt;code&amp;gt;POST&amp;lt;/code&amp;gt; method and abort execution if this is not the case:&lt;br /&gt;
* &amp;lt;code&amp;gt;timestamp&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;profileId&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;isWeekly&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;isDaily&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
When the parameters are provided, the API will execute one SQL transaction containing two statements:&lt;br /&gt;
* The first statement will delete the feed schedule entry identified by the timestamp.&lt;br /&gt;
* The second statement will insert a new feed schedule entry using the additionally provided parameter.&lt;br /&gt;
&lt;br /&gt;
This will effectively update an existing feed schedule entry as per [[REST_API#Feed_schedules|above requirement]].&lt;br /&gt;
&lt;br /&gt;
If the transaction throws an exception (when one of the SQL statements fails), the API will trigger the rollback of the transaction.&lt;br /&gt;
&lt;br /&gt;
=== delete_feed_schedule_entry ===&lt;br /&gt;
When using this parameter value, the API will check if the &amp;lt;code&amp;gt;timestamp&amp;lt;/code&amp;gt; was provided using &amp;lt;code&amp;gt;POST&amp;lt;/code&amp;gt; method and abort execution if this is not the case.&lt;br /&gt;
&lt;br /&gt;
When the additional timestamp parameter is provided, the API will issue an SQL statement to delete the corresponding feed schedule entry as per [[REST_API#Feed_schedules|above requirement]].&lt;br /&gt;
&lt;br /&gt;
=== load_heating_set_vals ===&lt;br /&gt;
When using this parameter value, the API will load the heating control set values as per [[REST_API#Heating_set_values|above requirement]].&lt;br /&gt;
&lt;br /&gt;
=== update_heating_set_vals ===&lt;br /&gt;
When using this parameter value, the API will check if the following parameters were provided via &amp;lt;code&amp;gt;POST&amp;lt;/code&amp;gt; method and will abort execution if this is not the case:&lt;br /&gt;
* heatingSwitchOnTemp&lt;br /&gt;
* heatingSwitchOffTemp&lt;br /&gt;
&lt;br /&gt;
When the parameters are provided, the API will issue an SQL statement update the heating control set values as per [[REST_API#Heating_set_values|above requirement]].&lt;br /&gt;
&lt;br /&gt;
=== load_heating_stats ===&lt;br /&gt;
When using this parameter value, the API will load the heating statistical data as per [[REST_API#Heating_statistical_data|above requirement]].&lt;br /&gt;
&lt;br /&gt;
=== load_refill_log ===&lt;br /&gt;
When using this parameter value, the API provides the refill log as per [[REST_API#Refill_log|above requirements]].&lt;br /&gt;
&lt;br /&gt;
The second parameter &amp;lt;code&amp;gt;period&amp;lt;/code&amp;gt; determines the period: &amp;lt;code&amp;gt;7d&amp;lt;/code&amp;gt; for the last 7 days, otherwise only the last 24 hours.&lt;br /&gt;
&lt;br /&gt;
=== load_refill_state ===&lt;br /&gt;
When this parameter value is provided, the API will load the refill control state information from &amp;lt;code&amp;gt;/var/local/aquarium-ctrl/refillctrl&amp;lt;/code&amp;gt; as per [[REST_API#Refill_controller_state|above requirement]].&lt;br /&gt;
&lt;br /&gt;
=== set_refill_state ===&lt;br /&gt;
When using this parameter value, the API will check if an additional parameter named &amp;lt;code&amp;gt;command&amp;lt;/code&amp;gt; was provided via &amp;lt;code&amp;gt;GET&amp;lt;/code&amp;gt; method.&lt;br /&gt;
&lt;br /&gt;
The API will check if the parameter has a valid value. Valid values are:&lt;br /&gt;
* reset&lt;br /&gt;
* start&lt;br /&gt;
* stop&lt;br /&gt;
&lt;br /&gt;
When the additional parameter is provided with a valid value, the API will execute the command &amp;lt;code&amp;gt;/usr/local/bin/aquarium_client refill [command]&amp;lt;/code&amp;gt;. This will update the refill control state as per [[REST_API#Refill_controller_state]] above requirement.&lt;br /&gt;
&lt;br /&gt;
When no valid value is provided, the API will abort execution.&lt;br /&gt;
&lt;br /&gt;
=== load_actuator_schedule ===&lt;br /&gt;
When using this parameter value, the API will load the actuator schedule as per [[REST_API#Requirements_for_actuator_schedule_feature|above requirement]].&lt;br /&gt;
&lt;br /&gt;
For testing purposes, the schedule entries are provided in alphabetical order.&lt;br /&gt;
&lt;br /&gt;
=== update_actuator_schedule ===&lt;br /&gt;
When using this parameter value, the API will check if the following parameters were provided via &amp;lt;code&amp;gt;POST&amp;lt;/code&amp;gt; method and abort execution if this is not the case:&lt;br /&gt;
* ballingRangeStartTime&lt;br /&gt;
* ballingRangeFinishTime&lt;br /&gt;
* ballingRangeIsActive&lt;br /&gt;
* refillRangeStartTime&lt;br /&gt;
* refillRangeFinishTime&lt;br /&gt;
* refillRangeIsActive,&lt;br /&gt;
* ventilationRangeStartTime&lt;br /&gt;
* ventilationRangeFinishTime&lt;br /&gt;
* ventilationRangeIsActive&lt;br /&gt;
* heatingRangeStartTime&lt;br /&gt;
* heatingRangeFinishTime&lt;br /&gt;
* heatingRangeIsActive&lt;br /&gt;
&lt;br /&gt;
When all parameters are provided, the API will initiate a transaction consisting of four SQL statements for each actuator schedule.&lt;br /&gt;
&lt;br /&gt;
The SQL statement will update the actuator schedule as per above [[REST_API#Requirements_for_actuator_schedule_feature|requirement]].&lt;br /&gt;
&lt;br /&gt;
=== load_time_data ===&lt;br /&gt;
When using this parameter value, the API will load the time data as per [[REST_API#Requirements_for_time_data_feature|above requirement]].&lt;br /&gt;
&lt;br /&gt;
=== load_ventilation_set_vals ===&lt;br /&gt;
When using this parameter value, the API will load the ventilation control set values as per [[REST_API#Ventilation_set_values|above requirement]].&lt;br /&gt;
&lt;br /&gt;
=== update_ventilation_set_vals ===&lt;br /&gt;
When using this parameter value, the API will check if the following parameters were provided via &amp;lt;code&amp;gt;POST&amp;lt;/code&amp;gt; method and will abort execution if this is not the case:&lt;br /&gt;
* ventilationSwitchOnTemp&lt;br /&gt;
* ventilationSwitchOffTemp&lt;br /&gt;
&lt;br /&gt;
When the parameters are provided, the API will issue an SQL statement update the ventilation control set values as per [[REST_API#Ventilation_set_values|above requirement]].&lt;br /&gt;
&lt;br /&gt;
== Testing ==&lt;br /&gt;
Postman is used for testing of the API.&lt;br /&gt;
&lt;br /&gt;
The requests aggregated in collections.&lt;br /&gt;
&lt;br /&gt;
Each collection (except for the General collection) starts with a request to reset the database (truncating all tables and adding mock data).&lt;br /&gt;
&lt;br /&gt;
The server-based script for resetting the database cannot be run in parallel - limiting the ability to run all tests in parallel.&lt;br /&gt;
&lt;br /&gt;
Test execution is manual per collection.&lt;br /&gt;
&lt;br /&gt;
Reference values are stored in the Variables section of each collection.&lt;br /&gt;
&lt;br /&gt;
=== Feed collection ===&lt;br /&gt;
The collection executes the following requests:&lt;br /&gt;
* Reset database (GET)&lt;br /&gt;
* Load feed log (GET)&lt;br /&gt;
* Initial Load feed profiles (GET)&lt;br /&gt;
* Update feed profile (POST)&lt;br /&gt;
* Secondary Load feed profiles (GET): This request will verify if the previous request could modify the database successfully.&lt;br /&gt;
* Initial Create feed profile (POST)&lt;br /&gt;
* Secondary Create feed profile (POST): This request will verify if trying to create a new feed profile with an already existing name is rejected.&lt;br /&gt;
* Execute feed profile (GET)&lt;br /&gt;
* Create feed schedule entry (POST)&lt;br /&gt;
* Load feed schedule (GET)&lt;br /&gt;
* Update feed schedule entry (POST)&lt;br /&gt;
* Delete feed schedule entry (POST)&lt;br /&gt;
* Delete feed profile (GET)&lt;br /&gt;
&lt;br /&gt;
=== General collection ===&lt;br /&gt;
The collection executes the following requests:&lt;br /&gt;
* Hash test (GET)&lt;br /&gt;
* Check authorisation (GET)&lt;br /&gt;
&lt;br /&gt;
=== Balling collection ===&lt;br /&gt;
The collection executes the following requests:&lt;br /&gt;
* Reset database (GET)&lt;br /&gt;
* Load Balling log (GET)&lt;br /&gt;
* Initial Load Balling set values (GET)&lt;br /&gt;
* Update Balling set values (POST)&lt;br /&gt;
* Secondary Load Balling set values (GET): This request will verify if the previous request could successfully modify the database.&lt;br /&gt;
&lt;br /&gt;
=== Overview collection ===&lt;br /&gt;
The collection executes the following requests:&lt;br /&gt;
* Reset database (GET)&lt;br /&gt;
* Load overview signals (GET)&lt;br /&gt;
&lt;br /&gt;
=== Refill collection ===&lt;br /&gt;
The collection executes the following requests:&lt;br /&gt;
* Reset database (GET)&lt;br /&gt;
* Load refill log (GET)&lt;br /&gt;
* Load refill control state (GET)&lt;br /&gt;
* Set refill control state (GET)&lt;br /&gt;
&lt;br /&gt;
=== Actuator schedule collection ===&lt;br /&gt;
The collection executes the following requests:&lt;br /&gt;
* Reset database (GET)&lt;br /&gt;
* Initial Load actuator schedule (GET)&lt;br /&gt;
* Update actuator schedule (POST)&lt;br /&gt;
* Secondary Load actuator schedule (GET): This request will verify if the previous request could successfully modify the database.&lt;br /&gt;
&lt;br /&gt;
=== Time data collection ===&lt;br /&gt;
The collection executes the following requests:&lt;br /&gt;
* Reset database (GET)&lt;br /&gt;
* Load time data (GET)&lt;br /&gt;
&lt;br /&gt;
=== Ventilation collection ===&lt;br /&gt;
The collection executes the following requests:&lt;br /&gt;
* Reset database (GET)&lt;br /&gt;
* Initial Load ventilation set values (GET)&lt;br /&gt;
* Update ventilation set values (POST)&lt;br /&gt;
* Secondary Load ventilation set values (GET): This request will check if the previous request could successfully modify the database.&lt;br /&gt;
&lt;br /&gt;
=== Heating collection ===&lt;br /&gt;
The collection executes the following requests:&lt;br /&gt;
* Reset database&lt;br /&gt;
* Initial Load heating set values (GET)&lt;br /&gt;
* Update heating set values (POST)&lt;br /&gt;
* Secondary Load heating set values (GET): This request will verify if the previous request could successfully modify the database.&lt;br /&gt;
* Load heating statistics (GET)&lt;/div&gt;</summary>
		<author><name>Uwe</name></author>
	</entry>
	<entry>
		<id>http://217.79.180.177/mediawiki/index.php?title=REST_API&amp;diff=458</id>
		<title>REST API</title>
		<link rel="alternate" type="text/html" href="http://217.79.180.177/mediawiki/index.php?title=REST_API&amp;diff=458"/>
		<updated>2026-04-28T18:44:10Z</updated>

		<summary type="html">&lt;p&gt;Uwe: /* Feed log */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Requirements ==&lt;br /&gt;
&lt;br /&gt;
=== General requirements ===&lt;br /&gt;
The API shall communicate with mobile apps and dynamic webpage.&lt;br /&gt;
&lt;br /&gt;
For each request, the API shall validate the credentials (user, password).&lt;br /&gt;
&lt;br /&gt;
The credentials of authorised individuals are stored in the table 'users'.&lt;br /&gt;
* The name is stored as clear text.&lt;br /&gt;
* A hash value of the password is stored in the table. &lt;br /&gt;
&lt;br /&gt;
The API shall compute the hash function of the password when validating the credentials.&lt;br /&gt;
&lt;br /&gt;
For testing purposes, the API shall provide an endpoint which calculates the hash function of a string given as parameter.&lt;br /&gt;
&lt;br /&gt;
The API shall provide the data for all subsequent queries in JSON format.&lt;br /&gt;
&lt;br /&gt;
Timestamps shall have the format: YYYY-MM-DD hh:mm:ss&lt;br /&gt;
&lt;br /&gt;
Placeholders in subsequent SQL queries are described in brackets: &amp;lt;code&amp;gt;[placeholder]&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Coding conventions ====&lt;br /&gt;
The URL action keys shall use snake_case.&lt;br /&gt;
&lt;br /&gt;
The URL parameters sent by the client shall use camelCase.&lt;br /&gt;
&lt;br /&gt;
The JSON keys (responses from the API) shall use camelCase.&lt;br /&gt;
&lt;br /&gt;
The database columns shall use snake_case.&lt;br /&gt;
&lt;br /&gt;
The php variables shall use camelCase.&lt;br /&gt;
&lt;br /&gt;
Filenames shall use all minor letters or kebab-case.&lt;br /&gt;
* Exception: Executables may use snake_case.&lt;br /&gt;
&lt;br /&gt;
=== Requirements for overview feature ===&lt;br /&gt;
&lt;br /&gt;
The API shall provide an endpoint for informing the client about the validity of the credentials.&lt;br /&gt;
&lt;br /&gt;
The API shall provide an endpoint communicating from the server to the client a set of floating point data read from a set of files:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
!|Signal name&lt;br /&gt;
!|Signal format&lt;br /&gt;
!|Signal source&lt;br /&gt;
!|API response&lt;br /&gt;
|-&lt;br /&gt;
||timestamp&lt;br /&gt;
||string&lt;br /&gt;
||/var/local/aquarium-ctrl/aquarium-ctrl-ts&lt;br /&gt;
||timestamp&lt;br /&gt;
|-&lt;br /&gt;
||water temperature&lt;br /&gt;
||floating point number&lt;br /&gt;
||/var/local/aquarium-ctrl/atlsscntfc-temp&lt;br /&gt;
||waterTemperature&lt;br /&gt;
|-&lt;br /&gt;
||filtered water temperature&lt;br /&gt;
||floating point number&lt;br /&gt;
||/var/local/aquarium-ctrl/atlsscntfc-tempfltrd&lt;br /&gt;
||filteredWaterTemperature&lt;br /&gt;
|-&lt;br /&gt;
||pH value&lt;br /&gt;
||floating point number&lt;br /&gt;
||/var/local/aquarium-ctrl/atlsscntfc-ph&lt;br /&gt;
||phValue&lt;br /&gt;
|-&lt;br /&gt;
||filtered pH value&lt;br /&gt;
||floating point number&lt;br /&gt;
||/var/local/aquarium-ctrl/atlsscntfc-phfltrd&lt;br /&gt;
||filteredPhValue&lt;br /&gt;
|-&lt;br /&gt;
||conductivity&lt;br /&gt;
||floating point number&lt;br /&gt;
||/var/local/aquarium-ctrl/atlsscntfc-conduc&lt;br /&gt;
||conductivity&lt;br /&gt;
|-&lt;br /&gt;
||filtered conductivity&lt;br /&gt;
||floating point number&lt;br /&gt;
||/var/local/aquarium-ctrl/atlsscntfc-conducfltrd&lt;br /&gt;
||filteredConductivity&lt;br /&gt;
|-&lt;br /&gt;
||filtered water temperature&lt;br /&gt;
||floating point number&lt;br /&gt;
||/var/local/aquarium-ctrl/atlsscntfc-conducfltrd&lt;br /&gt;
||filteredWaterTemperature&lt;br /&gt;
|-&lt;br /&gt;
||tank level switch position&lt;br /&gt;
||floating point number&lt;br /&gt;
||/var/local/aquarium-ctrl/tnklvlsswtch&lt;br /&gt;
||tankLevelSwitchPosition&lt;br /&gt;
|-&lt;br /&gt;
||surface ventilation status&lt;br /&gt;
||string&lt;br /&gt;
||/var/local/aquarium-ctrl/srfcvntltn&lt;br /&gt;
||surfaceVentilationStatus&lt;br /&gt;
|-&lt;br /&gt;
||ambient temperature&lt;br /&gt;
||floating point number&lt;br /&gt;
||/var/local/aquarium-ctrl/ambtemp&lt;br /&gt;
||ambientTemperature&lt;br /&gt;
|-&lt;br /&gt;
||ambient humidity&lt;br /&gt;
||floating point number&lt;br /&gt;
||/var/local/aquarium-ctrl/ambhum&lt;br /&gt;
||ambientHumidity&lt;br /&gt;
|-&lt;br /&gt;
||heating status&lt;br /&gt;
||string&lt;br /&gt;
||/var/local/aquarium-ctrl/htng&lt;br /&gt;
||heatingStatus&lt;br /&gt;
|-&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Requirements for Balling feature ===&lt;br /&gt;
&lt;br /&gt;
==== Balling dosing log ====&lt;br /&gt;
The API shall provide an endpoint communicating from the server to the client the Balling dosing events read from the tables &amp;lt;code&amp;gt;balling_log&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;balling_set_vals&amp;lt;/code&amp;gt; of either the last 24 hours or the last 7 days or of a specific date depending on parameter provided by the client.&lt;br /&gt;
The corresponding SQL query (for a period of one day) is:&lt;br /&gt;
&lt;br /&gt;
For periods:&lt;br /&gt;
&amp;lt;code&amp;gt;SELECT balling_log.Timestamp, balling_log.pumpid, balling_log.dosingvolume, balling_set_vals.label FROM balling_log LEFT JOIN balling_set_vals ON balling_log.pumpid=balling_set_vals.pumpid WHERE Timestamp &amp;gt; (NOW() - INTERVAL 1 DAY) ORDER BY Timestamp&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The query for a specific date is:&lt;br /&gt;
&amp;lt;code&amp;gt;SELECT b.timestamp, b.pump_id, b.dosing_volume, s.label FROM balling_log AS b LEFT JOIN balling_set_vals AS s ON b.pump_id = s.pump_id WHERE DATE(b.timestamp) = ? ORDER BY b.timestamp DESC;&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
!|Signal name&lt;br /&gt;
!|Signal format&lt;br /&gt;
!|Database column name&lt;br /&gt;
!|API response&lt;br /&gt;
|-&lt;br /&gt;
||timestamp&lt;br /&gt;
||string&lt;br /&gt;
||balling_log.Timestamp&lt;br /&gt;
||timestamp&lt;br /&gt;
|-&lt;br /&gt;
||pump id&lt;br /&gt;
||integer number&lt;br /&gt;
||balling_log.pumpid&lt;br /&gt;
||pumpId&lt;br /&gt;
|-&lt;br /&gt;
||dosing volume&lt;br /&gt;
||floating point number&lt;br /&gt;
||balling_log.dosingvolume&lt;br /&gt;
||dosingVolume&lt;br /&gt;
|-&lt;br /&gt;
||label&lt;br /&gt;
||string&lt;br /&gt;
||balling_set_vals.label&lt;br /&gt;
||label&lt;br /&gt;
|-&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
==== Balling set values ====&lt;br /&gt;
The API shall provide an endpoint communicating from the server to the client the Balling dosing set values read from the table &amp;lt;code&amp;gt;balling_set_vals&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
The corresponding SQL query is:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;SELECT pumpid, dosingvolume, label FROM balling_set_vals;&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
!|Signal name&lt;br /&gt;
!|Signal format&lt;br /&gt;
!|Database column name&lt;br /&gt;
|-&lt;br /&gt;
||pump id&lt;br /&gt;
||integer number&lt;br /&gt;
||balling_set_vals.pumpid&lt;br /&gt;
|-&lt;br /&gt;
||dosing volume&lt;br /&gt;
||floating point number&lt;br /&gt;
||balling_set_vals.dosing_volume&lt;br /&gt;
|-&lt;br /&gt;
||label&lt;br /&gt;
||string&lt;br /&gt;
||balling_set_vals.label&lt;br /&gt;
|-&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
The API shall provide an endpoint which allows the client to update the dosing volume of an existing dosing set value identified by the pump id.&lt;br /&gt;
The corresponding SQL query is:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;UPDATE balling_set_vals SET dosing_volume=&amp;quot;[dosingVolume]&amp;quot; WHERE pump_id=&amp;quot;[pumpId]&amp;quot;;&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Requirements for feed feature ===&lt;br /&gt;
==== Feed log ====&lt;br /&gt;
The API shall provide an endpoint communicating from the server to the client the feed events read from the tables &amp;lt;code&amp;gt;feed_log&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;feed_profiles&amp;lt;/code&amp;gt; of either the last 24 hours or the last 7 days depending on parameter provided by the client.&lt;br /&gt;
The corresponding SQL query (for a period of one day) is:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;SELECT fl.timestamp, fl.feeder_on_time, fp.profile_name, fl.profile_id FROM feed_log AS fl LEFT JOIN feed_profiles AS fp ON fl.profile_id = fp.profile_id WHERE timestamp &amp;gt; (NOW() - INTERVAL 1 DAY) ORDER BY timestamp;&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The query for a specific date is:&lt;br /&gt;
&amp;lt;code&amp;gt;SELECT fl.timestamp, fl.feeder_on_time, fl.profile_name, fl.profile_id FROM feed_log AS fl LEFT JOIN feed_profiles AS fp ON fl.profile_id = fp.profile_id WHERE DATE(fl.timestamp) = ? ORDER BY fl.timestamp DESC &amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
!|Signal name&lt;br /&gt;
!|Signal format&lt;br /&gt;
!|Database column name&lt;br /&gt;
|-&lt;br /&gt;
||timestamp&lt;br /&gt;
||string&lt;br /&gt;
||feed_log.timestamp&lt;br /&gt;
|-&lt;br /&gt;
||feeder on time&lt;br /&gt;
||floating point number&lt;br /&gt;
||feed_log.feeder_on_time&lt;br /&gt;
|-&lt;br /&gt;
||feed profile name&lt;br /&gt;
||string&lt;br /&gt;
||feed_profiles.profile_name&lt;br /&gt;
|-&lt;br /&gt;
||feed profile id&lt;br /&gt;
||integer numer&lt;br /&gt;
||feed_log.profile_id&lt;br /&gt;
|-&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
==== Feed profiles ====&lt;br /&gt;
A feed profile consists of general information (ID, name) and 10 groups of repetitive data where each group contains a &amp;lt;code&amp;gt;pause&amp;lt;/code&amp;gt; section and a &amp;lt;code&amp;gt;feed&amp;lt;/code&amp;gt; section.&lt;br /&gt;
&lt;br /&gt;
The API shall provide an endpoint communicating from the server to the client the feed profiles read from the table &amp;lt;code&amp;gt;feedprofiles&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
!|Signal name&lt;br /&gt;
!|Signal format&lt;br /&gt;
!|Database column name&lt;br /&gt;
!|API response&lt;br /&gt;
|-&lt;br /&gt;
||profile id&lt;br /&gt;
||integer number&lt;br /&gt;
||feed_profiles.profile_id&lt;br /&gt;
||profileId&lt;br /&gt;
|-&lt;br /&gt;
||profile name&lt;br /&gt;
||string&lt;br /&gt;
||feed_profiles.profile_name&lt;br /&gt;
||profileName&lt;br /&gt;
|-&lt;br /&gt;
||pause 01 duration&lt;br /&gt;
||integer&lt;br /&gt;
||feed_profiles.pause_01_duration&lt;br /&gt;
||pause01Duration&lt;br /&gt;
|-&lt;br /&gt;
||pause 01 skimmer target state&lt;br /&gt;
||boolean&lt;br /&gt;
||feed_profiles.pause_01_skimmer&lt;br /&gt;
||pause01Skimmer&lt;br /&gt;
|-&lt;br /&gt;
||pause 01 main pump #1 target state&lt;br /&gt;
||boolean&lt;br /&gt;
||feed_profiles.pause_01_main_pump1&lt;br /&gt;
||pause01MainPump1&lt;br /&gt;
|-&lt;br /&gt;
||pause 01 main pump #2 target state&lt;br /&gt;
||boolean&lt;br /&gt;
||feed_profiles.pause_01_main_pump2&lt;br /&gt;
||pause01MainPump2&lt;br /&gt;
|-&lt;br /&gt;
||pause 01 aux. pump #1 target state&lt;br /&gt;
||boolean&lt;br /&gt;
||feed_profiles.pause_01_aux_pump1&lt;br /&gt;
||pause01AuxPump1&lt;br /&gt;
|-&lt;br /&gt;
||pause 01 aux. pump #2 target state&lt;br /&gt;
||boolean&lt;br /&gt;
||feed_profiles.pause_01_aux_pump2&lt;br /&gt;
||pause01AuxPump2&lt;br /&gt;
|-&lt;br /&gt;
||feed 01 duration&lt;br /&gt;
||integer&lt;br /&gt;
||feed_profiles.feed_01_duration&lt;br /&gt;
||feed01Duration&lt;br /&gt;
|-&lt;br /&gt;
||feed 01 skimmer target state&lt;br /&gt;
||boolean&lt;br /&gt;
||feed_profiles.feed_01_skimmer&lt;br /&gt;
||feed01Skimmer&lt;br /&gt;
|-&lt;br /&gt;
||feed 01 main pump #1 target state&lt;br /&gt;
||boolean&lt;br /&gt;
||feed_profiles.feed_01_main_pump1&lt;br /&gt;
||feed01MainPump1&lt;br /&gt;
|-&lt;br /&gt;
||feed 01 main pump #2 target state&lt;br /&gt;
||boolean&lt;br /&gt;
||feed_profiles.feed_01_main_pump2&lt;br /&gt;
||feed01MainPump2&lt;br /&gt;
|-&lt;br /&gt;
||feed 01 aux. pump #1 target state&lt;br /&gt;
||boolean&lt;br /&gt;
||feed_profiles.feed_01_aux_pump1&lt;br /&gt;
||feed01AuxPump1&lt;br /&gt;
|-&lt;br /&gt;
||feed 01 aux. pump #2 target state&lt;br /&gt;
||boolean&lt;br /&gt;
||feed_profiles.feed_01_aux_pump2&lt;br /&gt;
||feed01AuxPump2&lt;br /&gt;
|-&lt;br /&gt;
||...&lt;br /&gt;
||...&lt;br /&gt;
||...&lt;br /&gt;
|-&lt;br /&gt;
||pause 10 duration&lt;br /&gt;
||integer&lt;br /&gt;
||feed_profiles.pause_10_duration&lt;br /&gt;
||pause10Duration&lt;br /&gt;
|-&lt;br /&gt;
||pause 10 skimmer target state&lt;br /&gt;
||boolean&lt;br /&gt;
||feed_profiles.pause_10_skimmer&lt;br /&gt;
||pause10Skimmer&lt;br /&gt;
|-&lt;br /&gt;
||pause 10 main pump #1 target state&lt;br /&gt;
||boolean&lt;br /&gt;
||feed_profiles.pause_10_main_pump1&lt;br /&gt;
||pause10MainPump1&lt;br /&gt;
|-&lt;br /&gt;
||pause 10 main pump #2 target state&lt;br /&gt;
||boolean&lt;br /&gt;
||feed_profiles.pause_10_main_pump2&lt;br /&gt;
||pause10MainPump2&lt;br /&gt;
|-&lt;br /&gt;
||pause 10 aux. pump #1 target state&lt;br /&gt;
||boolean&lt;br /&gt;
||feed_profiles.pause_10_aux_pump1&lt;br /&gt;
||pause10AuxPump1&lt;br /&gt;
|-&lt;br /&gt;
||pause 10 aux. pump #2 target state&lt;br /&gt;
||boolean&lt;br /&gt;
||feed_profiles.pause_10_aux_pump2&lt;br /&gt;
||pause10AuxPump2&lt;br /&gt;
|-&lt;br /&gt;
||feed 10 duration&lt;br /&gt;
||integer&lt;br /&gt;
||feed_profiles.feed_10_duration&lt;br /&gt;
||feed10Duration&lt;br /&gt;
|-&lt;br /&gt;
||feed 10 skimmer target state&lt;br /&gt;
||boolean&lt;br /&gt;
||feed_profiles.feed_10_skimmer&lt;br /&gt;
||feed10Skimmer&lt;br /&gt;
|-&lt;br /&gt;
||feed 10 main pump #1 target state&lt;br /&gt;
||boolean&lt;br /&gt;
||feed_profiles.feed_10_main_pump1&lt;br /&gt;
||feed10MainPump1&lt;br /&gt;
|-&lt;br /&gt;
||feed 10 main pump #2 target state&lt;br /&gt;
||boolean&lt;br /&gt;
||feed_profiles.feed_10_main_pump2&lt;br /&gt;
||feed10MainPump2&lt;br /&gt;
|-&lt;br /&gt;
||feed 10 aux. pump #1 target state&lt;br /&gt;
||boolean&lt;br /&gt;
||feed_profiles.feed_10_aux_pump1&lt;br /&gt;
||feed10AuxPump1&lt;br /&gt;
|-&lt;br /&gt;
||feed 10 aux. pump #2 target state&lt;br /&gt;
||boolean&lt;br /&gt;
||feed_profiles.feed_10_aux_pump2&lt;br /&gt;
||feed10AuxPump2&lt;br /&gt;
|-&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
The API shall provide an endpoint which allows the client to update an existing feed profile identified by the profile id.&lt;br /&gt;
&lt;br /&gt;
The API shall provide an endpoint which allows the client to create a new profile.&lt;br /&gt;
&lt;br /&gt;
The client shall only specify the name of the new profile.&lt;br /&gt;
&lt;br /&gt;
If the feed profile already exists, the endpoint shall provide an error code and not overwrite any existing data in the database.&lt;br /&gt;
&lt;br /&gt;
The related SQL query is:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;INSERT INTO feed_profiles(profile_name) VALUES(&amp;quot;[profileName]&amp;quot;);&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The API shall provide an endpoint which allows the client to execute an existing profile.&lt;br /&gt;
&lt;br /&gt;
The client shall only specify the ID of the feed profile.&lt;br /&gt;
* The API shall check if the profile identified by the ID exists in the database and output an error if the profile already exists.&lt;br /&gt;
&lt;br /&gt;
If the profile exists, then the API shall execute a shell script:&lt;br /&gt;
&amp;lt;code&amp;gt;shell_exec(&amp;quot;/usr/local/bin/aquarium_client feed execute [profileId]&amp;quot;);&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The API shall provide an endpoint which allows the client to remove an existing profile.&lt;br /&gt;
&lt;br /&gt;
The client shall only specify the ID of the feed profile.&lt;br /&gt;
&lt;br /&gt;
The related SQL query is:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;DELETE FROM feed_profiles WHERE profile_id=&amp;quot;[profileId]&amp;quot;;&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Feed schedules ====&lt;br /&gt;
The API shall provide an endpoint communicating from the server to the client the feed schedule entries read from the tables &amp;lt;code&amp;gt;feed_schedule&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;feed_profiles&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
The related SQL query is:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;SELECT fs.timestamp, fs.profile_id, fp.profile_name, fs.is_weekly, fs.is_daily FROM feed_schedule AS fs LEFT JOIN feed_profiles AS fp ON fs.profile_id = fp.profile_id;&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
!|Signal name&lt;br /&gt;
!|Signal format&lt;br /&gt;
!|Database column name&lt;br /&gt;
|-&lt;br /&gt;
||timestamp&lt;br /&gt;
||string&lt;br /&gt;
||feed_schedule.timestamp&lt;br /&gt;
|-&lt;br /&gt;
||profile id&lt;br /&gt;
||integer number&lt;br /&gt;
||feed_profiles.profile_id&lt;br /&gt;
|-&lt;br /&gt;
||profile name&lt;br /&gt;
||string&lt;br /&gt;
||feed_profiles.profile_name&lt;br /&gt;
|-&lt;br /&gt;
||weekly repetition indicator&lt;br /&gt;
||boolean&lt;br /&gt;
||feed_schedule.is_weekly&lt;br /&gt;
|-&lt;br /&gt;
||daily repetition indicator&lt;br /&gt;
||boolean&lt;br /&gt;
||feed_schedule.is_daily&lt;br /&gt;
|-&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
The API shall provide an endpoint which allows the client to update an existing feed schedule entry identified by the timestamp.&lt;br /&gt;
&lt;br /&gt;
Note: ''Depending on the database layout, an &amp;lt;code&amp;gt;UPDATE&amp;lt;/code&amp;gt; operation may not be applicable. In this case, a combined transaction of &amp;lt;code&amp;gt;DELETE&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;INSERT&amp;lt;/code&amp;gt; using rollback in case of failure shall be applied.''&lt;br /&gt;
&lt;br /&gt;
The API shall provide an endpoint which allows the client to insert a feed schedule entry.&lt;br /&gt;
* If the profile id of the feed schedule entry requested from the client does not exist, the API shall output an error message.&lt;br /&gt;
* If the feed schedule already contains an entry with a timestamp identical to the one requested from the client, the API shall output an error message.&lt;br /&gt;
&lt;br /&gt;
The related SQL query is:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;INSERT INTO feed_schedule(timestamp, profile_id, is_weekly, is_daily) VALUES(&amp;quot;[scheduleTimestamp]&amp;quot;, &amp;quot;[profileId]&amp;quot;, [scheduleRepeatWeekly], [scheduleRepeatDaily]);&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The API shall provide an endpoint which allows the client to delete an existing feed schedule entry identified by its timestamp.&lt;br /&gt;
&lt;br /&gt;
The related SQL query is:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;DELETE FROM feed_schedule WHERE timestamp=&amp;quot;[scheduleTimestamp]&amp;quot;;&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Requirements for heating feature ===&lt;br /&gt;
==== Heating set values ====&lt;br /&gt;
The API shall provide an endpoint communicating from the server to the client the heating set values read from the table &amp;lt;code&amp;gt;heating_set_vals&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
The corresponding SQL query is:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;SELECT heating_switch_off_temp, heating_switch_on_temp FROM heating_set_vals;&amp;quot;&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
!|Signal name&lt;br /&gt;
!|Signal format&lt;br /&gt;
!|Database column name&lt;br /&gt;
|-&lt;br /&gt;
||heating switch off temperature&lt;br /&gt;
||floating point number&lt;br /&gt;
||heating_set_vals.heating_switch_off_temp&lt;br /&gt;
|-&lt;br /&gt;
||heating switch on temperature&lt;br /&gt;
||floating point number&lt;br /&gt;
||heating_set_vals.heating_switch_on_temp&lt;br /&gt;
|-&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
The API shall provide an endpoint which allows the client to update both heating set values.&lt;br /&gt;
The corresponding SQL query is:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;UPDATE heating_set_vals SET heating_switch_on_temp=[heatingSwitchOnTemp], heating_switch_off_temp=[heatingSwitchOffTemp];&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Note: ''The database shall contain only one entry in the table. In case there are multiple entries, then the query will overwrite the data of all entries. This is intentional.''&lt;br /&gt;
&lt;br /&gt;
==== Heating statistical data ====&lt;br /&gt;
The API shall provide an endpoint communicating from the server to the client the heating statistical data read from the table &amp;lt;code&amp;gt;heating_stats&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
The corresponding SQL query is:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;SELECT date, energy, ambient_temp_average, water_temp_average, heating_control_runtime FROM heating_stats;&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
!|Signal name&lt;br /&gt;
!|Signal format&lt;br /&gt;
!|Database column name&lt;br /&gt;
|-&lt;br /&gt;
||date&lt;br /&gt;
||string&lt;br /&gt;
||heating_stats.date&lt;br /&gt;
|-&lt;br /&gt;
||daily energy consumption&lt;br /&gt;
||floating point number&lt;br /&gt;
||heating_stats.energy&lt;br /&gt;
|-&lt;br /&gt;
||daily average of ambient temperature&lt;br /&gt;
||floating point number&lt;br /&gt;
||heating_stats.ambient_temp_average&lt;br /&gt;
|-&lt;br /&gt;
||daily average of water temperature&lt;br /&gt;
||floating point number&lt;br /&gt;
||heating_stats.water_temp_average&lt;br /&gt;
|-&lt;br /&gt;
||heating control runtime&lt;br /&gt;
||integer number&lt;br /&gt;
||heating_stats.heating_control_runtime&lt;br /&gt;
|-&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Requirements for refill feature ===&lt;br /&gt;
==== Refill log ====&lt;br /&gt;
The API shall provide an endpoint communicating from the server to the client the refill events read from the table &amp;lt;code&amp;gt;refill_log&amp;lt;/code&amp;gt; of either the last 24 hours or the last 7 days depending on parameter provided by the client.&lt;br /&gt;
&lt;br /&gt;
The corresponding SQL query (for a period of one day) is:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;SELECT timestamp, duration, volume, error_code FROM refill_log WHERE timestamp &amp;gt; (NOW() - INTERVAL 1 DAY)&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
!|Signal name&lt;br /&gt;
!|Signal format&lt;br /&gt;
!|Database column name&lt;br /&gt;
|-&lt;br /&gt;
||timestamp&lt;br /&gt;
||string&lt;br /&gt;
||refill_log.timestamp&lt;br /&gt;
|-&lt;br /&gt;
||duration&lt;br /&gt;
||floating point number&lt;br /&gt;
||refill_log.duration&lt;br /&gt;
|-&lt;br /&gt;
||volume&lt;br /&gt;
||floating point number&lt;br /&gt;
||refill_log.volume&lt;br /&gt;
|-&lt;br /&gt;
||error code&lt;br /&gt;
||integer&lt;br /&gt;
||refill_log.error_code&lt;br /&gt;
|-&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
==== Refill controller state ====&lt;br /&gt;
The API shall provide an endpoint communicating from the server to the client the state of the refill control read from a file:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
!|Signal name&lt;br /&gt;
!|Signal format&lt;br /&gt;
!|Signal source&lt;br /&gt;
|-&lt;br /&gt;
||refill control state&lt;br /&gt;
||string&lt;br /&gt;
||/var/local/aquarium-ctrl/refillctrl&lt;br /&gt;
|-&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
The API shall provide an endpoint which allows the client to change the refill control state by executing:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;/usr/local/bin/aquarium_client refill [command]&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
!|operation&lt;br /&gt;
!|command&lt;br /&gt;
|-&lt;br /&gt;
||reset error state&lt;br /&gt;
||reset&lt;br /&gt;
|-&lt;br /&gt;
||(re-)start&lt;br /&gt;
||start&lt;br /&gt;
|-&lt;br /&gt;
||stop&lt;br /&gt;
||stop&lt;br /&gt;
|-&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Requirements for actuator schedule feature ===&lt;br /&gt;
The API shall provide an endpoint communicating from the server to the client the actuator schedule read from the table &amp;lt;code&amp;gt;schedule&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
The corresponding SQL query is:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;SELECT schedule_type, start_time, stop_time, is_active FROM schedule;&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
!|Signal name&lt;br /&gt;
!|Signal format&lt;br /&gt;
!|Database column name&lt;br /&gt;
|-&lt;br /&gt;
||schedule type&lt;br /&gt;
||string&lt;br /&gt;
||schedule.schedule_type&lt;br /&gt;
|-&lt;br /&gt;
||start time&lt;br /&gt;
||string&lt;br /&gt;
||schedule.start_time&lt;br /&gt;
|-&lt;br /&gt;
||stop time&lt;br /&gt;
||string&lt;br /&gt;
||schedule.stop_time&lt;br /&gt;
|-&lt;br /&gt;
||active indicator&lt;br /&gt;
||boolean&lt;br /&gt;
||schedule.is_active&lt;br /&gt;
|-&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
The API shall provide an endpoint that allows the client to update all actuator schedule entries.&lt;br /&gt;
The client provides the following time values as string using the format &amp;quot;hh:mm&amp;quot;:&lt;br /&gt;
* ballingRangeStartTime&lt;br /&gt;
* ballingRangeFinishTime&lt;br /&gt;
* refillRangeStartTime&lt;br /&gt;
* refillRangeFinishTime&lt;br /&gt;
* ventilationRangeStartTime&lt;br /&gt;
* ventilationRangeFinishTime&lt;br /&gt;
* heatingRangeStartTime&lt;br /&gt;
* heatingRangeFinishTime&lt;br /&gt;
&lt;br /&gt;
The client provides the following values as integer:&lt;br /&gt;
* ballingRangeIsActive&lt;br /&gt;
* refillRangeIsActive&lt;br /&gt;
* ventilationRangeIsActive&lt;br /&gt;
* heatingRangeIsActive&lt;br /&gt;
&lt;br /&gt;
The API shall check if all values were provided by the client.&lt;br /&gt;
If all values are provided, by the client, the API shall execute a set of database commands in one transaction:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
UPDATE schedule SET start_time=&amp;quot;[ballingRangeStartTime]&amp;quot;, stop_time=&amp;quot;[ballingRangeFinishTime]&amp;quot;, is_active=[ballingRangeIsActive] WHERE schedule_type=&amp;quot;balling&amp;quot;;&lt;br /&gt;
&lt;br /&gt;
UPDATE schedule SET start_time=&amp;quot;[refillRangeStartTime]&amp;quot;, stop_time=&amp;quot;[refillRangeFinishTime]&amp;quot;, is_active=[refillRangeIsActive] WHERE schedule_type=&amp;quot;refill&amp;quot;;&lt;br /&gt;
&lt;br /&gt;
UPDATE schedule SET start_time=&amp;quot;[ventilationRangeStartTime]&amp;quot;, stop_time=&amp;quot;[ventilationRangeFinishTime]&amp;quot;, is_active=[ventilationRangeIsActive] WHERE schedule_type=&amp;quot;ventilation&amp;quot;;&lt;br /&gt;
&lt;br /&gt;
UPDATE schedule SET start_time=&amp;quot;[heatingRangeStartTime]&amp;quot;, stop_time=&amp;quot;[heatingRangeFinishTime]&amp;quot;, is_active=[heatingRangeIsActive] WHERE schedule_type=&amp;quot;heating&amp;quot;;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Requirements for time data feature ===&lt;br /&gt;
The API shall provide an endpoint communicating from the server to the client the time data of the last 24 hours read from the table &amp;lt;code&amp;gt;data&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
The related SQL query is:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;SELECT timestamp, water_temperature, water_temperature_filtered, ph_value, pH_value_filtered, conductivity, conductivity_filtered, refill_in_progress, tank_level_switch_position, tank_level_switch_invalid, tank_level_switch_position_stabilized, surface_ventilation_status, ambient_temperature, ambient_humidity, heater_status FROM data WHERE (timestamp &amp;gt; (CURRENT_TIMESTAMP() - INTERVAL 1 DAY));&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
!|Signal name&lt;br /&gt;
!|Signal format&lt;br /&gt;
!|Database column name&lt;br /&gt;
!|API response&lt;br /&gt;
|-&lt;br /&gt;
||timestamp&lt;br /&gt;
||string&lt;br /&gt;
||data.timestamp&lt;br /&gt;
||timestamp&lt;br /&gt;
|-&lt;br /&gt;
||water temperature&lt;br /&gt;
||floating point number&lt;br /&gt;
||data.water_temperature&lt;br /&gt;
||waterTemperature&lt;br /&gt;
|-&lt;br /&gt;
||filtered water temperature&lt;br /&gt;
||floating point number&lt;br /&gt;
||data.water_temperature_filtered&lt;br /&gt;
||waterTemperatureFiltered&lt;br /&gt;
|-&lt;br /&gt;
||pH value&lt;br /&gt;
||floating point number&lt;br /&gt;
||data.ph_value&lt;br /&gt;
||phValue&lt;br /&gt;
|-&lt;br /&gt;
||filtered pH value&lt;br /&gt;
||floating point number&lt;br /&gt;
||data.ph_value_filtered&lt;br /&gt;
||filteredPhValue&lt;br /&gt;
|-&lt;br /&gt;
||conductivity&lt;br /&gt;
||floating point number&lt;br /&gt;
||data.conductivity&lt;br /&gt;
||conductivity&lt;br /&gt;
|-&lt;br /&gt;
||filtered conductivity&lt;br /&gt;
||floating point number&lt;br /&gt;
||data.conductivity_filtered&lt;br /&gt;
||filteredConductivity&lt;br /&gt;
|-&lt;br /&gt;
||refill in progress&lt;br /&gt;
||boolean&lt;br /&gt;
||data.refill_in_progress&lt;br /&gt;
||refillInProgress&lt;br /&gt;
|-&lt;br /&gt;
||tank level switch position&lt;br /&gt;
||boolean&lt;br /&gt;
||data.tank_level_switch_position&lt;br /&gt;
||tankLevelSwitchPosition&lt;br /&gt;
|-&lt;br /&gt;
||tank level switch validity indicator&lt;br /&gt;
||boolean&lt;br /&gt;
||data.tank_level_switch_invalid&lt;br /&gt;
||tankLevelSwitchInvalid&lt;br /&gt;
|-&lt;br /&gt;
||tank level switch position stabilized&lt;br /&gt;
||boolean&lt;br /&gt;
||data.tank_level_switch_position_stabilized&lt;br /&gt;
||tankLevelSwitchPositionStabilized&lt;br /&gt;
|-&lt;br /&gt;
||surface ventilation status&lt;br /&gt;
||boolean&lt;br /&gt;
||data.surface_ventilation_status&lt;br /&gt;
||surfaceVentilationStatus&lt;br /&gt;
|-&lt;br /&gt;
||ambient temperature&lt;br /&gt;
||floating point number&lt;br /&gt;
||data.ambient_temperature&lt;br /&gt;
||ambientTemperature&lt;br /&gt;
|-&lt;br /&gt;
||ambient humidity&lt;br /&gt;
||floating point number&lt;br /&gt;
||data.ambient_humidity&lt;br /&gt;
||ambientHumidity&lt;br /&gt;
|-&lt;br /&gt;
||heater status&lt;br /&gt;
||boolean&lt;br /&gt;
||data.heater_status&lt;br /&gt;
||heaterStatus&lt;br /&gt;
|-&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Requirements for ventilation feature ===&lt;br /&gt;
==== Ventilation set values ====&lt;br /&gt;
The API shall provide an endpoint communicating from the server to the client the ventilation set values read from the table &amp;lt;code&amp;gt;ventilation_set_vals&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
The corresponding SQL query is:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;SELECT ventilation_switch_off_temp, ventilation_switch_on_temp FROM ventilation_set_vals;&amp;quot;&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
!|Signal name&lt;br /&gt;
!|Signal format&lt;br /&gt;
!|Database column name&lt;br /&gt;
|-&lt;br /&gt;
||ventilation switch off temperature&lt;br /&gt;
||floating point number&lt;br /&gt;
||ventilation_set_vals.ventilation_switch_off_temp&lt;br /&gt;
|-&lt;br /&gt;
||ventilation switch on temperature&lt;br /&gt;
||floating point number&lt;br /&gt;
||ventilation_set_vals.ventilation_switch_on_temp&lt;br /&gt;
|-&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
The API shall provide an endpoint which allows the client to update both ventilation set values.&lt;br /&gt;
The corresponding SQL query is:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;UPDATE ventilation_set_vals SET ventilation_switch_on_temp=[ventilationSwitchOnTemp], ventilation_switch_off_temp=[ventilationSwitchOffTemp];&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Note: ''The database shall contain only one entry in the table. In case there are multiple entries, then the query will overwrite the data of all entries. This is intentional.''&lt;br /&gt;
&lt;br /&gt;
== Architecture ==&lt;br /&gt;
The API is distributed over the following files:&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
!|File name&lt;br /&gt;
!|Content description&lt;br /&gt;
|-&lt;br /&gt;
||api.php&lt;br /&gt;
||Main API functionality&lt;br /&gt;
|-&lt;br /&gt;
||db.php&lt;br /&gt;
||Adapter for connecting to SQL database&lt;br /&gt;
|-&lt;br /&gt;
||functions.php&lt;br /&gt;
||helper functionality repeatedly used throughout the API&lt;br /&gt;
|-&lt;br /&gt;
||test_reset_db.php&lt;br /&gt;
||functionality to reset and initialise the database with mock data: Do not deploy this file in productive environment!&lt;br /&gt;
|-&lt;br /&gt;
||test.php&lt;br /&gt;
||functionality to test access to database for development purposed. Do not deploy this file in productive environment!&lt;br /&gt;
|-&lt;br /&gt;
||test_generate_mock_data.php&lt;br /&gt;
||functionality to create mock data for front end testing. Do not deploy this file in productive environment!&lt;br /&gt;
|-&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Implementation ==&lt;br /&gt;
The behaviour of the &amp;lt;code&amp;gt;api.php&amp;lt;/code&amp;gt; is controlled by the first &amp;lt;code&amp;gt;action&amp;lt;/code&amp;gt; argument provided via &amp;lt;code&amp;gt;GET&amp;lt;/code&amp;gt; method which can assume the following values:&lt;br /&gt;
=== check_auth ===&lt;br /&gt;
The API responds with a JSON object containing the following data:&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
!|Label&lt;br /&gt;
!|Value&lt;br /&gt;
|-&lt;br /&gt;
||status&lt;br /&gt;
||message&lt;br /&gt;
|-&lt;br /&gt;
||authorized&lt;br /&gt;
||Credentials valid&lt;br /&gt;
|-&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== test_hash ===&lt;br /&gt;
The API responds with a JSON object containing the following data:&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
!|Label&lt;br /&gt;
!|Value&lt;br /&gt;
|-&lt;br /&gt;
||input&lt;br /&gt;
||hash&lt;br /&gt;
|-&lt;br /&gt;
||[Input value provided]&lt;br /&gt;
||[Hash calculated from the input value]&lt;br /&gt;
|-&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== load_overview_signals ===&lt;br /&gt;
When using this parameter value, the API provides the overview data in JSON format as per [[REST_API#Requirements_for_overview_feature|above requirement]].&lt;br /&gt;
&lt;br /&gt;
=== load_balling_log ===&lt;br /&gt;
When using this parameter value, the API provides the balling dosing log as per [[REST_API#Balling_dosing_log|above requirements]].&lt;br /&gt;
&lt;br /&gt;
The second parameter &amp;lt;code&amp;gt;period&amp;lt;/code&amp;gt; determines the period: &amp;lt;code&amp;gt;7d&amp;lt;/code&amp;gt; for the last 7 days, otherwise only the last 24 hours.&lt;br /&gt;
&lt;br /&gt;
=== load_balling_set_vals ===&lt;br /&gt;
When using this parameter value, the API provides the Balling mineral dosing set values of all configured pumps in JSON format as per [[REST_API#Balling_set_values|above requirement]].&lt;br /&gt;
&lt;br /&gt;
=== update_balling_set_vals ===&lt;br /&gt;
When using this parameter value, the API checks if the required &amp;lt;code&amp;gt;POST&amp;lt;/code&amp;gt; parameters were provided:&lt;br /&gt;
* dosingVolume&lt;br /&gt;
* pumpId&lt;br /&gt;
&lt;br /&gt;
If the parameters are provided, the API will issue the SQL statement to update the dosing volume for the dedicated pump as per [[REST_API#Balling_set_values|above requirements]].&lt;br /&gt;
&lt;br /&gt;
=== load_feed_log ===&lt;br /&gt;
When using this parameter value, the API provides the balling dosing log as per [[REST_API#Feed_log|above requirements]].&lt;br /&gt;
&lt;br /&gt;
The second parameter &amp;lt;code&amp;gt;period&amp;lt;/code&amp;gt; determines the period: &amp;lt;code&amp;gt;7d&amp;lt;/code&amp;gt; for the last 7 days, otherwise only the last 24 hours.&lt;br /&gt;
&lt;br /&gt;
=== load_feed_profiles ===&lt;br /&gt;
When using this parameter value, the API provides all existing feed profile in JSON format as per [[REST_API#Feed_profiles|above requirement]].&lt;br /&gt;
&lt;br /&gt;
=== update_feed_profile ===&lt;br /&gt;
When using this parameter value, the API will check if the &amp;lt;code&amp;gt;profileId&amp;lt;/code&amp;gt; is provided as POST parameter.&lt;br /&gt;
&lt;br /&gt;
All other parameters provided will be type checked if they are integer or string types.&lt;br /&gt;
&lt;br /&gt;
The API will issue the SQL statement to update the specific feed profile identified by &amp;lt;code&amp;gt;profileId&amp;lt;/code&amp;gt; as per [[REST_API#Feed_profiles|above requirement]].&lt;br /&gt;
&lt;br /&gt;
=== create_feed_profile ===&lt;br /&gt;
When using this parameter value, the query will do the following steps [[REST_API#create_feed_profile|as per above requirement]]:&lt;br /&gt;
* check if the parameter &amp;lt;code&amp;gt;profileName&amp;lt;/code&amp;gt; is provided via &amp;lt;code&amp;gt;POST&amp;lt;/code&amp;gt; method.&lt;br /&gt;
* issue an SQL statement to check if a profile with that name already exists in the database, issue an error response and abort further execution if indeed there already is such a profile&lt;br /&gt;
* issue and SQL statement to insert an empty feed profile into the data base using &amp;lt;code&amp;gt;profileName&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== execute_feed_profile ===&lt;br /&gt;
When using this parameter value, the API will check for the additional parameter &amp;lt;code&amp;gt;profileId&amp;lt;/code&amp;gt; provided via &amp;lt;code&amp;gt;GET&amp;lt;/code&amp;gt; method.&lt;br /&gt;
If the parameter is provided, the API will issue an SQL statement to check if the database contains a matching feed profile, issue an error message and abort execution if there is no such profile.&lt;br /&gt;
If the profile exists, the API will execute the external program &amp;lt;code&amp;gt;aquarium_client&amp;lt;/code&amp;gt; using the keywords &amp;lt;code&amp;gt;feed&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;execute&amp;lt;/code&amp;gt; and the feed profile ID as parameter as per [[REST_API#Feed_profiles|above requirement]].&lt;br /&gt;
&lt;br /&gt;
=== delete_feed_profile ===&lt;br /&gt;
When using this parameter value, the API checks if the parameter &amp;lt;code&amp;gt;profileId&amp;lt;/code&amp;gt; is provided via &amp;lt;code&amp;gt;GET&amp;lt;/code&amp;gt; method. If the parameter is provided, the API will issue an SQL statement to delete the matching feed profile from the database as per [[REST_API#Feed_profiles|above requirements]].&lt;br /&gt;
&lt;br /&gt;
=== load_feed_schedule ===&lt;br /&gt;
When using this parameter value, the API will load the feed schedule entries as per [[REST_API#Feed_schedules|above requirement]].&lt;br /&gt;
&lt;br /&gt;
The SQL query uses a &amp;lt;code&amp;gt;LEFT JOIN&amp;lt;/code&amp;gt; where the profile name is read from the &amp;lt;code&amp;gt;feed_profile&amp;lt;/code&amp;gt; table identified by the &amp;lt;code&amp;gt;profile_id&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== create_feed_schedule_entry ===&lt;br /&gt;
When using this parameter value, the API will check if the following parameters were provided via &amp;lt;code&amp;gt;POST&amp;lt;/code&amp;gt; method:&lt;br /&gt;
* &amp;lt;code&amp;gt;profileId&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;timestamp&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;scheduleIsWeekly&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;scheduleIsDaily&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Note: Functionality for &amp;lt;code&amp;gt;scheduleIsWeekly&amp;lt;/code&amp;gt; is not implemented as of December 2025.&lt;br /&gt;
&lt;br /&gt;
When the additional parameters are provided, the API will issue an SQL statement to check if a matching feed schedule entry already exists and abort further execution if that is the case.&lt;br /&gt;
&lt;br /&gt;
Otherwise, the API will issue a further SQL statement to insert the feed schedule entry into the database as per [[REST_API#Feed_schedules|above requirement]].&lt;br /&gt;
&lt;br /&gt;
=== update_feed_schedule_entry ===&lt;br /&gt;
When using this parameter value, the API will check if the following parameters were provided via &amp;lt;code&amp;gt;POST&amp;lt;/code&amp;gt; method and abort execution if this is not the case:&lt;br /&gt;
* &amp;lt;code&amp;gt;timestamp&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;profileId&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;isWeekly&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;isDaily&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
When the parameters are provided, the API will execute one SQL transaction containing two statements:&lt;br /&gt;
* The first statement will delete the feed schedule entry identified by the timestamp.&lt;br /&gt;
* The second statement will insert a new feed schedule entry using the additionally provided parameter.&lt;br /&gt;
&lt;br /&gt;
This will effectively update an existing feed schedule entry as per [[REST_API#Feed_schedules|above requirement]].&lt;br /&gt;
&lt;br /&gt;
If the transaction throws an exception (when one of the SQL statements fails), the API will trigger the rollback of the transaction.&lt;br /&gt;
&lt;br /&gt;
=== delete_feed_schedule_entry ===&lt;br /&gt;
When using this parameter value, the API will check if the &amp;lt;code&amp;gt;timestamp&amp;lt;/code&amp;gt; was provided using &amp;lt;code&amp;gt;POST&amp;lt;/code&amp;gt; method and abort execution if this is not the case.&lt;br /&gt;
&lt;br /&gt;
When the additional timestamp parameter is provided, the API will issue an SQL statement to delete the corresponding feed schedule entry as per [[REST_API#Feed_schedules|above requirement]].&lt;br /&gt;
&lt;br /&gt;
=== load_heating_set_vals ===&lt;br /&gt;
When using this parameter value, the API will load the heating control set values as per [[REST_API#Heating_set_values|above requirement]].&lt;br /&gt;
&lt;br /&gt;
=== update_heating_set_vals ===&lt;br /&gt;
When using this parameter value, the API will check if the following parameters were provided via &amp;lt;code&amp;gt;POST&amp;lt;/code&amp;gt; method and will abort execution if this is not the case:&lt;br /&gt;
* heatingSwitchOnTemp&lt;br /&gt;
* heatingSwitchOffTemp&lt;br /&gt;
&lt;br /&gt;
When the parameters are provided, the API will issue an SQL statement update the heating control set values as per [[REST_API#Heating_set_values|above requirement]].&lt;br /&gt;
&lt;br /&gt;
=== load_heating_stats ===&lt;br /&gt;
When using this parameter value, the API will load the heating statistical data as per [[REST_API#Heating_statistical_data|above requirement]].&lt;br /&gt;
&lt;br /&gt;
=== load_refill_log ===&lt;br /&gt;
When using this parameter value, the API provides the refill log as per [[REST_API#Refill_log|above requirements]].&lt;br /&gt;
&lt;br /&gt;
The second parameter &amp;lt;code&amp;gt;period&amp;lt;/code&amp;gt; determines the period: &amp;lt;code&amp;gt;7d&amp;lt;/code&amp;gt; for the last 7 days, otherwise only the last 24 hours.&lt;br /&gt;
&lt;br /&gt;
=== load_refill_state ===&lt;br /&gt;
When this parameter value is provided, the API will load the refill control state information from &amp;lt;code&amp;gt;/var/local/aquarium-ctrl/refillctrl&amp;lt;/code&amp;gt; as per [[REST_API#Refill_controller_state|above requirement]].&lt;br /&gt;
&lt;br /&gt;
=== set_refill_state ===&lt;br /&gt;
When using this parameter value, the API will check if an additional parameter named &amp;lt;code&amp;gt;command&amp;lt;/code&amp;gt; was provided via &amp;lt;code&amp;gt;GET&amp;lt;/code&amp;gt; method.&lt;br /&gt;
&lt;br /&gt;
The API will check if the parameter has a valid value. Valid values are:&lt;br /&gt;
* reset&lt;br /&gt;
* start&lt;br /&gt;
* stop&lt;br /&gt;
&lt;br /&gt;
When the additional parameter is provided with a valid value, the API will execute the command &amp;lt;code&amp;gt;/usr/local/bin/aquarium_client refill [command]&amp;lt;/code&amp;gt;. This will update the refill control state as per [[REST_API#Refill_controller_state]] above requirement.&lt;br /&gt;
&lt;br /&gt;
When no valid value is provided, the API will abort execution.&lt;br /&gt;
&lt;br /&gt;
=== load_actuator_schedule ===&lt;br /&gt;
When using this parameter value, the API will load the actuator schedule as per [[REST_API#Requirements_for_actuator_schedule_feature|above requirement]].&lt;br /&gt;
&lt;br /&gt;
For testing purposes, the schedule entries are provided in alphabetical order.&lt;br /&gt;
&lt;br /&gt;
=== update_actuator_schedule ===&lt;br /&gt;
When using this parameter value, the API will check if the following parameters were provided via &amp;lt;code&amp;gt;POST&amp;lt;/code&amp;gt; method and abort execution if this is not the case:&lt;br /&gt;
* ballingRangeStartTime&lt;br /&gt;
* ballingRangeFinishTime&lt;br /&gt;
* ballingRangeIsActive&lt;br /&gt;
* refillRangeStartTime&lt;br /&gt;
* refillRangeFinishTime&lt;br /&gt;
* refillRangeIsActive,&lt;br /&gt;
* ventilationRangeStartTime&lt;br /&gt;
* ventilationRangeFinishTime&lt;br /&gt;
* ventilationRangeIsActive&lt;br /&gt;
* heatingRangeStartTime&lt;br /&gt;
* heatingRangeFinishTime&lt;br /&gt;
* heatingRangeIsActive&lt;br /&gt;
&lt;br /&gt;
When all parameters are provided, the API will initiate a transaction consisting of four SQL statements for each actuator schedule.&lt;br /&gt;
&lt;br /&gt;
The SQL statement will update the actuator schedule as per above [[REST_API#Requirements_for_actuator_schedule_feature|requirement]].&lt;br /&gt;
&lt;br /&gt;
=== load_time_data ===&lt;br /&gt;
When using this parameter value, the API will load the time data as per [[REST_API#Requirements_for_time_data_feature|above requirement]].&lt;br /&gt;
&lt;br /&gt;
=== load_ventilation_set_vals ===&lt;br /&gt;
When using this parameter value, the API will load the ventilation control set values as per [[REST_API#Ventilation_set_values|above requirement]].&lt;br /&gt;
&lt;br /&gt;
=== update_ventilation_set_vals ===&lt;br /&gt;
When using this parameter value, the API will check if the following parameters were provided via &amp;lt;code&amp;gt;POST&amp;lt;/code&amp;gt; method and will abort execution if this is not the case:&lt;br /&gt;
* ventilationSwitchOnTemp&lt;br /&gt;
* ventilationSwitchOffTemp&lt;br /&gt;
&lt;br /&gt;
When the parameters are provided, the API will issue an SQL statement update the ventilation control set values as per [[REST_API#Ventilation_set_values|above requirement]].&lt;br /&gt;
&lt;br /&gt;
== Testing ==&lt;br /&gt;
Postman is used for testing of the API.&lt;br /&gt;
&lt;br /&gt;
The requests aggregated in collections.&lt;br /&gt;
&lt;br /&gt;
Each collection (except for the General collection) starts with a request to reset the database (truncating all tables and adding mock data).&lt;br /&gt;
&lt;br /&gt;
The server-based script for resetting the database cannot be run in parallel - limiting the ability to run all tests in parallel.&lt;br /&gt;
&lt;br /&gt;
Test execution is manual per collection.&lt;br /&gt;
&lt;br /&gt;
Reference values are stored in the Variables section of each collection.&lt;br /&gt;
&lt;br /&gt;
=== Feed collection ===&lt;br /&gt;
The collection executes the following requests:&lt;br /&gt;
* Reset database (GET)&lt;br /&gt;
* Load feed log (GET)&lt;br /&gt;
* Initial Load feed profiles (GET)&lt;br /&gt;
* Update feed profile (POST)&lt;br /&gt;
* Secondary Load feed profiles (GET): This request will verify if the previous request could modify the database successfully.&lt;br /&gt;
* Initial Create feed profile (POST)&lt;br /&gt;
* Secondary Create feed profile (POST): This request will verify if trying to create a new feed profile with an already existing name is rejected.&lt;br /&gt;
* Execute feed profile (GET)&lt;br /&gt;
* Create feed schedule entry (POST)&lt;br /&gt;
* Load feed schedule (GET)&lt;br /&gt;
* Update feed schedule entry (POST)&lt;br /&gt;
* Delete feed schedule entry (POST)&lt;br /&gt;
* Delete feed profile (GET)&lt;br /&gt;
&lt;br /&gt;
=== General collection ===&lt;br /&gt;
The collection executes the following requests:&lt;br /&gt;
* Hash test (GET)&lt;br /&gt;
* Check authorisation (GET)&lt;br /&gt;
&lt;br /&gt;
=== Balling collection ===&lt;br /&gt;
The collection executes the following requests:&lt;br /&gt;
* Reset database (GET)&lt;br /&gt;
* Load Balling log (GET)&lt;br /&gt;
* Initial Load Balling set values (GET)&lt;br /&gt;
* Update Balling set values (POST)&lt;br /&gt;
* Secondary Load Balling set values (GET): This request will verify if the previous request could successfully modify the database.&lt;br /&gt;
&lt;br /&gt;
=== Overview collection ===&lt;br /&gt;
The collection executes the following requests:&lt;br /&gt;
* Reset database (GET)&lt;br /&gt;
* Load overview signals (GET)&lt;br /&gt;
&lt;br /&gt;
=== Refill collection ===&lt;br /&gt;
The collection executes the following requests:&lt;br /&gt;
* Reset database (GET)&lt;br /&gt;
* Load refill log (GET)&lt;br /&gt;
* Load refill control state (GET)&lt;br /&gt;
* Set refill control state (GET)&lt;br /&gt;
&lt;br /&gt;
=== Actuator schedule collection ===&lt;br /&gt;
The collection executes the following requests:&lt;br /&gt;
* Reset database (GET)&lt;br /&gt;
* Initial Load actuator schedule (GET)&lt;br /&gt;
* Update actuator schedule (POST)&lt;br /&gt;
* Secondary Load actuator schedule (GET): This request will verify if the previous request could successfully modify the database.&lt;br /&gt;
&lt;br /&gt;
=== Time data collection ===&lt;br /&gt;
The collection executes the following requests:&lt;br /&gt;
* Reset database (GET)&lt;br /&gt;
* Load time data (GET)&lt;br /&gt;
&lt;br /&gt;
=== Ventilation collection ===&lt;br /&gt;
The collection executes the following requests:&lt;br /&gt;
* Reset database (GET)&lt;br /&gt;
* Initial Load ventilation set values (GET)&lt;br /&gt;
* Update ventilation set values (POST)&lt;br /&gt;
* Secondary Load ventilation set values (GET): This request will check if the previous request could successfully modify the database.&lt;br /&gt;
&lt;br /&gt;
=== Heating collection ===&lt;br /&gt;
The collection executes the following requests:&lt;br /&gt;
* Reset database&lt;br /&gt;
* Initial Load heating set values (GET)&lt;br /&gt;
* Update heating set values (POST)&lt;br /&gt;
* Secondary Load heating set values (GET): This request will verify if the previous request could successfully modify the database.&lt;br /&gt;
* Load heating statistics (GET)&lt;/div&gt;</summary>
		<author><name>Uwe</name></author>
	</entry>
	<entry>
		<id>http://217.79.180.177/mediawiki/index.php?title=REST_API&amp;diff=457</id>
		<title>REST API</title>
		<link rel="alternate" type="text/html" href="http://217.79.180.177/mediawiki/index.php?title=REST_API&amp;diff=457"/>
		<updated>2026-04-28T18:41:41Z</updated>

		<summary type="html">&lt;p&gt;Uwe: /* Balling dosing log */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Requirements ==&lt;br /&gt;
&lt;br /&gt;
=== General requirements ===&lt;br /&gt;
The API shall communicate with mobile apps and dynamic webpage.&lt;br /&gt;
&lt;br /&gt;
For each request, the API shall validate the credentials (user, password).&lt;br /&gt;
&lt;br /&gt;
The credentials of authorised individuals are stored in the table 'users'.&lt;br /&gt;
* The name is stored as clear text.&lt;br /&gt;
* A hash value of the password is stored in the table. &lt;br /&gt;
&lt;br /&gt;
The API shall compute the hash function of the password when validating the credentials.&lt;br /&gt;
&lt;br /&gt;
For testing purposes, the API shall provide an endpoint which calculates the hash function of a string given as parameter.&lt;br /&gt;
&lt;br /&gt;
The API shall provide the data for all subsequent queries in JSON format.&lt;br /&gt;
&lt;br /&gt;
Timestamps shall have the format: YYYY-MM-DD hh:mm:ss&lt;br /&gt;
&lt;br /&gt;
Placeholders in subsequent SQL queries are described in brackets: &amp;lt;code&amp;gt;[placeholder]&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Coding conventions ====&lt;br /&gt;
The URL action keys shall use snake_case.&lt;br /&gt;
&lt;br /&gt;
The URL parameters sent by the client shall use camelCase.&lt;br /&gt;
&lt;br /&gt;
The JSON keys (responses from the API) shall use camelCase.&lt;br /&gt;
&lt;br /&gt;
The database columns shall use snake_case.&lt;br /&gt;
&lt;br /&gt;
The php variables shall use camelCase.&lt;br /&gt;
&lt;br /&gt;
Filenames shall use all minor letters or kebab-case.&lt;br /&gt;
* Exception: Executables may use snake_case.&lt;br /&gt;
&lt;br /&gt;
=== Requirements for overview feature ===&lt;br /&gt;
&lt;br /&gt;
The API shall provide an endpoint for informing the client about the validity of the credentials.&lt;br /&gt;
&lt;br /&gt;
The API shall provide an endpoint communicating from the server to the client a set of floating point data read from a set of files:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
!|Signal name&lt;br /&gt;
!|Signal format&lt;br /&gt;
!|Signal source&lt;br /&gt;
!|API response&lt;br /&gt;
|-&lt;br /&gt;
||timestamp&lt;br /&gt;
||string&lt;br /&gt;
||/var/local/aquarium-ctrl/aquarium-ctrl-ts&lt;br /&gt;
||timestamp&lt;br /&gt;
|-&lt;br /&gt;
||water temperature&lt;br /&gt;
||floating point number&lt;br /&gt;
||/var/local/aquarium-ctrl/atlsscntfc-temp&lt;br /&gt;
||waterTemperature&lt;br /&gt;
|-&lt;br /&gt;
||filtered water temperature&lt;br /&gt;
||floating point number&lt;br /&gt;
||/var/local/aquarium-ctrl/atlsscntfc-tempfltrd&lt;br /&gt;
||filteredWaterTemperature&lt;br /&gt;
|-&lt;br /&gt;
||pH value&lt;br /&gt;
||floating point number&lt;br /&gt;
||/var/local/aquarium-ctrl/atlsscntfc-ph&lt;br /&gt;
||phValue&lt;br /&gt;
|-&lt;br /&gt;
||filtered pH value&lt;br /&gt;
||floating point number&lt;br /&gt;
||/var/local/aquarium-ctrl/atlsscntfc-phfltrd&lt;br /&gt;
||filteredPhValue&lt;br /&gt;
|-&lt;br /&gt;
||conductivity&lt;br /&gt;
||floating point number&lt;br /&gt;
||/var/local/aquarium-ctrl/atlsscntfc-conduc&lt;br /&gt;
||conductivity&lt;br /&gt;
|-&lt;br /&gt;
||filtered conductivity&lt;br /&gt;
||floating point number&lt;br /&gt;
||/var/local/aquarium-ctrl/atlsscntfc-conducfltrd&lt;br /&gt;
||filteredConductivity&lt;br /&gt;
|-&lt;br /&gt;
||filtered water temperature&lt;br /&gt;
||floating point number&lt;br /&gt;
||/var/local/aquarium-ctrl/atlsscntfc-conducfltrd&lt;br /&gt;
||filteredWaterTemperature&lt;br /&gt;
|-&lt;br /&gt;
||tank level switch position&lt;br /&gt;
||floating point number&lt;br /&gt;
||/var/local/aquarium-ctrl/tnklvlsswtch&lt;br /&gt;
||tankLevelSwitchPosition&lt;br /&gt;
|-&lt;br /&gt;
||surface ventilation status&lt;br /&gt;
||string&lt;br /&gt;
||/var/local/aquarium-ctrl/srfcvntltn&lt;br /&gt;
||surfaceVentilationStatus&lt;br /&gt;
|-&lt;br /&gt;
||ambient temperature&lt;br /&gt;
||floating point number&lt;br /&gt;
||/var/local/aquarium-ctrl/ambtemp&lt;br /&gt;
||ambientTemperature&lt;br /&gt;
|-&lt;br /&gt;
||ambient humidity&lt;br /&gt;
||floating point number&lt;br /&gt;
||/var/local/aquarium-ctrl/ambhum&lt;br /&gt;
||ambientHumidity&lt;br /&gt;
|-&lt;br /&gt;
||heating status&lt;br /&gt;
||string&lt;br /&gt;
||/var/local/aquarium-ctrl/htng&lt;br /&gt;
||heatingStatus&lt;br /&gt;
|-&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Requirements for Balling feature ===&lt;br /&gt;
&lt;br /&gt;
==== Balling dosing log ====&lt;br /&gt;
The API shall provide an endpoint communicating from the server to the client the Balling dosing events read from the tables &amp;lt;code&amp;gt;balling_log&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;balling_set_vals&amp;lt;/code&amp;gt; of either the last 24 hours or the last 7 days or of a specific date depending on parameter provided by the client.&lt;br /&gt;
The corresponding SQL query (for a period of one day) is:&lt;br /&gt;
&lt;br /&gt;
For periods:&lt;br /&gt;
&amp;lt;code&amp;gt;SELECT balling_log.Timestamp, balling_log.pumpid, balling_log.dosingvolume, balling_set_vals.label FROM balling_log LEFT JOIN balling_set_vals ON balling_log.pumpid=balling_set_vals.pumpid WHERE Timestamp &amp;gt; (NOW() - INTERVAL 1 DAY) ORDER BY Timestamp&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The query for a specific date is:&lt;br /&gt;
&amp;lt;code&amp;gt;SELECT b.timestamp, b.pump_id, b.dosing_volume, s.label FROM balling_log AS b LEFT JOIN balling_set_vals AS s ON b.pump_id = s.pump_id WHERE DATE(b.timestamp) = ? ORDER BY b.timestamp DESC;&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
!|Signal name&lt;br /&gt;
!|Signal format&lt;br /&gt;
!|Database column name&lt;br /&gt;
!|API response&lt;br /&gt;
|-&lt;br /&gt;
||timestamp&lt;br /&gt;
||string&lt;br /&gt;
||balling_log.Timestamp&lt;br /&gt;
||timestamp&lt;br /&gt;
|-&lt;br /&gt;
||pump id&lt;br /&gt;
||integer number&lt;br /&gt;
||balling_log.pumpid&lt;br /&gt;
||pumpId&lt;br /&gt;
|-&lt;br /&gt;
||dosing volume&lt;br /&gt;
||floating point number&lt;br /&gt;
||balling_log.dosingvolume&lt;br /&gt;
||dosingVolume&lt;br /&gt;
|-&lt;br /&gt;
||label&lt;br /&gt;
||string&lt;br /&gt;
||balling_set_vals.label&lt;br /&gt;
||label&lt;br /&gt;
|-&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
==== Balling set values ====&lt;br /&gt;
The API shall provide an endpoint communicating from the server to the client the Balling dosing set values read from the table &amp;lt;code&amp;gt;balling_set_vals&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
The corresponding SQL query is:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;SELECT pumpid, dosingvolume, label FROM balling_set_vals;&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
!|Signal name&lt;br /&gt;
!|Signal format&lt;br /&gt;
!|Database column name&lt;br /&gt;
|-&lt;br /&gt;
||pump id&lt;br /&gt;
||integer number&lt;br /&gt;
||balling_set_vals.pumpid&lt;br /&gt;
|-&lt;br /&gt;
||dosing volume&lt;br /&gt;
||floating point number&lt;br /&gt;
||balling_set_vals.dosing_volume&lt;br /&gt;
|-&lt;br /&gt;
||label&lt;br /&gt;
||string&lt;br /&gt;
||balling_set_vals.label&lt;br /&gt;
|-&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
The API shall provide an endpoint which allows the client to update the dosing volume of an existing dosing set value identified by the pump id.&lt;br /&gt;
The corresponding SQL query is:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;UPDATE balling_set_vals SET dosing_volume=&amp;quot;[dosingVolume]&amp;quot; WHERE pump_id=&amp;quot;[pumpId]&amp;quot;;&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Requirements for feed feature ===&lt;br /&gt;
==== Feed log ====&lt;br /&gt;
The API shall provide an endpoint communicating from the server to the client the feed events read from the tables &amp;lt;code&amp;gt;feed_log&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;feed_profiles&amp;lt;/code&amp;gt; of either the last 24 hours or the last 7 days depending on parameter provided by the client.&lt;br /&gt;
The corresponding SQL query (for a period of one day) is:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;SELECT fl.timestamp, fl.feeder_on_time, fp.profile_name, fl.profile_id FROM feed_log AS fl LEFT JOIN feed_profiles AS fp ON fl.profile_id = fp.profile_id WHERE timestamp &amp;gt; (NOW() - INTERVAL 1 DAY) ORDER BY timestamp;&amp;lt;/code&amp;gt;&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
!|Signal name&lt;br /&gt;
!|Signal format&lt;br /&gt;
!|Database column name&lt;br /&gt;
|-&lt;br /&gt;
||timestamp&lt;br /&gt;
||string&lt;br /&gt;
||feed_log.timestamp&lt;br /&gt;
|-&lt;br /&gt;
||feeder on time&lt;br /&gt;
||floating point number&lt;br /&gt;
||feed_log.feeder_on_time&lt;br /&gt;
|-&lt;br /&gt;
||feed profile name&lt;br /&gt;
||string&lt;br /&gt;
||feed_profiles.profile_name&lt;br /&gt;
|-&lt;br /&gt;
||feed profile id&lt;br /&gt;
||integer numer&lt;br /&gt;
||feed_log.profile_id&lt;br /&gt;
|-&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
==== Feed profiles ====&lt;br /&gt;
A feed profile consists of general information (ID, name) and 10 groups of repetitive data where each group contains a &amp;lt;code&amp;gt;pause&amp;lt;/code&amp;gt; section and a &amp;lt;code&amp;gt;feed&amp;lt;/code&amp;gt; section.&lt;br /&gt;
&lt;br /&gt;
The API shall provide an endpoint communicating from the server to the client the feed profiles read from the table &amp;lt;code&amp;gt;feedprofiles&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
!|Signal name&lt;br /&gt;
!|Signal format&lt;br /&gt;
!|Database column name&lt;br /&gt;
!|API response&lt;br /&gt;
|-&lt;br /&gt;
||profile id&lt;br /&gt;
||integer number&lt;br /&gt;
||feed_profiles.profile_id&lt;br /&gt;
||profileId&lt;br /&gt;
|-&lt;br /&gt;
||profile name&lt;br /&gt;
||string&lt;br /&gt;
||feed_profiles.profile_name&lt;br /&gt;
||profileName&lt;br /&gt;
|-&lt;br /&gt;
||pause 01 duration&lt;br /&gt;
||integer&lt;br /&gt;
||feed_profiles.pause_01_duration&lt;br /&gt;
||pause01Duration&lt;br /&gt;
|-&lt;br /&gt;
||pause 01 skimmer target state&lt;br /&gt;
||boolean&lt;br /&gt;
||feed_profiles.pause_01_skimmer&lt;br /&gt;
||pause01Skimmer&lt;br /&gt;
|-&lt;br /&gt;
||pause 01 main pump #1 target state&lt;br /&gt;
||boolean&lt;br /&gt;
||feed_profiles.pause_01_main_pump1&lt;br /&gt;
||pause01MainPump1&lt;br /&gt;
|-&lt;br /&gt;
||pause 01 main pump #2 target state&lt;br /&gt;
||boolean&lt;br /&gt;
||feed_profiles.pause_01_main_pump2&lt;br /&gt;
||pause01MainPump2&lt;br /&gt;
|-&lt;br /&gt;
||pause 01 aux. pump #1 target state&lt;br /&gt;
||boolean&lt;br /&gt;
||feed_profiles.pause_01_aux_pump1&lt;br /&gt;
||pause01AuxPump1&lt;br /&gt;
|-&lt;br /&gt;
||pause 01 aux. pump #2 target state&lt;br /&gt;
||boolean&lt;br /&gt;
||feed_profiles.pause_01_aux_pump2&lt;br /&gt;
||pause01AuxPump2&lt;br /&gt;
|-&lt;br /&gt;
||feed 01 duration&lt;br /&gt;
||integer&lt;br /&gt;
||feed_profiles.feed_01_duration&lt;br /&gt;
||feed01Duration&lt;br /&gt;
|-&lt;br /&gt;
||feed 01 skimmer target state&lt;br /&gt;
||boolean&lt;br /&gt;
||feed_profiles.feed_01_skimmer&lt;br /&gt;
||feed01Skimmer&lt;br /&gt;
|-&lt;br /&gt;
||feed 01 main pump #1 target state&lt;br /&gt;
||boolean&lt;br /&gt;
||feed_profiles.feed_01_main_pump1&lt;br /&gt;
||feed01MainPump1&lt;br /&gt;
|-&lt;br /&gt;
||feed 01 main pump #2 target state&lt;br /&gt;
||boolean&lt;br /&gt;
||feed_profiles.feed_01_main_pump2&lt;br /&gt;
||feed01MainPump2&lt;br /&gt;
|-&lt;br /&gt;
||feed 01 aux. pump #1 target state&lt;br /&gt;
||boolean&lt;br /&gt;
||feed_profiles.feed_01_aux_pump1&lt;br /&gt;
||feed01AuxPump1&lt;br /&gt;
|-&lt;br /&gt;
||feed 01 aux. pump #2 target state&lt;br /&gt;
||boolean&lt;br /&gt;
||feed_profiles.feed_01_aux_pump2&lt;br /&gt;
||feed01AuxPump2&lt;br /&gt;
|-&lt;br /&gt;
||...&lt;br /&gt;
||...&lt;br /&gt;
||...&lt;br /&gt;
|-&lt;br /&gt;
||pause 10 duration&lt;br /&gt;
||integer&lt;br /&gt;
||feed_profiles.pause_10_duration&lt;br /&gt;
||pause10Duration&lt;br /&gt;
|-&lt;br /&gt;
||pause 10 skimmer target state&lt;br /&gt;
||boolean&lt;br /&gt;
||feed_profiles.pause_10_skimmer&lt;br /&gt;
||pause10Skimmer&lt;br /&gt;
|-&lt;br /&gt;
||pause 10 main pump #1 target state&lt;br /&gt;
||boolean&lt;br /&gt;
||feed_profiles.pause_10_main_pump1&lt;br /&gt;
||pause10MainPump1&lt;br /&gt;
|-&lt;br /&gt;
||pause 10 main pump #2 target state&lt;br /&gt;
||boolean&lt;br /&gt;
||feed_profiles.pause_10_main_pump2&lt;br /&gt;
||pause10MainPump2&lt;br /&gt;
|-&lt;br /&gt;
||pause 10 aux. pump #1 target state&lt;br /&gt;
||boolean&lt;br /&gt;
||feed_profiles.pause_10_aux_pump1&lt;br /&gt;
||pause10AuxPump1&lt;br /&gt;
|-&lt;br /&gt;
||pause 10 aux. pump #2 target state&lt;br /&gt;
||boolean&lt;br /&gt;
||feed_profiles.pause_10_aux_pump2&lt;br /&gt;
||pause10AuxPump2&lt;br /&gt;
|-&lt;br /&gt;
||feed 10 duration&lt;br /&gt;
||integer&lt;br /&gt;
||feed_profiles.feed_10_duration&lt;br /&gt;
||feed10Duration&lt;br /&gt;
|-&lt;br /&gt;
||feed 10 skimmer target state&lt;br /&gt;
||boolean&lt;br /&gt;
||feed_profiles.feed_10_skimmer&lt;br /&gt;
||feed10Skimmer&lt;br /&gt;
|-&lt;br /&gt;
||feed 10 main pump #1 target state&lt;br /&gt;
||boolean&lt;br /&gt;
||feed_profiles.feed_10_main_pump1&lt;br /&gt;
||feed10MainPump1&lt;br /&gt;
|-&lt;br /&gt;
||feed 10 main pump #2 target state&lt;br /&gt;
||boolean&lt;br /&gt;
||feed_profiles.feed_10_main_pump2&lt;br /&gt;
||feed10MainPump2&lt;br /&gt;
|-&lt;br /&gt;
||feed 10 aux. pump #1 target state&lt;br /&gt;
||boolean&lt;br /&gt;
||feed_profiles.feed_10_aux_pump1&lt;br /&gt;
||feed10AuxPump1&lt;br /&gt;
|-&lt;br /&gt;
||feed 10 aux. pump #2 target state&lt;br /&gt;
||boolean&lt;br /&gt;
||feed_profiles.feed_10_aux_pump2&lt;br /&gt;
||feed10AuxPump2&lt;br /&gt;
|-&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
The API shall provide an endpoint which allows the client to update an existing feed profile identified by the profile id.&lt;br /&gt;
&lt;br /&gt;
The API shall provide an endpoint which allows the client to create a new profile.&lt;br /&gt;
&lt;br /&gt;
The client shall only specify the name of the new profile.&lt;br /&gt;
&lt;br /&gt;
If the feed profile already exists, the endpoint shall provide an error code and not overwrite any existing data in the database.&lt;br /&gt;
&lt;br /&gt;
The related SQL query is:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;INSERT INTO feed_profiles(profile_name) VALUES(&amp;quot;[profileName]&amp;quot;);&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The API shall provide an endpoint which allows the client to execute an existing profile.&lt;br /&gt;
&lt;br /&gt;
The client shall only specify the ID of the feed profile.&lt;br /&gt;
* The API shall check if the profile identified by the ID exists in the database and output an error if the profile already exists.&lt;br /&gt;
&lt;br /&gt;
If the profile exists, then the API shall execute a shell script:&lt;br /&gt;
&amp;lt;code&amp;gt;shell_exec(&amp;quot;/usr/local/bin/aquarium_client feed execute [profileId]&amp;quot;);&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The API shall provide an endpoint which allows the client to remove an existing profile.&lt;br /&gt;
&lt;br /&gt;
The client shall only specify the ID of the feed profile.&lt;br /&gt;
&lt;br /&gt;
The related SQL query is:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;DELETE FROM feed_profiles WHERE profile_id=&amp;quot;[profileId]&amp;quot;;&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Feed schedules ====&lt;br /&gt;
The API shall provide an endpoint communicating from the server to the client the feed schedule entries read from the tables &amp;lt;code&amp;gt;feed_schedule&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;feed_profiles&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
The related SQL query is:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;SELECT fs.timestamp, fs.profile_id, fp.profile_name, fs.is_weekly, fs.is_daily FROM feed_schedule AS fs LEFT JOIN feed_profiles AS fp ON fs.profile_id = fp.profile_id;&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
!|Signal name&lt;br /&gt;
!|Signal format&lt;br /&gt;
!|Database column name&lt;br /&gt;
|-&lt;br /&gt;
||timestamp&lt;br /&gt;
||string&lt;br /&gt;
||feed_schedule.timestamp&lt;br /&gt;
|-&lt;br /&gt;
||profile id&lt;br /&gt;
||integer number&lt;br /&gt;
||feed_profiles.profile_id&lt;br /&gt;
|-&lt;br /&gt;
||profile name&lt;br /&gt;
||string&lt;br /&gt;
||feed_profiles.profile_name&lt;br /&gt;
|-&lt;br /&gt;
||weekly repetition indicator&lt;br /&gt;
||boolean&lt;br /&gt;
||feed_schedule.is_weekly&lt;br /&gt;
|-&lt;br /&gt;
||daily repetition indicator&lt;br /&gt;
||boolean&lt;br /&gt;
||feed_schedule.is_daily&lt;br /&gt;
|-&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
The API shall provide an endpoint which allows the client to update an existing feed schedule entry identified by the timestamp.&lt;br /&gt;
&lt;br /&gt;
Note: ''Depending on the database layout, an &amp;lt;code&amp;gt;UPDATE&amp;lt;/code&amp;gt; operation may not be applicable. In this case, a combined transaction of &amp;lt;code&amp;gt;DELETE&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;INSERT&amp;lt;/code&amp;gt; using rollback in case of failure shall be applied.''&lt;br /&gt;
&lt;br /&gt;
The API shall provide an endpoint which allows the client to insert a feed schedule entry.&lt;br /&gt;
* If the profile id of the feed schedule entry requested from the client does not exist, the API shall output an error message.&lt;br /&gt;
* If the feed schedule already contains an entry with a timestamp identical to the one requested from the client, the API shall output an error message.&lt;br /&gt;
&lt;br /&gt;
The related SQL query is:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;INSERT INTO feed_schedule(timestamp, profile_id, is_weekly, is_daily) VALUES(&amp;quot;[scheduleTimestamp]&amp;quot;, &amp;quot;[profileId]&amp;quot;, [scheduleRepeatWeekly], [scheduleRepeatDaily]);&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The API shall provide an endpoint which allows the client to delete an existing feed schedule entry identified by its timestamp.&lt;br /&gt;
&lt;br /&gt;
The related SQL query is:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;DELETE FROM feed_schedule WHERE timestamp=&amp;quot;[scheduleTimestamp]&amp;quot;;&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Requirements for heating feature ===&lt;br /&gt;
==== Heating set values ====&lt;br /&gt;
The API shall provide an endpoint communicating from the server to the client the heating set values read from the table &amp;lt;code&amp;gt;heating_set_vals&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
The corresponding SQL query is:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;SELECT heating_switch_off_temp, heating_switch_on_temp FROM heating_set_vals;&amp;quot;&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
!|Signal name&lt;br /&gt;
!|Signal format&lt;br /&gt;
!|Database column name&lt;br /&gt;
|-&lt;br /&gt;
||heating switch off temperature&lt;br /&gt;
||floating point number&lt;br /&gt;
||heating_set_vals.heating_switch_off_temp&lt;br /&gt;
|-&lt;br /&gt;
||heating switch on temperature&lt;br /&gt;
||floating point number&lt;br /&gt;
||heating_set_vals.heating_switch_on_temp&lt;br /&gt;
|-&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
The API shall provide an endpoint which allows the client to update both heating set values.&lt;br /&gt;
The corresponding SQL query is:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;UPDATE heating_set_vals SET heating_switch_on_temp=[heatingSwitchOnTemp], heating_switch_off_temp=[heatingSwitchOffTemp];&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Note: ''The database shall contain only one entry in the table. In case there are multiple entries, then the query will overwrite the data of all entries. This is intentional.''&lt;br /&gt;
&lt;br /&gt;
==== Heating statistical data ====&lt;br /&gt;
The API shall provide an endpoint communicating from the server to the client the heating statistical data read from the table &amp;lt;code&amp;gt;heating_stats&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
The corresponding SQL query is:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;SELECT date, energy, ambient_temp_average, water_temp_average, heating_control_runtime FROM heating_stats;&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
!|Signal name&lt;br /&gt;
!|Signal format&lt;br /&gt;
!|Database column name&lt;br /&gt;
|-&lt;br /&gt;
||date&lt;br /&gt;
||string&lt;br /&gt;
||heating_stats.date&lt;br /&gt;
|-&lt;br /&gt;
||daily energy consumption&lt;br /&gt;
||floating point number&lt;br /&gt;
||heating_stats.energy&lt;br /&gt;
|-&lt;br /&gt;
||daily average of ambient temperature&lt;br /&gt;
||floating point number&lt;br /&gt;
||heating_stats.ambient_temp_average&lt;br /&gt;
|-&lt;br /&gt;
||daily average of water temperature&lt;br /&gt;
||floating point number&lt;br /&gt;
||heating_stats.water_temp_average&lt;br /&gt;
|-&lt;br /&gt;
||heating control runtime&lt;br /&gt;
||integer number&lt;br /&gt;
||heating_stats.heating_control_runtime&lt;br /&gt;
|-&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Requirements for refill feature ===&lt;br /&gt;
==== Refill log ====&lt;br /&gt;
The API shall provide an endpoint communicating from the server to the client the refill events read from the table &amp;lt;code&amp;gt;refill_log&amp;lt;/code&amp;gt; of either the last 24 hours or the last 7 days depending on parameter provided by the client.&lt;br /&gt;
&lt;br /&gt;
The corresponding SQL query (for a period of one day) is:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;SELECT timestamp, duration, volume, error_code FROM refill_log WHERE timestamp &amp;gt; (NOW() - INTERVAL 1 DAY)&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
!|Signal name&lt;br /&gt;
!|Signal format&lt;br /&gt;
!|Database column name&lt;br /&gt;
|-&lt;br /&gt;
||timestamp&lt;br /&gt;
||string&lt;br /&gt;
||refill_log.timestamp&lt;br /&gt;
|-&lt;br /&gt;
||duration&lt;br /&gt;
||floating point number&lt;br /&gt;
||refill_log.duration&lt;br /&gt;
|-&lt;br /&gt;
||volume&lt;br /&gt;
||floating point number&lt;br /&gt;
||refill_log.volume&lt;br /&gt;
|-&lt;br /&gt;
||error code&lt;br /&gt;
||integer&lt;br /&gt;
||refill_log.error_code&lt;br /&gt;
|-&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
==== Refill controller state ====&lt;br /&gt;
The API shall provide an endpoint communicating from the server to the client the state of the refill control read from a file:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
!|Signal name&lt;br /&gt;
!|Signal format&lt;br /&gt;
!|Signal source&lt;br /&gt;
|-&lt;br /&gt;
||refill control state&lt;br /&gt;
||string&lt;br /&gt;
||/var/local/aquarium-ctrl/refillctrl&lt;br /&gt;
|-&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
The API shall provide an endpoint which allows the client to change the refill control state by executing:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;/usr/local/bin/aquarium_client refill [command]&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
!|operation&lt;br /&gt;
!|command&lt;br /&gt;
|-&lt;br /&gt;
||reset error state&lt;br /&gt;
||reset&lt;br /&gt;
|-&lt;br /&gt;
||(re-)start&lt;br /&gt;
||start&lt;br /&gt;
|-&lt;br /&gt;
||stop&lt;br /&gt;
||stop&lt;br /&gt;
|-&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Requirements for actuator schedule feature ===&lt;br /&gt;
The API shall provide an endpoint communicating from the server to the client the actuator schedule read from the table &amp;lt;code&amp;gt;schedule&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
The corresponding SQL query is:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;SELECT schedule_type, start_time, stop_time, is_active FROM schedule;&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
!|Signal name&lt;br /&gt;
!|Signal format&lt;br /&gt;
!|Database column name&lt;br /&gt;
|-&lt;br /&gt;
||schedule type&lt;br /&gt;
||string&lt;br /&gt;
||schedule.schedule_type&lt;br /&gt;
|-&lt;br /&gt;
||start time&lt;br /&gt;
||string&lt;br /&gt;
||schedule.start_time&lt;br /&gt;
|-&lt;br /&gt;
||stop time&lt;br /&gt;
||string&lt;br /&gt;
||schedule.stop_time&lt;br /&gt;
|-&lt;br /&gt;
||active indicator&lt;br /&gt;
||boolean&lt;br /&gt;
||schedule.is_active&lt;br /&gt;
|-&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
The API shall provide an endpoint that allows the client to update all actuator schedule entries.&lt;br /&gt;
The client provides the following time values as string using the format &amp;quot;hh:mm&amp;quot;:&lt;br /&gt;
* ballingRangeStartTime&lt;br /&gt;
* ballingRangeFinishTime&lt;br /&gt;
* refillRangeStartTime&lt;br /&gt;
* refillRangeFinishTime&lt;br /&gt;
* ventilationRangeStartTime&lt;br /&gt;
* ventilationRangeFinishTime&lt;br /&gt;
* heatingRangeStartTime&lt;br /&gt;
* heatingRangeFinishTime&lt;br /&gt;
&lt;br /&gt;
The client provides the following values as integer:&lt;br /&gt;
* ballingRangeIsActive&lt;br /&gt;
* refillRangeIsActive&lt;br /&gt;
* ventilationRangeIsActive&lt;br /&gt;
* heatingRangeIsActive&lt;br /&gt;
&lt;br /&gt;
The API shall check if all values were provided by the client.&lt;br /&gt;
If all values are provided, by the client, the API shall execute a set of database commands in one transaction:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
UPDATE schedule SET start_time=&amp;quot;[ballingRangeStartTime]&amp;quot;, stop_time=&amp;quot;[ballingRangeFinishTime]&amp;quot;, is_active=[ballingRangeIsActive] WHERE schedule_type=&amp;quot;balling&amp;quot;;&lt;br /&gt;
&lt;br /&gt;
UPDATE schedule SET start_time=&amp;quot;[refillRangeStartTime]&amp;quot;, stop_time=&amp;quot;[refillRangeFinishTime]&amp;quot;, is_active=[refillRangeIsActive] WHERE schedule_type=&amp;quot;refill&amp;quot;;&lt;br /&gt;
&lt;br /&gt;
UPDATE schedule SET start_time=&amp;quot;[ventilationRangeStartTime]&amp;quot;, stop_time=&amp;quot;[ventilationRangeFinishTime]&amp;quot;, is_active=[ventilationRangeIsActive] WHERE schedule_type=&amp;quot;ventilation&amp;quot;;&lt;br /&gt;
&lt;br /&gt;
UPDATE schedule SET start_time=&amp;quot;[heatingRangeStartTime]&amp;quot;, stop_time=&amp;quot;[heatingRangeFinishTime]&amp;quot;, is_active=[heatingRangeIsActive] WHERE schedule_type=&amp;quot;heating&amp;quot;;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Requirements for time data feature ===&lt;br /&gt;
The API shall provide an endpoint communicating from the server to the client the time data of the last 24 hours read from the table &amp;lt;code&amp;gt;data&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
The related SQL query is:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;SELECT timestamp, water_temperature, water_temperature_filtered, ph_value, pH_value_filtered, conductivity, conductivity_filtered, refill_in_progress, tank_level_switch_position, tank_level_switch_invalid, tank_level_switch_position_stabilized, surface_ventilation_status, ambient_temperature, ambient_humidity, heater_status FROM data WHERE (timestamp &amp;gt; (CURRENT_TIMESTAMP() - INTERVAL 1 DAY));&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
!|Signal name&lt;br /&gt;
!|Signal format&lt;br /&gt;
!|Database column name&lt;br /&gt;
!|API response&lt;br /&gt;
|-&lt;br /&gt;
||timestamp&lt;br /&gt;
||string&lt;br /&gt;
||data.timestamp&lt;br /&gt;
||timestamp&lt;br /&gt;
|-&lt;br /&gt;
||water temperature&lt;br /&gt;
||floating point number&lt;br /&gt;
||data.water_temperature&lt;br /&gt;
||waterTemperature&lt;br /&gt;
|-&lt;br /&gt;
||filtered water temperature&lt;br /&gt;
||floating point number&lt;br /&gt;
||data.water_temperature_filtered&lt;br /&gt;
||waterTemperatureFiltered&lt;br /&gt;
|-&lt;br /&gt;
||pH value&lt;br /&gt;
||floating point number&lt;br /&gt;
||data.ph_value&lt;br /&gt;
||phValue&lt;br /&gt;
|-&lt;br /&gt;
||filtered pH value&lt;br /&gt;
||floating point number&lt;br /&gt;
||data.ph_value_filtered&lt;br /&gt;
||filteredPhValue&lt;br /&gt;
|-&lt;br /&gt;
||conductivity&lt;br /&gt;
||floating point number&lt;br /&gt;
||data.conductivity&lt;br /&gt;
||conductivity&lt;br /&gt;
|-&lt;br /&gt;
||filtered conductivity&lt;br /&gt;
||floating point number&lt;br /&gt;
||data.conductivity_filtered&lt;br /&gt;
||filteredConductivity&lt;br /&gt;
|-&lt;br /&gt;
||refill in progress&lt;br /&gt;
||boolean&lt;br /&gt;
||data.refill_in_progress&lt;br /&gt;
||refillInProgress&lt;br /&gt;
|-&lt;br /&gt;
||tank level switch position&lt;br /&gt;
||boolean&lt;br /&gt;
||data.tank_level_switch_position&lt;br /&gt;
||tankLevelSwitchPosition&lt;br /&gt;
|-&lt;br /&gt;
||tank level switch validity indicator&lt;br /&gt;
||boolean&lt;br /&gt;
||data.tank_level_switch_invalid&lt;br /&gt;
||tankLevelSwitchInvalid&lt;br /&gt;
|-&lt;br /&gt;
||tank level switch position stabilized&lt;br /&gt;
||boolean&lt;br /&gt;
||data.tank_level_switch_position_stabilized&lt;br /&gt;
||tankLevelSwitchPositionStabilized&lt;br /&gt;
|-&lt;br /&gt;
||surface ventilation status&lt;br /&gt;
||boolean&lt;br /&gt;
||data.surface_ventilation_status&lt;br /&gt;
||surfaceVentilationStatus&lt;br /&gt;
|-&lt;br /&gt;
||ambient temperature&lt;br /&gt;
||floating point number&lt;br /&gt;
||data.ambient_temperature&lt;br /&gt;
||ambientTemperature&lt;br /&gt;
|-&lt;br /&gt;
||ambient humidity&lt;br /&gt;
||floating point number&lt;br /&gt;
||data.ambient_humidity&lt;br /&gt;
||ambientHumidity&lt;br /&gt;
|-&lt;br /&gt;
||heater status&lt;br /&gt;
||boolean&lt;br /&gt;
||data.heater_status&lt;br /&gt;
||heaterStatus&lt;br /&gt;
|-&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Requirements for ventilation feature ===&lt;br /&gt;
==== Ventilation set values ====&lt;br /&gt;
The API shall provide an endpoint communicating from the server to the client the ventilation set values read from the table &amp;lt;code&amp;gt;ventilation_set_vals&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
The corresponding SQL query is:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;SELECT ventilation_switch_off_temp, ventilation_switch_on_temp FROM ventilation_set_vals;&amp;quot;&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
!|Signal name&lt;br /&gt;
!|Signal format&lt;br /&gt;
!|Database column name&lt;br /&gt;
|-&lt;br /&gt;
||ventilation switch off temperature&lt;br /&gt;
||floating point number&lt;br /&gt;
||ventilation_set_vals.ventilation_switch_off_temp&lt;br /&gt;
|-&lt;br /&gt;
||ventilation switch on temperature&lt;br /&gt;
||floating point number&lt;br /&gt;
||ventilation_set_vals.ventilation_switch_on_temp&lt;br /&gt;
|-&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
The API shall provide an endpoint which allows the client to update both ventilation set values.&lt;br /&gt;
The corresponding SQL query is:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;UPDATE ventilation_set_vals SET ventilation_switch_on_temp=[ventilationSwitchOnTemp], ventilation_switch_off_temp=[ventilationSwitchOffTemp];&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Note: ''The database shall contain only one entry in the table. In case there are multiple entries, then the query will overwrite the data of all entries. This is intentional.''&lt;br /&gt;
&lt;br /&gt;
== Architecture ==&lt;br /&gt;
The API is distributed over the following files:&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
!|File name&lt;br /&gt;
!|Content description&lt;br /&gt;
|-&lt;br /&gt;
||api.php&lt;br /&gt;
||Main API functionality&lt;br /&gt;
|-&lt;br /&gt;
||db.php&lt;br /&gt;
||Adapter for connecting to SQL database&lt;br /&gt;
|-&lt;br /&gt;
||functions.php&lt;br /&gt;
||helper functionality repeatedly used throughout the API&lt;br /&gt;
|-&lt;br /&gt;
||test_reset_db.php&lt;br /&gt;
||functionality to reset and initialise the database with mock data: Do not deploy this file in productive environment!&lt;br /&gt;
|-&lt;br /&gt;
||test.php&lt;br /&gt;
||functionality to test access to database for development purposed. Do not deploy this file in productive environment!&lt;br /&gt;
|-&lt;br /&gt;
||test_generate_mock_data.php&lt;br /&gt;
||functionality to create mock data for front end testing. Do not deploy this file in productive environment!&lt;br /&gt;
|-&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Implementation ==&lt;br /&gt;
The behaviour of the &amp;lt;code&amp;gt;api.php&amp;lt;/code&amp;gt; is controlled by the first &amp;lt;code&amp;gt;action&amp;lt;/code&amp;gt; argument provided via &amp;lt;code&amp;gt;GET&amp;lt;/code&amp;gt; method which can assume the following values:&lt;br /&gt;
=== check_auth ===&lt;br /&gt;
The API responds with a JSON object containing the following data:&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
!|Label&lt;br /&gt;
!|Value&lt;br /&gt;
|-&lt;br /&gt;
||status&lt;br /&gt;
||message&lt;br /&gt;
|-&lt;br /&gt;
||authorized&lt;br /&gt;
||Credentials valid&lt;br /&gt;
|-&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== test_hash ===&lt;br /&gt;
The API responds with a JSON object containing the following data:&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
!|Label&lt;br /&gt;
!|Value&lt;br /&gt;
|-&lt;br /&gt;
||input&lt;br /&gt;
||hash&lt;br /&gt;
|-&lt;br /&gt;
||[Input value provided]&lt;br /&gt;
||[Hash calculated from the input value]&lt;br /&gt;
|-&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== load_overview_signals ===&lt;br /&gt;
When using this parameter value, the API provides the overview data in JSON format as per [[REST_API#Requirements_for_overview_feature|above requirement]].&lt;br /&gt;
&lt;br /&gt;
=== load_balling_log ===&lt;br /&gt;
When using this parameter value, the API provides the balling dosing log as per [[REST_API#Balling_dosing_log|above requirements]].&lt;br /&gt;
&lt;br /&gt;
The second parameter &amp;lt;code&amp;gt;period&amp;lt;/code&amp;gt; determines the period: &amp;lt;code&amp;gt;7d&amp;lt;/code&amp;gt; for the last 7 days, otherwise only the last 24 hours.&lt;br /&gt;
&lt;br /&gt;
=== load_balling_set_vals ===&lt;br /&gt;
When using this parameter value, the API provides the Balling mineral dosing set values of all configured pumps in JSON format as per [[REST_API#Balling_set_values|above requirement]].&lt;br /&gt;
&lt;br /&gt;
=== update_balling_set_vals ===&lt;br /&gt;
When using this parameter value, the API checks if the required &amp;lt;code&amp;gt;POST&amp;lt;/code&amp;gt; parameters were provided:&lt;br /&gt;
* dosingVolume&lt;br /&gt;
* pumpId&lt;br /&gt;
&lt;br /&gt;
If the parameters are provided, the API will issue the SQL statement to update the dosing volume for the dedicated pump as per [[REST_API#Balling_set_values|above requirements]].&lt;br /&gt;
&lt;br /&gt;
=== load_feed_log ===&lt;br /&gt;
When using this parameter value, the API provides the balling dosing log as per [[REST_API#Feed_log|above requirements]].&lt;br /&gt;
&lt;br /&gt;
The second parameter &amp;lt;code&amp;gt;period&amp;lt;/code&amp;gt; determines the period: &amp;lt;code&amp;gt;7d&amp;lt;/code&amp;gt; for the last 7 days, otherwise only the last 24 hours.&lt;br /&gt;
&lt;br /&gt;
=== load_feed_profiles ===&lt;br /&gt;
When using this parameter value, the API provides all existing feed profile in JSON format as per [[REST_API#Feed_profiles|above requirement]].&lt;br /&gt;
&lt;br /&gt;
=== update_feed_profile ===&lt;br /&gt;
When using this parameter value, the API will check if the &amp;lt;code&amp;gt;profileId&amp;lt;/code&amp;gt; is provided as POST parameter.&lt;br /&gt;
&lt;br /&gt;
All other parameters provided will be type checked if they are integer or string types.&lt;br /&gt;
&lt;br /&gt;
The API will issue the SQL statement to update the specific feed profile identified by &amp;lt;code&amp;gt;profileId&amp;lt;/code&amp;gt; as per [[REST_API#Feed_profiles|above requirement]].&lt;br /&gt;
&lt;br /&gt;
=== create_feed_profile ===&lt;br /&gt;
When using this parameter value, the query will do the following steps [[REST_API#create_feed_profile|as per above requirement]]:&lt;br /&gt;
* check if the parameter &amp;lt;code&amp;gt;profileName&amp;lt;/code&amp;gt; is provided via &amp;lt;code&amp;gt;POST&amp;lt;/code&amp;gt; method.&lt;br /&gt;
* issue an SQL statement to check if a profile with that name already exists in the database, issue an error response and abort further execution if indeed there already is such a profile&lt;br /&gt;
* issue and SQL statement to insert an empty feed profile into the data base using &amp;lt;code&amp;gt;profileName&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== execute_feed_profile ===&lt;br /&gt;
When using this parameter value, the API will check for the additional parameter &amp;lt;code&amp;gt;profileId&amp;lt;/code&amp;gt; provided via &amp;lt;code&amp;gt;GET&amp;lt;/code&amp;gt; method.&lt;br /&gt;
If the parameter is provided, the API will issue an SQL statement to check if the database contains a matching feed profile, issue an error message and abort execution if there is no such profile.&lt;br /&gt;
If the profile exists, the API will execute the external program &amp;lt;code&amp;gt;aquarium_client&amp;lt;/code&amp;gt; using the keywords &amp;lt;code&amp;gt;feed&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;execute&amp;lt;/code&amp;gt; and the feed profile ID as parameter as per [[REST_API#Feed_profiles|above requirement]].&lt;br /&gt;
&lt;br /&gt;
=== delete_feed_profile ===&lt;br /&gt;
When using this parameter value, the API checks if the parameter &amp;lt;code&amp;gt;profileId&amp;lt;/code&amp;gt; is provided via &amp;lt;code&amp;gt;GET&amp;lt;/code&amp;gt; method. If the parameter is provided, the API will issue an SQL statement to delete the matching feed profile from the database as per [[REST_API#Feed_profiles|above requirements]].&lt;br /&gt;
&lt;br /&gt;
=== load_feed_schedule ===&lt;br /&gt;
When using this parameter value, the API will load the feed schedule entries as per [[REST_API#Feed_schedules|above requirement]].&lt;br /&gt;
&lt;br /&gt;
The SQL query uses a &amp;lt;code&amp;gt;LEFT JOIN&amp;lt;/code&amp;gt; where the profile name is read from the &amp;lt;code&amp;gt;feed_profile&amp;lt;/code&amp;gt; table identified by the &amp;lt;code&amp;gt;profile_id&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== create_feed_schedule_entry ===&lt;br /&gt;
When using this parameter value, the API will check if the following parameters were provided via &amp;lt;code&amp;gt;POST&amp;lt;/code&amp;gt; method:&lt;br /&gt;
* &amp;lt;code&amp;gt;profileId&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;timestamp&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;scheduleIsWeekly&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;scheduleIsDaily&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Note: Functionality for &amp;lt;code&amp;gt;scheduleIsWeekly&amp;lt;/code&amp;gt; is not implemented as of December 2025.&lt;br /&gt;
&lt;br /&gt;
When the additional parameters are provided, the API will issue an SQL statement to check if a matching feed schedule entry already exists and abort further execution if that is the case.&lt;br /&gt;
&lt;br /&gt;
Otherwise, the API will issue a further SQL statement to insert the feed schedule entry into the database as per [[REST_API#Feed_schedules|above requirement]].&lt;br /&gt;
&lt;br /&gt;
=== update_feed_schedule_entry ===&lt;br /&gt;
When using this parameter value, the API will check if the following parameters were provided via &amp;lt;code&amp;gt;POST&amp;lt;/code&amp;gt; method and abort execution if this is not the case:&lt;br /&gt;
* &amp;lt;code&amp;gt;timestamp&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;profileId&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;isWeekly&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;isDaily&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
When the parameters are provided, the API will execute one SQL transaction containing two statements:&lt;br /&gt;
* The first statement will delete the feed schedule entry identified by the timestamp.&lt;br /&gt;
* The second statement will insert a new feed schedule entry using the additionally provided parameter.&lt;br /&gt;
&lt;br /&gt;
This will effectively update an existing feed schedule entry as per [[REST_API#Feed_schedules|above requirement]].&lt;br /&gt;
&lt;br /&gt;
If the transaction throws an exception (when one of the SQL statements fails), the API will trigger the rollback of the transaction.&lt;br /&gt;
&lt;br /&gt;
=== delete_feed_schedule_entry ===&lt;br /&gt;
When using this parameter value, the API will check if the &amp;lt;code&amp;gt;timestamp&amp;lt;/code&amp;gt; was provided using &amp;lt;code&amp;gt;POST&amp;lt;/code&amp;gt; method and abort execution if this is not the case.&lt;br /&gt;
&lt;br /&gt;
When the additional timestamp parameter is provided, the API will issue an SQL statement to delete the corresponding feed schedule entry as per [[REST_API#Feed_schedules|above requirement]].&lt;br /&gt;
&lt;br /&gt;
=== load_heating_set_vals ===&lt;br /&gt;
When using this parameter value, the API will load the heating control set values as per [[REST_API#Heating_set_values|above requirement]].&lt;br /&gt;
&lt;br /&gt;
=== update_heating_set_vals ===&lt;br /&gt;
When using this parameter value, the API will check if the following parameters were provided via &amp;lt;code&amp;gt;POST&amp;lt;/code&amp;gt; method and will abort execution if this is not the case:&lt;br /&gt;
* heatingSwitchOnTemp&lt;br /&gt;
* heatingSwitchOffTemp&lt;br /&gt;
&lt;br /&gt;
When the parameters are provided, the API will issue an SQL statement update the heating control set values as per [[REST_API#Heating_set_values|above requirement]].&lt;br /&gt;
&lt;br /&gt;
=== load_heating_stats ===&lt;br /&gt;
When using this parameter value, the API will load the heating statistical data as per [[REST_API#Heating_statistical_data|above requirement]].&lt;br /&gt;
&lt;br /&gt;
=== load_refill_log ===&lt;br /&gt;
When using this parameter value, the API provides the refill log as per [[REST_API#Refill_log|above requirements]].&lt;br /&gt;
&lt;br /&gt;
The second parameter &amp;lt;code&amp;gt;period&amp;lt;/code&amp;gt; determines the period: &amp;lt;code&amp;gt;7d&amp;lt;/code&amp;gt; for the last 7 days, otherwise only the last 24 hours.&lt;br /&gt;
&lt;br /&gt;
=== load_refill_state ===&lt;br /&gt;
When this parameter value is provided, the API will load the refill control state information from &amp;lt;code&amp;gt;/var/local/aquarium-ctrl/refillctrl&amp;lt;/code&amp;gt; as per [[REST_API#Refill_controller_state|above requirement]].&lt;br /&gt;
&lt;br /&gt;
=== set_refill_state ===&lt;br /&gt;
When using this parameter value, the API will check if an additional parameter named &amp;lt;code&amp;gt;command&amp;lt;/code&amp;gt; was provided via &amp;lt;code&amp;gt;GET&amp;lt;/code&amp;gt; method.&lt;br /&gt;
&lt;br /&gt;
The API will check if the parameter has a valid value. Valid values are:&lt;br /&gt;
* reset&lt;br /&gt;
* start&lt;br /&gt;
* stop&lt;br /&gt;
&lt;br /&gt;
When the additional parameter is provided with a valid value, the API will execute the command &amp;lt;code&amp;gt;/usr/local/bin/aquarium_client refill [command]&amp;lt;/code&amp;gt;. This will update the refill control state as per [[REST_API#Refill_controller_state]] above requirement.&lt;br /&gt;
&lt;br /&gt;
When no valid value is provided, the API will abort execution.&lt;br /&gt;
&lt;br /&gt;
=== load_actuator_schedule ===&lt;br /&gt;
When using this parameter value, the API will load the actuator schedule as per [[REST_API#Requirements_for_actuator_schedule_feature|above requirement]].&lt;br /&gt;
&lt;br /&gt;
For testing purposes, the schedule entries are provided in alphabetical order.&lt;br /&gt;
&lt;br /&gt;
=== update_actuator_schedule ===&lt;br /&gt;
When using this parameter value, the API will check if the following parameters were provided via &amp;lt;code&amp;gt;POST&amp;lt;/code&amp;gt; method and abort execution if this is not the case:&lt;br /&gt;
* ballingRangeStartTime&lt;br /&gt;
* ballingRangeFinishTime&lt;br /&gt;
* ballingRangeIsActive&lt;br /&gt;
* refillRangeStartTime&lt;br /&gt;
* refillRangeFinishTime&lt;br /&gt;
* refillRangeIsActive,&lt;br /&gt;
* ventilationRangeStartTime&lt;br /&gt;
* ventilationRangeFinishTime&lt;br /&gt;
* ventilationRangeIsActive&lt;br /&gt;
* heatingRangeStartTime&lt;br /&gt;
* heatingRangeFinishTime&lt;br /&gt;
* heatingRangeIsActive&lt;br /&gt;
&lt;br /&gt;
When all parameters are provided, the API will initiate a transaction consisting of four SQL statements for each actuator schedule.&lt;br /&gt;
&lt;br /&gt;
The SQL statement will update the actuator schedule as per above [[REST_API#Requirements_for_actuator_schedule_feature|requirement]].&lt;br /&gt;
&lt;br /&gt;
=== load_time_data ===&lt;br /&gt;
When using this parameter value, the API will load the time data as per [[REST_API#Requirements_for_time_data_feature|above requirement]].&lt;br /&gt;
&lt;br /&gt;
=== load_ventilation_set_vals ===&lt;br /&gt;
When using this parameter value, the API will load the ventilation control set values as per [[REST_API#Ventilation_set_values|above requirement]].&lt;br /&gt;
&lt;br /&gt;
=== update_ventilation_set_vals ===&lt;br /&gt;
When using this parameter value, the API will check if the following parameters were provided via &amp;lt;code&amp;gt;POST&amp;lt;/code&amp;gt; method and will abort execution if this is not the case:&lt;br /&gt;
* ventilationSwitchOnTemp&lt;br /&gt;
* ventilationSwitchOffTemp&lt;br /&gt;
&lt;br /&gt;
When the parameters are provided, the API will issue an SQL statement update the ventilation control set values as per [[REST_API#Ventilation_set_values|above requirement]].&lt;br /&gt;
&lt;br /&gt;
== Testing ==&lt;br /&gt;
Postman is used for testing of the API.&lt;br /&gt;
&lt;br /&gt;
The requests aggregated in collections.&lt;br /&gt;
&lt;br /&gt;
Each collection (except for the General collection) starts with a request to reset the database (truncating all tables and adding mock data).&lt;br /&gt;
&lt;br /&gt;
The server-based script for resetting the database cannot be run in parallel - limiting the ability to run all tests in parallel.&lt;br /&gt;
&lt;br /&gt;
Test execution is manual per collection.&lt;br /&gt;
&lt;br /&gt;
Reference values are stored in the Variables section of each collection.&lt;br /&gt;
&lt;br /&gt;
=== Feed collection ===&lt;br /&gt;
The collection executes the following requests:&lt;br /&gt;
* Reset database (GET)&lt;br /&gt;
* Load feed log (GET)&lt;br /&gt;
* Initial Load feed profiles (GET)&lt;br /&gt;
* Update feed profile (POST)&lt;br /&gt;
* Secondary Load feed profiles (GET): This request will verify if the previous request could modify the database successfully.&lt;br /&gt;
* Initial Create feed profile (POST)&lt;br /&gt;
* Secondary Create feed profile (POST): This request will verify if trying to create a new feed profile with an already existing name is rejected.&lt;br /&gt;
* Execute feed profile (GET)&lt;br /&gt;
* Create feed schedule entry (POST)&lt;br /&gt;
* Load feed schedule (GET)&lt;br /&gt;
* Update feed schedule entry (POST)&lt;br /&gt;
* Delete feed schedule entry (POST)&lt;br /&gt;
* Delete feed profile (GET)&lt;br /&gt;
&lt;br /&gt;
=== General collection ===&lt;br /&gt;
The collection executes the following requests:&lt;br /&gt;
* Hash test (GET)&lt;br /&gt;
* Check authorisation (GET)&lt;br /&gt;
&lt;br /&gt;
=== Balling collection ===&lt;br /&gt;
The collection executes the following requests:&lt;br /&gt;
* Reset database (GET)&lt;br /&gt;
* Load Balling log (GET)&lt;br /&gt;
* Initial Load Balling set values (GET)&lt;br /&gt;
* Update Balling set values (POST)&lt;br /&gt;
* Secondary Load Balling set values (GET): This request will verify if the previous request could successfully modify the database.&lt;br /&gt;
&lt;br /&gt;
=== Overview collection ===&lt;br /&gt;
The collection executes the following requests:&lt;br /&gt;
* Reset database (GET)&lt;br /&gt;
* Load overview signals (GET)&lt;br /&gt;
&lt;br /&gt;
=== Refill collection ===&lt;br /&gt;
The collection executes the following requests:&lt;br /&gt;
* Reset database (GET)&lt;br /&gt;
* Load refill log (GET)&lt;br /&gt;
* Load refill control state (GET)&lt;br /&gt;
* Set refill control state (GET)&lt;br /&gt;
&lt;br /&gt;
=== Actuator schedule collection ===&lt;br /&gt;
The collection executes the following requests:&lt;br /&gt;
* Reset database (GET)&lt;br /&gt;
* Initial Load actuator schedule (GET)&lt;br /&gt;
* Update actuator schedule (POST)&lt;br /&gt;
* Secondary Load actuator schedule (GET): This request will verify if the previous request could successfully modify the database.&lt;br /&gt;
&lt;br /&gt;
=== Time data collection ===&lt;br /&gt;
The collection executes the following requests:&lt;br /&gt;
* Reset database (GET)&lt;br /&gt;
* Load time data (GET)&lt;br /&gt;
&lt;br /&gt;
=== Ventilation collection ===&lt;br /&gt;
The collection executes the following requests:&lt;br /&gt;
* Reset database (GET)&lt;br /&gt;
* Initial Load ventilation set values (GET)&lt;br /&gt;
* Update ventilation set values (POST)&lt;br /&gt;
* Secondary Load ventilation set values (GET): This request will check if the previous request could successfully modify the database.&lt;br /&gt;
&lt;br /&gt;
=== Heating collection ===&lt;br /&gt;
The collection executes the following requests:&lt;br /&gt;
* Reset database&lt;br /&gt;
* Initial Load heating set values (GET)&lt;br /&gt;
* Update heating set values (POST)&lt;br /&gt;
* Secondary Load heating set values (GET): This request will verify if the previous request could successfully modify the database.&lt;br /&gt;
* Load heating statistics (GET)&lt;/div&gt;</summary>
		<author><name>Uwe</name></author>
	</entry>
	<entry>
		<id>http://217.79.180.177/mediawiki/index.php?title=Android_application&amp;diff=456</id>
		<title>Android application</title>
		<link rel="alternate" type="text/html" href="http://217.79.180.177/mediawiki/index.php?title=Android_application&amp;diff=456"/>
		<updated>2026-04-26T06:47:16Z</updated>

		<summary type="html">&lt;p&gt;Uwe: /* Unit tests */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;The app is available in the [https://play.google.com/store/apps/details?id=com.laimburggasse.aquariumcontroller Play store].&lt;br /&gt;
== Requirements ==&lt;br /&gt;
== Architecture ==&lt;br /&gt;
The app is written completely in Kotlin using Jetpack Compose, Dagger/Hilt and aiming to implement a clean architecture.&lt;br /&gt;
=== Module structure ===&lt;br /&gt;
Besides the main app module, the app consists of the following library modules:&lt;br /&gt;
* balling&lt;br /&gt;
* common&lt;br /&gt;
* controller&lt;br /&gt;
* feed&lt;br /&gt;
* heating&lt;br /&gt;
* info&lt;br /&gt;
* overview&lt;br /&gt;
* refill&lt;br /&gt;
* schedule&lt;br /&gt;
* timedata&lt;br /&gt;
* ventilation&lt;br /&gt;
&lt;br /&gt;
The main &amp;lt;code&amp;gt;app&amp;lt;/code&amp;gt; module contains the AndroidManifest and the main activity (As of November 2025, the app only uses one activity).&lt;br /&gt;
The activity contains the navigation &amp;lt;code&amp;gt;NavHost&amp;lt;/code&amp;gt; in &amp;lt;code&amp;gt;onCreate&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
None of the other modules has a dependency to the main &amp;lt;code&amp;gt;app&amp;lt;/code&amp;gt; module.&lt;br /&gt;
&lt;br /&gt;
The &amp;lt;code&amp;gt;common&amp;lt;/code&amp;gt; module provides low-level functionalities and layout elements used in various places.&lt;br /&gt;
The following functionalities are located in &amp;lt;code&amp;gt;common&amp;lt;/code&amp;gt; module:&lt;br /&gt;
* &amp;lt;code&amp;gt;DataFetchResult&amp;lt;/code&amp;gt;: A wrapper class indicating the status of data fetching operation (&amp;lt;code&amp;gt;Success&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Error&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Loading&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Empty&amp;lt;/code&amp;gt;).&lt;br /&gt;
* &amp;lt;code&amp;gt;WebApiParams&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;WebApiPostRequest&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;WebApiPostRequestExecution&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;WebApiUrls&amp;lt;/code&amp;gt;: Classes and objects used for communication from the app to the server.&lt;br /&gt;
* &amp;lt;code&amp;gt;ControllerProfileDao&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;ControllerProfileDatabase&amp;lt;/code&amp;gt;: Classes which provide Room database functionality.&lt;br /&gt;
* Various classes used for dependency injection (Hilt)&lt;br /&gt;
* &amp;lt;code&amp;gt;SetValsSanityCheckResult&amp;lt;/code&amp;gt;: Class used for to communicate status of set values for &amp;lt;code&amp;gt;heating&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;ventilation&amp;lt;/code&amp;gt; module.&lt;br /&gt;
* &amp;lt;code&amp;gt;ControllerProfile&amp;lt;/code&amp;gt;: Class containing the profile data of the server (address, port, credentials, ...)&lt;br /&gt;
* &amp;lt;code&amp;gt;GlobalConstants&amp;lt;/code&amp;gt;: Mainly UI-related strings (non-context-related) and some minor functional constants&lt;br /&gt;
* Composable functions for the main drop down menu&lt;br /&gt;
* Composable functions for the theme&lt;br /&gt;
* Composable functions for hyperlinks, text edit fields, headline&lt;br /&gt;
* &amp;lt;code&amp;gt;ViewNavigationRoutes&amp;lt;/code&amp;gt;: Object containing the routing information processed in main &amp;lt;code&amp;gt;app&amp;lt;/code&amp;gt; module&lt;br /&gt;
* &amp;lt;code&amp;gt;ScreenshotTestHelper&amp;lt;/code&amp;gt;: Helper class for executing the instrumented snapshot testing using the emulator.&lt;br /&gt;
&lt;br /&gt;
=== Layer structure ===&lt;br /&gt;
The code is separated into the following layers:&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
[module]/&lt;br /&gt;
&lt;br /&gt;
├── src/main/kotlin/com/laimburggasse/aquariumcontrol/[module]/data/&lt;br /&gt;
&lt;br /&gt;
│   ├── remote/                   // Folder for data retrieval functionality&lt;br /&gt;
&lt;br /&gt;
│   │   └── dto/                  // Folder for Data Transfer Objects&lt;br /&gt;
&lt;br /&gt;
│   │       └── [module][name]Import.kt&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module][name]Reader.kt      // Implementation of GET server request&lt;br /&gt;
&lt;br /&gt;
│   ├── [module][name]RepositoryImpl.kt  // Repositories (implementation of interface)&lt;br /&gt;
&lt;br /&gt;
├── src/main/kotlin/com/laimburggasse/aquariumcontrol/[module]/domain/&lt;br /&gt;
&lt;br /&gt;
│   ├── model/                    // Folder for data definition classes&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module][name].kt.    // Classes for data definition&lt;br /&gt;
&lt;br /&gt;
│   ├── repository/               // Folder containing repository interface definitions&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module][name]Repository.kt.  // Interface defining the repository, used by view model&lt;br /&gt;
&lt;br /&gt;
├── src/main/kotlin/com/laimburggasse/aquariumcontrol/[module]/presentation/&lt;br /&gt;
&lt;br /&gt;
│   ├── entry/                    // First screen of feature&lt;br /&gt;
&lt;br /&gt;
│   ├── [components]/             // Shared code between different screens&lt;br /&gt;
&lt;br /&gt;
│   ├── [screen name]/            // Folder for each screen&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module][name]Screen.kt       // Composable showing the complete screen&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module][name]Composable.kt   // Composable showing elements inside the screen&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module][name]UiEvent.kt      // Sealed interface describing the communication from the composable to the view model&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module][name]UiState.kt      // Data class describing the content to be rendered by the composable&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module][name]ViewModel.kt    // Class implementing the view model&lt;br /&gt;
&lt;br /&gt;
│   ├── navigation/               // Folder containing class and functionality related to screen navigation&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module]NavigationEvent.kt  // One sealed class per module containing the navigation intent of the user&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module]Navigator.kt  // Module-specific navigation functionality&lt;br /&gt;
&lt;br /&gt;
│&lt;br /&gt;
└── build.gradle                  // Plugins, setting, dependencies&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Screen navigation ===&lt;br /&gt;
The screen navigation is controlled by a &amp;lt;code&amp;gt;NavHostController&amp;lt;/code&amp;gt; which is created in &amp;lt;code&amp;gt;MainActivity&amp;lt;/code&amp;gt; and remembered using &amp;lt;code&amp;gt;rememberNavController&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
==== Modular navigation ====&lt;br /&gt;
Each module adds its own navigation function to &amp;lt;code&amp;gt;NavHostController&amp;lt;/code&amp;gt;.&lt;br /&gt;
These extension functions are stored in &amp;lt;code&amp;gt;[module]/presentation/navigation/[module]Navigator.kt&amp;lt;/code&amp;gt;.&lt;br /&gt;
They encapsulate the routing logic for that specific module.&lt;br /&gt;
&lt;br /&gt;
==== Event-Driven Navigation ====&lt;br /&gt;
The functions receive as parameter a specific sealed class &amp;lt;code&amp;gt;[module]NavigationEvent&amp;lt;/code&amp;gt;.&lt;br /&gt;
These sealed classes contain objects for each navigation event inside the module.&lt;br /&gt;
&lt;br /&gt;
==== Implementation pattern ====&lt;br /&gt;
The navigation functions are used in the following locations:&lt;br /&gt;
* inside the lambda function passed to &amp;lt;code&amp;gt;NavHost&amp;lt;/code&amp;gt; in &amp;lt;code&amp;gt;MainActivity&amp;lt;/code&amp;gt; for each screen.&lt;br /&gt;
* inside composables themselves being passed as lambda function from within &amp;lt;code&amp;gt;MainActivity&amp;lt;/code&amp;gt;&lt;br /&gt;
:* The composable remain unaware of &amp;lt;code&amp;gt;NavHostController&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Routes and arguments ====&lt;br /&gt;
Source and destination of the routing is defined using strings from the object &amp;lt;code&amp;gt;ViewNavigationRoutes&amp;lt;/code&amp;gt; where data that needs to be transferred between screens is embedded in the strings.&lt;br /&gt;
&lt;br /&gt;
== Example workflow for adding API communication ==&lt;br /&gt;
=== Implementation ===&lt;br /&gt;
Create the model: &amp;lt;code&amp;gt;RefillState&amp;lt;/code&amp;gt; as &amp;lt;code&amp;gt;enum&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Add a reader &amp;lt;code&amp;gt;class&amp;lt;/code&amp;gt; &amp;lt;code&amp;gt;RefillStateReader&amp;lt;/code&amp;gt; which uses the injected interface (Retrofit)&lt;br /&gt;
&lt;br /&gt;
Update the &amp;lt;code&amp;gt;RefillStateApiService&amp;lt;/code&amp;gt;: Add a function to load the state via Retrofit&lt;br /&gt;
&lt;br /&gt;
Add the label for the JSON property in &amp;lt;code&amp;gt;WebApiParams&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Add the URL for the end point in &amp;lt;code&amp;gt;WebApiUrls&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Add a wrapper &amp;lt;code&amp;gt;RefillStateResponse&amp;lt;/code&amp;gt; for the model to allow Retrofit parsing the JSON&lt;br /&gt;
&lt;br /&gt;
Update the repository interface and the repository interface implementation:&lt;br /&gt;
* Add a flow that delivers a &amp;lt;code&amp;gt;DataFetchResult&amp;lt;/code&amp;gt; of the &amp;lt;code&amp;gt;RefillState&amp;lt;/code&amp;gt;&lt;br /&gt;
* Add a function that allows the view model to trigger a refresh&lt;br /&gt;
&lt;br /&gt;
Add a wrapper &amp;lt;code&amp;gt;class&amp;lt;/code&amp;gt; &amp;lt;code&amp;gt;RefillStateUiState&amp;lt;/code&amp;gt;&lt;br /&gt;
Update the view model&lt;br /&gt;
* Add a flow that delivers a &amp;lt;code&amp;gt;RefillStateUiState&amp;lt;/code&amp;gt; based on the &amp;lt;code&amp;gt;DataFetchResult&amp;lt;/code&amp;gt; from repository&lt;br /&gt;
* Handle the new UI event for refreshing&lt;br /&gt;
&lt;br /&gt;
Add composable &amp;lt;code&amp;gt;RefillStateComposable&amp;lt;/code&amp;gt; for displaying the &amp;lt;code&amp;gt;RefillState&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Update &amp;lt;code&amp;gt;RefillUiEvent&amp;lt;/code&amp;gt; with an object for the refresh&lt;br /&gt;
&lt;br /&gt;
Update &amp;lt;code&amp;gt;RefillScreen&amp;lt;/code&amp;gt;:&lt;br /&gt;
* additional parameter (&amp;lt;code&amp;gt;RefillStateUiState&amp;lt;/code&amp;gt;) in composable function and in previews&lt;br /&gt;
* Added &amp;lt;code&amp;gt;PullToRefreshBox&amp;lt;/code&amp;gt;&lt;br /&gt;
* Added parsing of &amp;lt;code&amp;gt;RefillUiState&amp;lt;/code&amp;gt; and display of either&lt;br /&gt;
:* &amp;lt;code&amp;gt;RefillStateComposable&amp;lt;/code&amp;gt; in case of success&lt;br /&gt;
:* Text-based error message in case of error&lt;br /&gt;
:* &amp;lt;code&amp;gt;CircularProgressIndicator&amp;lt;/code&amp;gt; in case of loading&lt;br /&gt;
&lt;br /&gt;
Inject &amp;lt;code&amp;gt;RefillUiState&amp;lt;/code&amp;gt; into &amp;lt;code&amp;gt;RefillScreen&amp;lt;/code&amp;gt; in the &amp;lt;code&amp;gt;MainActivity&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;RefillScreenTest&amp;lt;/code&amp;gt; as well as in previews of &amp;lt;code&amp;gt;RefillScreen&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
(Optional) Check if &amp;lt;code&amp;gt;RefillDataModule&amp;lt;/code&amp;gt; needs update (not required in this case)&lt;br /&gt;
&lt;br /&gt;
=== Testing ===&lt;br /&gt;
Update existing screenshot testing (compose screenshot testing and instrumented tests)&lt;br /&gt;
&lt;br /&gt;
Add compose screenshot testing for new composable&lt;br /&gt;
&lt;br /&gt;
== Test strategy ==&lt;br /&gt;
The app uses different testing approaches:&lt;br /&gt;
* Compose preview screenshot testing&lt;br /&gt;
* Screenshot tests running as instrumented tests on emulator&lt;br /&gt;
* Unit tests&lt;br /&gt;
&lt;br /&gt;
=== Compose preview screenshot tests ===&lt;br /&gt;
These tests run comparably fast and device-independent.&lt;br /&gt;
&lt;br /&gt;
The compose preview screenshot tests cover portrait mode/landscape mode, light/dark mode (four variants).&lt;br /&gt;
&lt;br /&gt;
Limitations:&lt;br /&gt;
* Two tests within &amp;lt;code&amp;gt;feed&amp;lt;/code&amp;gt; module can only run in landscape mode (&amp;lt;code&amp;gt;FeedProfileScheduleScreenTest&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;FeedHistoryScreenTest&amp;lt;/code&amp;gt;) due to excessive heap memory utilisation.&lt;br /&gt;
* &amp;lt;code&amp;gt;Info&amp;lt;/code&amp;gt; screen is not tested due to &amp;lt;code&amp;gt;LibrariesContainer&amp;lt;/code&amp;gt; (dynamic content which cannot be mocked).&lt;br /&gt;
&lt;br /&gt;
The tests are located in &amp;lt;code&amp;gt;[module]/src/screenshotTest/kotlin/com/aquariumcontrol/[module]/&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
The references are located in &amp;lt;code&amp;gt;[module]/src/screenshotTestDebug/kotlin/com/aquariumcontrol/[module]/&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
The test reports are located in &amp;lt;code&amp;gt;build/reports/screenshotTest/preview/debug/&amp;lt;/code&amp;gt;. The test reports for the compose screenshot tests contain reference, actual and delta images.&lt;br /&gt;
&lt;br /&gt;
The images are generated with &amp;lt;code&amp;gt;./gradlew [module]:updateDebugScreenshotTest&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
The images are validated with &amp;lt;code&amp;gt;./gradlew [module]:validateDebugScreenshotTest&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Omitting the module will execute all tests.&lt;br /&gt;
&lt;br /&gt;
=== Instrumented tests ===&lt;br /&gt;
The tests run comparably long and are bound to an emulator device.&lt;br /&gt;
As of November 2025, a Pixel 5 API S is used for the instrumented test.&lt;br /&gt;
The instrumented tests contain&lt;br /&gt;
* screenshot testing for the complete screens (&amp;lt;code&amp;gt;ScreenNameTest&amp;lt;/code&amp;gt;)&lt;br /&gt;
* verification if the composable triggers the right event when tapping on buttons bound to navigation to another screen (&amp;lt;code&amp;gt;ScreenNameUITest&amp;lt;/code&amp;gt;)&lt;br /&gt;
&lt;br /&gt;
Limitations:&lt;br /&gt;
* &amp;lt;code&amp;gt;Info&amp;lt;/code&amp;gt; screen is not tested due to &amp;lt;code&amp;gt;LibrariesContainer&amp;lt;/code&amp;gt; (dynamic content which cannot be mocked and also does not allow execution of instrumented test).&lt;br /&gt;
&lt;br /&gt;
The test reports are located in &amp;lt;code&amp;gt;build/reports/androidTests/connected/debug/&amp;lt;/code&amp;gt;. The test reports for the compose screenshot tests do not contain any images.&lt;br /&gt;
&lt;br /&gt;
The tests are executed with &amp;lt;code&amp;gt;./gradlew [module]:connectedDebugAndroidTest&amp;lt;/code&amp;gt;.&lt;br /&gt;
Omitting the module will execute all tests.&lt;br /&gt;
&lt;br /&gt;
A switch called &amp;lt;code&amp;gt;recordMode&amp;lt;/code&amp;gt; inside each test class for screenshot testing determines if the image is generated &amp;lt;code&amp;gt;(true)&amp;lt;/code&amp;gt; or if the image is validated &amp;lt;code&amp;gt;(false)&amp;lt;/code&amp;gt;. The images are generated on the device &amp;lt;code&amp;gt;(/data/data/com.laimburggasse.aquariumcontrol.[module].test/files/screenshots_output/)&amp;lt;/code&amp;gt; and need to be downloaded to the repository.&lt;br /&gt;
&lt;br /&gt;
=== Unit tests ===&lt;br /&gt;
As of April 2026, unit tests are developed for:&lt;br /&gt;
* all view models&lt;br /&gt;
* all repositories&lt;br /&gt;
&lt;br /&gt;
The unit test cases for the view models make extensive use of the &amp;lt;code&amp;gt;MockK&amp;lt;/code&amp;gt; library.&lt;br /&gt;
&lt;br /&gt;
The unit test cases for the repositories use mock implementations of the &amp;lt;code&amp;gt;Retrofit&amp;lt;/code&amp;gt; interface.&lt;br /&gt;
&lt;br /&gt;
All unit test cases are executed using the command: &amp;lt;code&amp;gt;./gradlew :[module]:testDebugUnitTest&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The code coverage report can be created using: &amp;lt;code&amp;gt;./gradlew createDebugUnitTestCoverageReport&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The gradle task removes the injected code from the test coverage report: &amp;lt;code&amp;gt;./gradlew jacocoTestReport&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The report is stored in &amp;lt;code&amp;gt;[module]/build/reports/jacoco/jacocoTestReport/html&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Release procedure ==&lt;br /&gt;
# Update &amp;lt;code&amp;gt;versionCode&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;versionName&amp;lt;/code&amp;gt; in &amp;lt;code&amp;gt;build.gradle.kts&amp;lt;/code&amp;gt; of &amp;lt;code&amp;gt;app&amp;lt;/code&amp;gt; module.&lt;br /&gt;
# Create a release branch (&amp;lt;code&amp;gt;release/[version]&amp;lt;/code&amp;gt;), checkout, commit and push.&lt;br /&gt;
# Change to release build variant.&lt;br /&gt;
# Build&lt;br /&gt;
# Generate a signed App bundle&lt;br /&gt;
# Upload to play store&lt;/div&gt;</summary>
		<author><name>Uwe</name></author>
	</entry>
	<entry>
		<id>http://217.79.180.177/mediawiki/index.php?title=Android_application&amp;diff=455</id>
		<title>Android application</title>
		<link rel="alternate" type="text/html" href="http://217.79.180.177/mediawiki/index.php?title=Android_application&amp;diff=455"/>
		<updated>2026-04-26T06:44:03Z</updated>

		<summary type="html">&lt;p&gt;Uwe: /* Unit tests */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;The app is available in the [https://play.google.com/store/apps/details?id=com.laimburggasse.aquariumcontroller Play store].&lt;br /&gt;
== Requirements ==&lt;br /&gt;
== Architecture ==&lt;br /&gt;
The app is written completely in Kotlin using Jetpack Compose, Dagger/Hilt and aiming to implement a clean architecture.&lt;br /&gt;
=== Module structure ===&lt;br /&gt;
Besides the main app module, the app consists of the following library modules:&lt;br /&gt;
* balling&lt;br /&gt;
* common&lt;br /&gt;
* controller&lt;br /&gt;
* feed&lt;br /&gt;
* heating&lt;br /&gt;
* info&lt;br /&gt;
* overview&lt;br /&gt;
* refill&lt;br /&gt;
* schedule&lt;br /&gt;
* timedata&lt;br /&gt;
* ventilation&lt;br /&gt;
&lt;br /&gt;
The main &amp;lt;code&amp;gt;app&amp;lt;/code&amp;gt; module contains the AndroidManifest and the main activity (As of November 2025, the app only uses one activity).&lt;br /&gt;
The activity contains the navigation &amp;lt;code&amp;gt;NavHost&amp;lt;/code&amp;gt; in &amp;lt;code&amp;gt;onCreate&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
None of the other modules has a dependency to the main &amp;lt;code&amp;gt;app&amp;lt;/code&amp;gt; module.&lt;br /&gt;
&lt;br /&gt;
The &amp;lt;code&amp;gt;common&amp;lt;/code&amp;gt; module provides low-level functionalities and layout elements used in various places.&lt;br /&gt;
The following functionalities are located in &amp;lt;code&amp;gt;common&amp;lt;/code&amp;gt; module:&lt;br /&gt;
* &amp;lt;code&amp;gt;DataFetchResult&amp;lt;/code&amp;gt;: A wrapper class indicating the status of data fetching operation (&amp;lt;code&amp;gt;Success&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Error&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Loading&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Empty&amp;lt;/code&amp;gt;).&lt;br /&gt;
* &amp;lt;code&amp;gt;WebApiParams&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;WebApiPostRequest&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;WebApiPostRequestExecution&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;WebApiUrls&amp;lt;/code&amp;gt;: Classes and objects used for communication from the app to the server.&lt;br /&gt;
* &amp;lt;code&amp;gt;ControllerProfileDao&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;ControllerProfileDatabase&amp;lt;/code&amp;gt;: Classes which provide Room database functionality.&lt;br /&gt;
* Various classes used for dependency injection (Hilt)&lt;br /&gt;
* &amp;lt;code&amp;gt;SetValsSanityCheckResult&amp;lt;/code&amp;gt;: Class used for to communicate status of set values for &amp;lt;code&amp;gt;heating&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;ventilation&amp;lt;/code&amp;gt; module.&lt;br /&gt;
* &amp;lt;code&amp;gt;ControllerProfile&amp;lt;/code&amp;gt;: Class containing the profile data of the server (address, port, credentials, ...)&lt;br /&gt;
* &amp;lt;code&amp;gt;GlobalConstants&amp;lt;/code&amp;gt;: Mainly UI-related strings (non-context-related) and some minor functional constants&lt;br /&gt;
* Composable functions for the main drop down menu&lt;br /&gt;
* Composable functions for the theme&lt;br /&gt;
* Composable functions for hyperlinks, text edit fields, headline&lt;br /&gt;
* &amp;lt;code&amp;gt;ViewNavigationRoutes&amp;lt;/code&amp;gt;: Object containing the routing information processed in main &amp;lt;code&amp;gt;app&amp;lt;/code&amp;gt; module&lt;br /&gt;
* &amp;lt;code&amp;gt;ScreenshotTestHelper&amp;lt;/code&amp;gt;: Helper class for executing the instrumented snapshot testing using the emulator.&lt;br /&gt;
&lt;br /&gt;
=== Layer structure ===&lt;br /&gt;
The code is separated into the following layers:&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
[module]/&lt;br /&gt;
&lt;br /&gt;
├── src/main/kotlin/com/laimburggasse/aquariumcontrol/[module]/data/&lt;br /&gt;
&lt;br /&gt;
│   ├── remote/                   // Folder for data retrieval functionality&lt;br /&gt;
&lt;br /&gt;
│   │   └── dto/                  // Folder for Data Transfer Objects&lt;br /&gt;
&lt;br /&gt;
│   │       └── [module][name]Import.kt&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module][name]Reader.kt      // Implementation of GET server request&lt;br /&gt;
&lt;br /&gt;
│   ├── [module][name]RepositoryImpl.kt  // Repositories (implementation of interface)&lt;br /&gt;
&lt;br /&gt;
├── src/main/kotlin/com/laimburggasse/aquariumcontrol/[module]/domain/&lt;br /&gt;
&lt;br /&gt;
│   ├── model/                    // Folder for data definition classes&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module][name].kt.    // Classes for data definition&lt;br /&gt;
&lt;br /&gt;
│   ├── repository/               // Folder containing repository interface definitions&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module][name]Repository.kt.  // Interface defining the repository, used by view model&lt;br /&gt;
&lt;br /&gt;
├── src/main/kotlin/com/laimburggasse/aquariumcontrol/[module]/presentation/&lt;br /&gt;
&lt;br /&gt;
│   ├── entry/                    // First screen of feature&lt;br /&gt;
&lt;br /&gt;
│   ├── [components]/             // Shared code between different screens&lt;br /&gt;
&lt;br /&gt;
│   ├── [screen name]/            // Folder for each screen&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module][name]Screen.kt       // Composable showing the complete screen&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module][name]Composable.kt   // Composable showing elements inside the screen&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module][name]UiEvent.kt      // Sealed interface describing the communication from the composable to the view model&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module][name]UiState.kt      // Data class describing the content to be rendered by the composable&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module][name]ViewModel.kt    // Class implementing the view model&lt;br /&gt;
&lt;br /&gt;
│   ├── navigation/               // Folder containing class and functionality related to screen navigation&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module]NavigationEvent.kt  // One sealed class per module containing the navigation intent of the user&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module]Navigator.kt  // Module-specific navigation functionality&lt;br /&gt;
&lt;br /&gt;
│&lt;br /&gt;
└── build.gradle                  // Plugins, setting, dependencies&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Screen navigation ===&lt;br /&gt;
The screen navigation is controlled by a &amp;lt;code&amp;gt;NavHostController&amp;lt;/code&amp;gt; which is created in &amp;lt;code&amp;gt;MainActivity&amp;lt;/code&amp;gt; and remembered using &amp;lt;code&amp;gt;rememberNavController&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
==== Modular navigation ====&lt;br /&gt;
Each module adds its own navigation function to &amp;lt;code&amp;gt;NavHostController&amp;lt;/code&amp;gt;.&lt;br /&gt;
These extension functions are stored in &amp;lt;code&amp;gt;[module]/presentation/navigation/[module]Navigator.kt&amp;lt;/code&amp;gt;.&lt;br /&gt;
They encapsulate the routing logic for that specific module.&lt;br /&gt;
&lt;br /&gt;
==== Event-Driven Navigation ====&lt;br /&gt;
The functions receive as parameter a specific sealed class &amp;lt;code&amp;gt;[module]NavigationEvent&amp;lt;/code&amp;gt;.&lt;br /&gt;
These sealed classes contain objects for each navigation event inside the module.&lt;br /&gt;
&lt;br /&gt;
==== Implementation pattern ====&lt;br /&gt;
The navigation functions are used in the following locations:&lt;br /&gt;
* inside the lambda function passed to &amp;lt;code&amp;gt;NavHost&amp;lt;/code&amp;gt; in &amp;lt;code&amp;gt;MainActivity&amp;lt;/code&amp;gt; for each screen.&lt;br /&gt;
* inside composables themselves being passed as lambda function from within &amp;lt;code&amp;gt;MainActivity&amp;lt;/code&amp;gt;&lt;br /&gt;
:* The composable remain unaware of &amp;lt;code&amp;gt;NavHostController&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Routes and arguments ====&lt;br /&gt;
Source and destination of the routing is defined using strings from the object &amp;lt;code&amp;gt;ViewNavigationRoutes&amp;lt;/code&amp;gt; where data that needs to be transferred between screens is embedded in the strings.&lt;br /&gt;
&lt;br /&gt;
== Example workflow for adding API communication ==&lt;br /&gt;
=== Implementation ===&lt;br /&gt;
Create the model: &amp;lt;code&amp;gt;RefillState&amp;lt;/code&amp;gt; as &amp;lt;code&amp;gt;enum&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Add a reader &amp;lt;code&amp;gt;class&amp;lt;/code&amp;gt; &amp;lt;code&amp;gt;RefillStateReader&amp;lt;/code&amp;gt; which uses the injected interface (Retrofit)&lt;br /&gt;
&lt;br /&gt;
Update the &amp;lt;code&amp;gt;RefillStateApiService&amp;lt;/code&amp;gt;: Add a function to load the state via Retrofit&lt;br /&gt;
&lt;br /&gt;
Add the label for the JSON property in &amp;lt;code&amp;gt;WebApiParams&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Add the URL for the end point in &amp;lt;code&amp;gt;WebApiUrls&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Add a wrapper &amp;lt;code&amp;gt;RefillStateResponse&amp;lt;/code&amp;gt; for the model to allow Retrofit parsing the JSON&lt;br /&gt;
&lt;br /&gt;
Update the repository interface and the repository interface implementation:&lt;br /&gt;
* Add a flow that delivers a &amp;lt;code&amp;gt;DataFetchResult&amp;lt;/code&amp;gt; of the &amp;lt;code&amp;gt;RefillState&amp;lt;/code&amp;gt;&lt;br /&gt;
* Add a function that allows the view model to trigger a refresh&lt;br /&gt;
&lt;br /&gt;
Add a wrapper &amp;lt;code&amp;gt;class&amp;lt;/code&amp;gt; &amp;lt;code&amp;gt;RefillStateUiState&amp;lt;/code&amp;gt;&lt;br /&gt;
Update the view model&lt;br /&gt;
* Add a flow that delivers a &amp;lt;code&amp;gt;RefillStateUiState&amp;lt;/code&amp;gt; based on the &amp;lt;code&amp;gt;DataFetchResult&amp;lt;/code&amp;gt; from repository&lt;br /&gt;
* Handle the new UI event for refreshing&lt;br /&gt;
&lt;br /&gt;
Add composable &amp;lt;code&amp;gt;RefillStateComposable&amp;lt;/code&amp;gt; for displaying the &amp;lt;code&amp;gt;RefillState&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Update &amp;lt;code&amp;gt;RefillUiEvent&amp;lt;/code&amp;gt; with an object for the refresh&lt;br /&gt;
&lt;br /&gt;
Update &amp;lt;code&amp;gt;RefillScreen&amp;lt;/code&amp;gt;:&lt;br /&gt;
* additional parameter (&amp;lt;code&amp;gt;RefillStateUiState&amp;lt;/code&amp;gt;) in composable function and in previews&lt;br /&gt;
* Added &amp;lt;code&amp;gt;PullToRefreshBox&amp;lt;/code&amp;gt;&lt;br /&gt;
* Added parsing of &amp;lt;code&amp;gt;RefillUiState&amp;lt;/code&amp;gt; and display of either&lt;br /&gt;
:* &amp;lt;code&amp;gt;RefillStateComposable&amp;lt;/code&amp;gt; in case of success&lt;br /&gt;
:* Text-based error message in case of error&lt;br /&gt;
:* &amp;lt;code&amp;gt;CircularProgressIndicator&amp;lt;/code&amp;gt; in case of loading&lt;br /&gt;
&lt;br /&gt;
Inject &amp;lt;code&amp;gt;RefillUiState&amp;lt;/code&amp;gt; into &amp;lt;code&amp;gt;RefillScreen&amp;lt;/code&amp;gt; in the &amp;lt;code&amp;gt;MainActivity&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;RefillScreenTest&amp;lt;/code&amp;gt; as well as in previews of &amp;lt;code&amp;gt;RefillScreen&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
(Optional) Check if &amp;lt;code&amp;gt;RefillDataModule&amp;lt;/code&amp;gt; needs update (not required in this case)&lt;br /&gt;
&lt;br /&gt;
=== Testing ===&lt;br /&gt;
Update existing screenshot testing (compose screenshot testing and instrumented tests)&lt;br /&gt;
&lt;br /&gt;
Add compose screenshot testing for new composable&lt;br /&gt;
&lt;br /&gt;
== Test strategy ==&lt;br /&gt;
The app uses different testing approaches:&lt;br /&gt;
* Compose preview screenshot testing&lt;br /&gt;
* Screenshot tests running as instrumented tests on emulator&lt;br /&gt;
* Unit tests&lt;br /&gt;
&lt;br /&gt;
=== Compose preview screenshot tests ===&lt;br /&gt;
These tests run comparably fast and device-independent.&lt;br /&gt;
&lt;br /&gt;
The compose preview screenshot tests cover portrait mode/landscape mode, light/dark mode (four variants).&lt;br /&gt;
&lt;br /&gt;
Limitations:&lt;br /&gt;
* Two tests within &amp;lt;code&amp;gt;feed&amp;lt;/code&amp;gt; module can only run in landscape mode (&amp;lt;code&amp;gt;FeedProfileScheduleScreenTest&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;FeedHistoryScreenTest&amp;lt;/code&amp;gt;) due to excessive heap memory utilisation.&lt;br /&gt;
* &amp;lt;code&amp;gt;Info&amp;lt;/code&amp;gt; screen is not tested due to &amp;lt;code&amp;gt;LibrariesContainer&amp;lt;/code&amp;gt; (dynamic content which cannot be mocked).&lt;br /&gt;
&lt;br /&gt;
The tests are located in &amp;lt;code&amp;gt;[module]/src/screenshotTest/kotlin/com/aquariumcontrol/[module]/&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
The references are located in &amp;lt;code&amp;gt;[module]/src/screenshotTestDebug/kotlin/com/aquariumcontrol/[module]/&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
The test reports are located in &amp;lt;code&amp;gt;build/reports/screenshotTest/preview/debug/&amp;lt;/code&amp;gt;. The test reports for the compose screenshot tests contain reference, actual and delta images.&lt;br /&gt;
&lt;br /&gt;
The images are generated with &amp;lt;code&amp;gt;./gradlew [module]:updateDebugScreenshotTest&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
The images are validated with &amp;lt;code&amp;gt;./gradlew [module]:validateDebugScreenshotTest&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Omitting the module will execute all tests.&lt;br /&gt;
&lt;br /&gt;
=== Instrumented tests ===&lt;br /&gt;
The tests run comparably long and are bound to an emulator device.&lt;br /&gt;
As of November 2025, a Pixel 5 API S is used for the instrumented test.&lt;br /&gt;
The instrumented tests contain&lt;br /&gt;
* screenshot testing for the complete screens (&amp;lt;code&amp;gt;ScreenNameTest&amp;lt;/code&amp;gt;)&lt;br /&gt;
* verification if the composable triggers the right event when tapping on buttons bound to navigation to another screen (&amp;lt;code&amp;gt;ScreenNameUITest&amp;lt;/code&amp;gt;)&lt;br /&gt;
&lt;br /&gt;
Limitations:&lt;br /&gt;
* &amp;lt;code&amp;gt;Info&amp;lt;/code&amp;gt; screen is not tested due to &amp;lt;code&amp;gt;LibrariesContainer&amp;lt;/code&amp;gt; (dynamic content which cannot be mocked and also does not allow execution of instrumented test).&lt;br /&gt;
&lt;br /&gt;
The test reports are located in &amp;lt;code&amp;gt;build/reports/androidTests/connected/debug/&amp;lt;/code&amp;gt;. The test reports for the compose screenshot tests do not contain any images.&lt;br /&gt;
&lt;br /&gt;
The tests are executed with &amp;lt;code&amp;gt;./gradlew [module]:connectedDebugAndroidTest&amp;lt;/code&amp;gt;.&lt;br /&gt;
Omitting the module will execute all tests.&lt;br /&gt;
&lt;br /&gt;
A switch called &amp;lt;code&amp;gt;recordMode&amp;lt;/code&amp;gt; inside each test class for screenshot testing determines if the image is generated &amp;lt;code&amp;gt;(true)&amp;lt;/code&amp;gt; or if the image is validated &amp;lt;code&amp;gt;(false)&amp;lt;/code&amp;gt;. The images are generated on the device &amp;lt;code&amp;gt;(/data/data/com.laimburggasse.aquariumcontrol.[module].test/files/screenshots_output/)&amp;lt;/code&amp;gt; and need to be downloaded to the repository.&lt;br /&gt;
&lt;br /&gt;
=== Unit tests ===&lt;br /&gt;
As of April 2026, unit tests are developed for:&lt;br /&gt;
* all view models&lt;br /&gt;
* all repositories&lt;br /&gt;
&lt;br /&gt;
The unit test cases for the view models make extensive use of the &amp;lt;code&amp;gt;MockK&amp;lt;/code&amp;gt; library.&lt;br /&gt;
&lt;br /&gt;
The unit test cases for the repositories use mock implementations of the &amp;lt;code&amp;gt;Retrofit&amp;lt;/code&amp;gt; interface.&lt;br /&gt;
&lt;br /&gt;
All unit test cases are executed using the command: &amp;lt;code&amp;gt;./gradlew :[module]:testDebugUnitTest&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The code coverage report can be created using: &amp;lt;code&amp;gt;./gradlew createDebugUnitTestCoverageReport&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The gradle task removes the injected code from the test coverage report: &amp;lt;code&amp;gt;./gradlew jacocoTestReport&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Release procedure ==&lt;br /&gt;
# Update &amp;lt;code&amp;gt;versionCode&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;versionName&amp;lt;/code&amp;gt; in &amp;lt;code&amp;gt;build.gradle.kts&amp;lt;/code&amp;gt; of &amp;lt;code&amp;gt;app&amp;lt;/code&amp;gt; module.&lt;br /&gt;
# Create a release branch (&amp;lt;code&amp;gt;release/[version]&amp;lt;/code&amp;gt;), checkout, commit and push.&lt;br /&gt;
# Change to release build variant.&lt;br /&gt;
# Build&lt;br /&gt;
# Generate a signed App bundle&lt;br /&gt;
# Upload to play store&lt;/div&gt;</summary>
		<author><name>Uwe</name></author>
	</entry>
	<entry>
		<id>http://217.79.180.177/mediawiki/index.php?title=Android_application&amp;diff=454</id>
		<title>Android application</title>
		<link rel="alternate" type="text/html" href="http://217.79.180.177/mediawiki/index.php?title=Android_application&amp;diff=454"/>
		<updated>2026-04-25T08:45:29Z</updated>

		<summary type="html">&lt;p&gt;Uwe: /* Unit tests */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;The app is available in the [https://play.google.com/store/apps/details?id=com.laimburggasse.aquariumcontroller Play store].&lt;br /&gt;
== Requirements ==&lt;br /&gt;
== Architecture ==&lt;br /&gt;
The app is written completely in Kotlin using Jetpack Compose, Dagger/Hilt and aiming to implement a clean architecture.&lt;br /&gt;
=== Module structure ===&lt;br /&gt;
Besides the main app module, the app consists of the following library modules:&lt;br /&gt;
* balling&lt;br /&gt;
* common&lt;br /&gt;
* controller&lt;br /&gt;
* feed&lt;br /&gt;
* heating&lt;br /&gt;
* info&lt;br /&gt;
* overview&lt;br /&gt;
* refill&lt;br /&gt;
* schedule&lt;br /&gt;
* timedata&lt;br /&gt;
* ventilation&lt;br /&gt;
&lt;br /&gt;
The main &amp;lt;code&amp;gt;app&amp;lt;/code&amp;gt; module contains the AndroidManifest and the main activity (As of November 2025, the app only uses one activity).&lt;br /&gt;
The activity contains the navigation &amp;lt;code&amp;gt;NavHost&amp;lt;/code&amp;gt; in &amp;lt;code&amp;gt;onCreate&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
None of the other modules has a dependency to the main &amp;lt;code&amp;gt;app&amp;lt;/code&amp;gt; module.&lt;br /&gt;
&lt;br /&gt;
The &amp;lt;code&amp;gt;common&amp;lt;/code&amp;gt; module provides low-level functionalities and layout elements used in various places.&lt;br /&gt;
The following functionalities are located in &amp;lt;code&amp;gt;common&amp;lt;/code&amp;gt; module:&lt;br /&gt;
* &amp;lt;code&amp;gt;DataFetchResult&amp;lt;/code&amp;gt;: A wrapper class indicating the status of data fetching operation (&amp;lt;code&amp;gt;Success&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Error&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Loading&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Empty&amp;lt;/code&amp;gt;).&lt;br /&gt;
* &amp;lt;code&amp;gt;WebApiParams&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;WebApiPostRequest&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;WebApiPostRequestExecution&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;WebApiUrls&amp;lt;/code&amp;gt;: Classes and objects used for communication from the app to the server.&lt;br /&gt;
* &amp;lt;code&amp;gt;ControllerProfileDao&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;ControllerProfileDatabase&amp;lt;/code&amp;gt;: Classes which provide Room database functionality.&lt;br /&gt;
* Various classes used for dependency injection (Hilt)&lt;br /&gt;
* &amp;lt;code&amp;gt;SetValsSanityCheckResult&amp;lt;/code&amp;gt;: Class used for to communicate status of set values for &amp;lt;code&amp;gt;heating&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;ventilation&amp;lt;/code&amp;gt; module.&lt;br /&gt;
* &amp;lt;code&amp;gt;ControllerProfile&amp;lt;/code&amp;gt;: Class containing the profile data of the server (address, port, credentials, ...)&lt;br /&gt;
* &amp;lt;code&amp;gt;GlobalConstants&amp;lt;/code&amp;gt;: Mainly UI-related strings (non-context-related) and some minor functional constants&lt;br /&gt;
* Composable functions for the main drop down menu&lt;br /&gt;
* Composable functions for the theme&lt;br /&gt;
* Composable functions for hyperlinks, text edit fields, headline&lt;br /&gt;
* &amp;lt;code&amp;gt;ViewNavigationRoutes&amp;lt;/code&amp;gt;: Object containing the routing information processed in main &amp;lt;code&amp;gt;app&amp;lt;/code&amp;gt; module&lt;br /&gt;
* &amp;lt;code&amp;gt;ScreenshotTestHelper&amp;lt;/code&amp;gt;: Helper class for executing the instrumented snapshot testing using the emulator.&lt;br /&gt;
&lt;br /&gt;
=== Layer structure ===&lt;br /&gt;
The code is separated into the following layers:&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
[module]/&lt;br /&gt;
&lt;br /&gt;
├── src/main/kotlin/com/laimburggasse/aquariumcontrol/[module]/data/&lt;br /&gt;
&lt;br /&gt;
│   ├── remote/                   // Folder for data retrieval functionality&lt;br /&gt;
&lt;br /&gt;
│   │   └── dto/                  // Folder for Data Transfer Objects&lt;br /&gt;
&lt;br /&gt;
│   │       └── [module][name]Import.kt&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module][name]Reader.kt      // Implementation of GET server request&lt;br /&gt;
&lt;br /&gt;
│   ├── [module][name]RepositoryImpl.kt  // Repositories (implementation of interface)&lt;br /&gt;
&lt;br /&gt;
├── src/main/kotlin/com/laimburggasse/aquariumcontrol/[module]/domain/&lt;br /&gt;
&lt;br /&gt;
│   ├── model/                    // Folder for data definition classes&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module][name].kt.    // Classes for data definition&lt;br /&gt;
&lt;br /&gt;
│   ├── repository/               // Folder containing repository interface definitions&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module][name]Repository.kt.  // Interface defining the repository, used by view model&lt;br /&gt;
&lt;br /&gt;
├── src/main/kotlin/com/laimburggasse/aquariumcontrol/[module]/presentation/&lt;br /&gt;
&lt;br /&gt;
│   ├── entry/                    // First screen of feature&lt;br /&gt;
&lt;br /&gt;
│   ├── [components]/             // Shared code between different screens&lt;br /&gt;
&lt;br /&gt;
│   ├── [screen name]/            // Folder for each screen&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module][name]Screen.kt       // Composable showing the complete screen&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module][name]Composable.kt   // Composable showing elements inside the screen&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module][name]UiEvent.kt      // Sealed interface describing the communication from the composable to the view model&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module][name]UiState.kt      // Data class describing the content to be rendered by the composable&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module][name]ViewModel.kt    // Class implementing the view model&lt;br /&gt;
&lt;br /&gt;
│   ├── navigation/               // Folder containing class and functionality related to screen navigation&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module]NavigationEvent.kt  // One sealed class per module containing the navigation intent of the user&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module]Navigator.kt  // Module-specific navigation functionality&lt;br /&gt;
&lt;br /&gt;
│&lt;br /&gt;
└── build.gradle                  // Plugins, setting, dependencies&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Screen navigation ===&lt;br /&gt;
The screen navigation is controlled by a &amp;lt;code&amp;gt;NavHostController&amp;lt;/code&amp;gt; which is created in &amp;lt;code&amp;gt;MainActivity&amp;lt;/code&amp;gt; and remembered using &amp;lt;code&amp;gt;rememberNavController&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
==== Modular navigation ====&lt;br /&gt;
Each module adds its own navigation function to &amp;lt;code&amp;gt;NavHostController&amp;lt;/code&amp;gt;.&lt;br /&gt;
These extension functions are stored in &amp;lt;code&amp;gt;[module]/presentation/navigation/[module]Navigator.kt&amp;lt;/code&amp;gt;.&lt;br /&gt;
They encapsulate the routing logic for that specific module.&lt;br /&gt;
&lt;br /&gt;
==== Event-Driven Navigation ====&lt;br /&gt;
The functions receive as parameter a specific sealed class &amp;lt;code&amp;gt;[module]NavigationEvent&amp;lt;/code&amp;gt;.&lt;br /&gt;
These sealed classes contain objects for each navigation event inside the module.&lt;br /&gt;
&lt;br /&gt;
==== Implementation pattern ====&lt;br /&gt;
The navigation functions are used in the following locations:&lt;br /&gt;
* inside the lambda function passed to &amp;lt;code&amp;gt;NavHost&amp;lt;/code&amp;gt; in &amp;lt;code&amp;gt;MainActivity&amp;lt;/code&amp;gt; for each screen.&lt;br /&gt;
* inside composables themselves being passed as lambda function from within &amp;lt;code&amp;gt;MainActivity&amp;lt;/code&amp;gt;&lt;br /&gt;
:* The composable remain unaware of &amp;lt;code&amp;gt;NavHostController&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Routes and arguments ====&lt;br /&gt;
Source and destination of the routing is defined using strings from the object &amp;lt;code&amp;gt;ViewNavigationRoutes&amp;lt;/code&amp;gt; where data that needs to be transferred between screens is embedded in the strings.&lt;br /&gt;
&lt;br /&gt;
== Example workflow for adding API communication ==&lt;br /&gt;
=== Implementation ===&lt;br /&gt;
Create the model: &amp;lt;code&amp;gt;RefillState&amp;lt;/code&amp;gt; as &amp;lt;code&amp;gt;enum&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Add a reader &amp;lt;code&amp;gt;class&amp;lt;/code&amp;gt; &amp;lt;code&amp;gt;RefillStateReader&amp;lt;/code&amp;gt; which uses the injected interface (Retrofit)&lt;br /&gt;
&lt;br /&gt;
Update the &amp;lt;code&amp;gt;RefillStateApiService&amp;lt;/code&amp;gt;: Add a function to load the state via Retrofit&lt;br /&gt;
&lt;br /&gt;
Add the label for the JSON property in &amp;lt;code&amp;gt;WebApiParams&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Add the URL for the end point in &amp;lt;code&amp;gt;WebApiUrls&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Add a wrapper &amp;lt;code&amp;gt;RefillStateResponse&amp;lt;/code&amp;gt; for the model to allow Retrofit parsing the JSON&lt;br /&gt;
&lt;br /&gt;
Update the repository interface and the repository interface implementation:&lt;br /&gt;
* Add a flow that delivers a &amp;lt;code&amp;gt;DataFetchResult&amp;lt;/code&amp;gt; of the &amp;lt;code&amp;gt;RefillState&amp;lt;/code&amp;gt;&lt;br /&gt;
* Add a function that allows the view model to trigger a refresh&lt;br /&gt;
&lt;br /&gt;
Add a wrapper &amp;lt;code&amp;gt;class&amp;lt;/code&amp;gt; &amp;lt;code&amp;gt;RefillStateUiState&amp;lt;/code&amp;gt;&lt;br /&gt;
Update the view model&lt;br /&gt;
* Add a flow that delivers a &amp;lt;code&amp;gt;RefillStateUiState&amp;lt;/code&amp;gt; based on the &amp;lt;code&amp;gt;DataFetchResult&amp;lt;/code&amp;gt; from repository&lt;br /&gt;
* Handle the new UI event for refreshing&lt;br /&gt;
&lt;br /&gt;
Add composable &amp;lt;code&amp;gt;RefillStateComposable&amp;lt;/code&amp;gt; for displaying the &amp;lt;code&amp;gt;RefillState&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Update &amp;lt;code&amp;gt;RefillUiEvent&amp;lt;/code&amp;gt; with an object for the refresh&lt;br /&gt;
&lt;br /&gt;
Update &amp;lt;code&amp;gt;RefillScreen&amp;lt;/code&amp;gt;:&lt;br /&gt;
* additional parameter (&amp;lt;code&amp;gt;RefillStateUiState&amp;lt;/code&amp;gt;) in composable function and in previews&lt;br /&gt;
* Added &amp;lt;code&amp;gt;PullToRefreshBox&amp;lt;/code&amp;gt;&lt;br /&gt;
* Added parsing of &amp;lt;code&amp;gt;RefillUiState&amp;lt;/code&amp;gt; and display of either&lt;br /&gt;
:* &amp;lt;code&amp;gt;RefillStateComposable&amp;lt;/code&amp;gt; in case of success&lt;br /&gt;
:* Text-based error message in case of error&lt;br /&gt;
:* &amp;lt;code&amp;gt;CircularProgressIndicator&amp;lt;/code&amp;gt; in case of loading&lt;br /&gt;
&lt;br /&gt;
Inject &amp;lt;code&amp;gt;RefillUiState&amp;lt;/code&amp;gt; into &amp;lt;code&amp;gt;RefillScreen&amp;lt;/code&amp;gt; in the &amp;lt;code&amp;gt;MainActivity&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;RefillScreenTest&amp;lt;/code&amp;gt; as well as in previews of &amp;lt;code&amp;gt;RefillScreen&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
(Optional) Check if &amp;lt;code&amp;gt;RefillDataModule&amp;lt;/code&amp;gt; needs update (not required in this case)&lt;br /&gt;
&lt;br /&gt;
=== Testing ===&lt;br /&gt;
Update existing screenshot testing (compose screenshot testing and instrumented tests)&lt;br /&gt;
&lt;br /&gt;
Add compose screenshot testing for new composable&lt;br /&gt;
&lt;br /&gt;
== Test strategy ==&lt;br /&gt;
The app uses different testing approaches:&lt;br /&gt;
* Compose preview screenshot testing&lt;br /&gt;
* Screenshot tests running as instrumented tests on emulator&lt;br /&gt;
* Unit tests&lt;br /&gt;
&lt;br /&gt;
=== Compose preview screenshot tests ===&lt;br /&gt;
These tests run comparably fast and device-independent.&lt;br /&gt;
&lt;br /&gt;
The compose preview screenshot tests cover portrait mode/landscape mode, light/dark mode (four variants).&lt;br /&gt;
&lt;br /&gt;
Limitations:&lt;br /&gt;
* Two tests within &amp;lt;code&amp;gt;feed&amp;lt;/code&amp;gt; module can only run in landscape mode (&amp;lt;code&amp;gt;FeedProfileScheduleScreenTest&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;FeedHistoryScreenTest&amp;lt;/code&amp;gt;) due to excessive heap memory utilisation.&lt;br /&gt;
* &amp;lt;code&amp;gt;Info&amp;lt;/code&amp;gt; screen is not tested due to &amp;lt;code&amp;gt;LibrariesContainer&amp;lt;/code&amp;gt; (dynamic content which cannot be mocked).&lt;br /&gt;
&lt;br /&gt;
The tests are located in &amp;lt;code&amp;gt;[module]/src/screenshotTest/kotlin/com/aquariumcontrol/[module]/&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
The references are located in &amp;lt;code&amp;gt;[module]/src/screenshotTestDebug/kotlin/com/aquariumcontrol/[module]/&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
The test reports are located in &amp;lt;code&amp;gt;build/reports/screenshotTest/preview/debug/&amp;lt;/code&amp;gt;. The test reports for the compose screenshot tests contain reference, actual and delta images.&lt;br /&gt;
&lt;br /&gt;
The images are generated with &amp;lt;code&amp;gt;./gradlew [module]:updateDebugScreenshotTest&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
The images are validated with &amp;lt;code&amp;gt;./gradlew [module]:validateDebugScreenshotTest&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Omitting the module will execute all tests.&lt;br /&gt;
&lt;br /&gt;
=== Instrumented tests ===&lt;br /&gt;
The tests run comparably long and are bound to an emulator device.&lt;br /&gt;
As of November 2025, a Pixel 5 API S is used for the instrumented test.&lt;br /&gt;
The instrumented tests contain&lt;br /&gt;
* screenshot testing for the complete screens (&amp;lt;code&amp;gt;ScreenNameTest&amp;lt;/code&amp;gt;)&lt;br /&gt;
* verification if the composable triggers the right event when tapping on buttons bound to navigation to another screen (&amp;lt;code&amp;gt;ScreenNameUITest&amp;lt;/code&amp;gt;)&lt;br /&gt;
&lt;br /&gt;
Limitations:&lt;br /&gt;
* &amp;lt;code&amp;gt;Info&amp;lt;/code&amp;gt; screen is not tested due to &amp;lt;code&amp;gt;LibrariesContainer&amp;lt;/code&amp;gt; (dynamic content which cannot be mocked and also does not allow execution of instrumented test).&lt;br /&gt;
&lt;br /&gt;
The test reports are located in &amp;lt;code&amp;gt;build/reports/androidTests/connected/debug/&amp;lt;/code&amp;gt;. The test reports for the compose screenshot tests do not contain any images.&lt;br /&gt;
&lt;br /&gt;
The tests are executed with &amp;lt;code&amp;gt;./gradlew [module]:connectedDebugAndroidTest&amp;lt;/code&amp;gt;.&lt;br /&gt;
Omitting the module will execute all tests.&lt;br /&gt;
&lt;br /&gt;
A switch called &amp;lt;code&amp;gt;recordMode&amp;lt;/code&amp;gt; inside each test class for screenshot testing determines if the image is generated &amp;lt;code&amp;gt;(true)&amp;lt;/code&amp;gt; or if the image is validated &amp;lt;code&amp;gt;(false)&amp;lt;/code&amp;gt;. The images are generated on the device &amp;lt;code&amp;gt;(/data/data/com.laimburggasse.aquariumcontrol.[module].test/files/screenshots_output/)&amp;lt;/code&amp;gt; and need to be downloaded to the repository.&lt;br /&gt;
&lt;br /&gt;
=== Unit tests ===&lt;br /&gt;
As of April 2026, unit tests are being developed for:&lt;br /&gt;
* all view models (finished)&lt;br /&gt;
* all repositories&lt;br /&gt;
&lt;br /&gt;
The unit test cases for the view models make extensive use of the &amp;lt;code&amp;gt;MockK&amp;lt;/code&amp;gt; library.&lt;br /&gt;
&lt;br /&gt;
The unit test cases for the repositories use mock implementations of the &amp;lt;code&amp;gt;Retrofit&amp;lt;/code&amp;gt; interface.&lt;br /&gt;
&lt;br /&gt;
All test cases are executed using the command: &amp;lt;code&amp;gt;./gradlew test&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The code coverage report can be created using: &amp;lt;code&amp;gt;./gradlew createDebugUnitTestCoverageReport&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The gradle task removes the injected code from the test coverage report: &amp;lt;code&amp;gt;./gradlew jacocoTestReport&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Release procedure ==&lt;br /&gt;
# Update &amp;lt;code&amp;gt;versionCode&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;versionName&amp;lt;/code&amp;gt; in &amp;lt;code&amp;gt;build.gradle.kts&amp;lt;/code&amp;gt; of &amp;lt;code&amp;gt;app&amp;lt;/code&amp;gt; module.&lt;br /&gt;
# Create a release branch (&amp;lt;code&amp;gt;release/[version]&amp;lt;/code&amp;gt;), checkout, commit and push.&lt;br /&gt;
# Change to release build variant.&lt;br /&gt;
# Build&lt;br /&gt;
# Generate a signed App bundle&lt;br /&gt;
# Upload to play store&lt;/div&gt;</summary>
		<author><name>Uwe</name></author>
	</entry>
	<entry>
		<id>http://217.79.180.177/mediawiki/index.php?title=Android_application&amp;diff=453</id>
		<title>Android application</title>
		<link rel="alternate" type="text/html" href="http://217.79.180.177/mediawiki/index.php?title=Android_application&amp;diff=453"/>
		<updated>2026-04-18T13:44:12Z</updated>

		<summary type="html">&lt;p&gt;Uwe: /* Testing */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;The app is available in the [https://play.google.com/store/apps/details?id=com.laimburggasse.aquariumcontroller Play store].&lt;br /&gt;
== Requirements ==&lt;br /&gt;
== Architecture ==&lt;br /&gt;
The app is written completely in Kotlin using Jetpack Compose, Dagger/Hilt and aiming to implement a clean architecture.&lt;br /&gt;
=== Module structure ===&lt;br /&gt;
Besides the main app module, the app consists of the following library modules:&lt;br /&gt;
* balling&lt;br /&gt;
* common&lt;br /&gt;
* controller&lt;br /&gt;
* feed&lt;br /&gt;
* heating&lt;br /&gt;
* info&lt;br /&gt;
* overview&lt;br /&gt;
* refill&lt;br /&gt;
* schedule&lt;br /&gt;
* timedata&lt;br /&gt;
* ventilation&lt;br /&gt;
&lt;br /&gt;
The main &amp;lt;code&amp;gt;app&amp;lt;/code&amp;gt; module contains the AndroidManifest and the main activity (As of November 2025, the app only uses one activity).&lt;br /&gt;
The activity contains the navigation &amp;lt;code&amp;gt;NavHost&amp;lt;/code&amp;gt; in &amp;lt;code&amp;gt;onCreate&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
None of the other modules has a dependency to the main &amp;lt;code&amp;gt;app&amp;lt;/code&amp;gt; module.&lt;br /&gt;
&lt;br /&gt;
The &amp;lt;code&amp;gt;common&amp;lt;/code&amp;gt; module provides low-level functionalities and layout elements used in various places.&lt;br /&gt;
The following functionalities are located in &amp;lt;code&amp;gt;common&amp;lt;/code&amp;gt; module:&lt;br /&gt;
* &amp;lt;code&amp;gt;DataFetchResult&amp;lt;/code&amp;gt;: A wrapper class indicating the status of data fetching operation (&amp;lt;code&amp;gt;Success&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Error&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Loading&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Empty&amp;lt;/code&amp;gt;).&lt;br /&gt;
* &amp;lt;code&amp;gt;WebApiParams&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;WebApiPostRequest&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;WebApiPostRequestExecution&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;WebApiUrls&amp;lt;/code&amp;gt;: Classes and objects used for communication from the app to the server.&lt;br /&gt;
* &amp;lt;code&amp;gt;ControllerProfileDao&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;ControllerProfileDatabase&amp;lt;/code&amp;gt;: Classes which provide Room database functionality.&lt;br /&gt;
* Various classes used for dependency injection (Hilt)&lt;br /&gt;
* &amp;lt;code&amp;gt;SetValsSanityCheckResult&amp;lt;/code&amp;gt;: Class used for to communicate status of set values for &amp;lt;code&amp;gt;heating&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;ventilation&amp;lt;/code&amp;gt; module.&lt;br /&gt;
* &amp;lt;code&amp;gt;ControllerProfile&amp;lt;/code&amp;gt;: Class containing the profile data of the server (address, port, credentials, ...)&lt;br /&gt;
* &amp;lt;code&amp;gt;GlobalConstants&amp;lt;/code&amp;gt;: Mainly UI-related strings (non-context-related) and some minor functional constants&lt;br /&gt;
* Composable functions for the main drop down menu&lt;br /&gt;
* Composable functions for the theme&lt;br /&gt;
* Composable functions for hyperlinks, text edit fields, headline&lt;br /&gt;
* &amp;lt;code&amp;gt;ViewNavigationRoutes&amp;lt;/code&amp;gt;: Object containing the routing information processed in main &amp;lt;code&amp;gt;app&amp;lt;/code&amp;gt; module&lt;br /&gt;
* &amp;lt;code&amp;gt;ScreenshotTestHelper&amp;lt;/code&amp;gt;: Helper class for executing the instrumented snapshot testing using the emulator.&lt;br /&gt;
&lt;br /&gt;
=== Layer structure ===&lt;br /&gt;
The code is separated into the following layers:&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
[module]/&lt;br /&gt;
&lt;br /&gt;
├── src/main/kotlin/com/laimburggasse/aquariumcontrol/[module]/data/&lt;br /&gt;
&lt;br /&gt;
│   ├── remote/                   // Folder for data retrieval functionality&lt;br /&gt;
&lt;br /&gt;
│   │   └── dto/                  // Folder for Data Transfer Objects&lt;br /&gt;
&lt;br /&gt;
│   │       └── [module][name]Import.kt&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module][name]Reader.kt      // Implementation of GET server request&lt;br /&gt;
&lt;br /&gt;
│   ├── [module][name]RepositoryImpl.kt  // Repositories (implementation of interface)&lt;br /&gt;
&lt;br /&gt;
├── src/main/kotlin/com/laimburggasse/aquariumcontrol/[module]/domain/&lt;br /&gt;
&lt;br /&gt;
│   ├── model/                    // Folder for data definition classes&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module][name].kt.    // Classes for data definition&lt;br /&gt;
&lt;br /&gt;
│   ├── repository/               // Folder containing repository interface definitions&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module][name]Repository.kt.  // Interface defining the repository, used by view model&lt;br /&gt;
&lt;br /&gt;
├── src/main/kotlin/com/laimburggasse/aquariumcontrol/[module]/presentation/&lt;br /&gt;
&lt;br /&gt;
│   ├── entry/                    // First screen of feature&lt;br /&gt;
&lt;br /&gt;
│   ├── [components]/             // Shared code between different screens&lt;br /&gt;
&lt;br /&gt;
│   ├── [screen name]/            // Folder for each screen&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module][name]Screen.kt       // Composable showing the complete screen&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module][name]Composable.kt   // Composable showing elements inside the screen&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module][name]UiEvent.kt      // Sealed interface describing the communication from the composable to the view model&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module][name]UiState.kt      // Data class describing the content to be rendered by the composable&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module][name]ViewModel.kt    // Class implementing the view model&lt;br /&gt;
&lt;br /&gt;
│   ├── navigation/               // Folder containing class and functionality related to screen navigation&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module]NavigationEvent.kt  // One sealed class per module containing the navigation intent of the user&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module]Navigator.kt  // Module-specific navigation functionality&lt;br /&gt;
&lt;br /&gt;
│&lt;br /&gt;
└── build.gradle                  // Plugins, setting, dependencies&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Screen navigation ===&lt;br /&gt;
The screen navigation is controlled by a &amp;lt;code&amp;gt;NavHostController&amp;lt;/code&amp;gt; which is created in &amp;lt;code&amp;gt;MainActivity&amp;lt;/code&amp;gt; and remembered using &amp;lt;code&amp;gt;rememberNavController&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
==== Modular navigation ====&lt;br /&gt;
Each module adds its own navigation function to &amp;lt;code&amp;gt;NavHostController&amp;lt;/code&amp;gt;.&lt;br /&gt;
These extension functions are stored in &amp;lt;code&amp;gt;[module]/presentation/navigation/[module]Navigator.kt&amp;lt;/code&amp;gt;.&lt;br /&gt;
They encapsulate the routing logic for that specific module.&lt;br /&gt;
&lt;br /&gt;
==== Event-Driven Navigation ====&lt;br /&gt;
The functions receive as parameter a specific sealed class &amp;lt;code&amp;gt;[module]NavigationEvent&amp;lt;/code&amp;gt;.&lt;br /&gt;
These sealed classes contain objects for each navigation event inside the module.&lt;br /&gt;
&lt;br /&gt;
==== Implementation pattern ====&lt;br /&gt;
The navigation functions are used in the following locations:&lt;br /&gt;
* inside the lambda function passed to &amp;lt;code&amp;gt;NavHost&amp;lt;/code&amp;gt; in &amp;lt;code&amp;gt;MainActivity&amp;lt;/code&amp;gt; for each screen.&lt;br /&gt;
* inside composables themselves being passed as lambda function from within &amp;lt;code&amp;gt;MainActivity&amp;lt;/code&amp;gt;&lt;br /&gt;
:* The composable remain unaware of &amp;lt;code&amp;gt;NavHostController&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Routes and arguments ====&lt;br /&gt;
Source and destination of the routing is defined using strings from the object &amp;lt;code&amp;gt;ViewNavigationRoutes&amp;lt;/code&amp;gt; where data that needs to be transferred between screens is embedded in the strings.&lt;br /&gt;
&lt;br /&gt;
== Example workflow for adding API communication ==&lt;br /&gt;
=== Implementation ===&lt;br /&gt;
Create the model: &amp;lt;code&amp;gt;RefillState&amp;lt;/code&amp;gt; as &amp;lt;code&amp;gt;enum&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Add a reader &amp;lt;code&amp;gt;class&amp;lt;/code&amp;gt; &amp;lt;code&amp;gt;RefillStateReader&amp;lt;/code&amp;gt; which uses the injected interface (Retrofit)&lt;br /&gt;
&lt;br /&gt;
Update the &amp;lt;code&amp;gt;RefillStateApiService&amp;lt;/code&amp;gt;: Add a function to load the state via Retrofit&lt;br /&gt;
&lt;br /&gt;
Add the label for the JSON property in &amp;lt;code&amp;gt;WebApiParams&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Add the URL for the end point in &amp;lt;code&amp;gt;WebApiUrls&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Add a wrapper &amp;lt;code&amp;gt;RefillStateResponse&amp;lt;/code&amp;gt; for the model to allow Retrofit parsing the JSON&lt;br /&gt;
&lt;br /&gt;
Update the repository interface and the repository interface implementation:&lt;br /&gt;
* Add a flow that delivers a &amp;lt;code&amp;gt;DataFetchResult&amp;lt;/code&amp;gt; of the &amp;lt;code&amp;gt;RefillState&amp;lt;/code&amp;gt;&lt;br /&gt;
* Add a function that allows the view model to trigger a refresh&lt;br /&gt;
&lt;br /&gt;
Add a wrapper &amp;lt;code&amp;gt;class&amp;lt;/code&amp;gt; &amp;lt;code&amp;gt;RefillStateUiState&amp;lt;/code&amp;gt;&lt;br /&gt;
Update the view model&lt;br /&gt;
* Add a flow that delivers a &amp;lt;code&amp;gt;RefillStateUiState&amp;lt;/code&amp;gt; based on the &amp;lt;code&amp;gt;DataFetchResult&amp;lt;/code&amp;gt; from repository&lt;br /&gt;
* Handle the new UI event for refreshing&lt;br /&gt;
&lt;br /&gt;
Add composable &amp;lt;code&amp;gt;RefillStateComposable&amp;lt;/code&amp;gt; for displaying the &amp;lt;code&amp;gt;RefillState&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Update &amp;lt;code&amp;gt;RefillUiEvent&amp;lt;/code&amp;gt; with an object for the refresh&lt;br /&gt;
&lt;br /&gt;
Update &amp;lt;code&amp;gt;RefillScreen&amp;lt;/code&amp;gt;:&lt;br /&gt;
* additional parameter (&amp;lt;code&amp;gt;RefillStateUiState&amp;lt;/code&amp;gt;) in composable function and in previews&lt;br /&gt;
* Added &amp;lt;code&amp;gt;PullToRefreshBox&amp;lt;/code&amp;gt;&lt;br /&gt;
* Added parsing of &amp;lt;code&amp;gt;RefillUiState&amp;lt;/code&amp;gt; and display of either&lt;br /&gt;
:* &amp;lt;code&amp;gt;RefillStateComposable&amp;lt;/code&amp;gt; in case of success&lt;br /&gt;
:* Text-based error message in case of error&lt;br /&gt;
:* &amp;lt;code&amp;gt;CircularProgressIndicator&amp;lt;/code&amp;gt; in case of loading&lt;br /&gt;
&lt;br /&gt;
Inject &amp;lt;code&amp;gt;RefillUiState&amp;lt;/code&amp;gt; into &amp;lt;code&amp;gt;RefillScreen&amp;lt;/code&amp;gt; in the &amp;lt;code&amp;gt;MainActivity&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;RefillScreenTest&amp;lt;/code&amp;gt; as well as in previews of &amp;lt;code&amp;gt;RefillScreen&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
(Optional) Check if &amp;lt;code&amp;gt;RefillDataModule&amp;lt;/code&amp;gt; needs update (not required in this case)&lt;br /&gt;
&lt;br /&gt;
=== Testing ===&lt;br /&gt;
Update existing screenshot testing (compose screenshot testing and instrumented tests)&lt;br /&gt;
&lt;br /&gt;
Add compose screenshot testing for new composable&lt;br /&gt;
&lt;br /&gt;
== Test strategy ==&lt;br /&gt;
The app uses different testing approaches:&lt;br /&gt;
* Compose preview screenshot testing&lt;br /&gt;
* Screenshot tests running as instrumented tests on emulator&lt;br /&gt;
* Unit tests&lt;br /&gt;
&lt;br /&gt;
=== Compose preview screenshot tests ===&lt;br /&gt;
These tests run comparably fast and device-independent.&lt;br /&gt;
&lt;br /&gt;
The compose preview screenshot tests cover portrait mode/landscape mode, light/dark mode (four variants).&lt;br /&gt;
&lt;br /&gt;
Limitations:&lt;br /&gt;
* Two tests within &amp;lt;code&amp;gt;feed&amp;lt;/code&amp;gt; module can only run in landscape mode (&amp;lt;code&amp;gt;FeedProfileScheduleScreenTest&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;FeedHistoryScreenTest&amp;lt;/code&amp;gt;) due to excessive heap memory utilisation.&lt;br /&gt;
* &amp;lt;code&amp;gt;Info&amp;lt;/code&amp;gt; screen is not tested due to &amp;lt;code&amp;gt;LibrariesContainer&amp;lt;/code&amp;gt; (dynamic content which cannot be mocked).&lt;br /&gt;
&lt;br /&gt;
The tests are located in &amp;lt;code&amp;gt;[module]/src/screenshotTest/kotlin/com/aquariumcontrol/[module]/&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
The references are located in &amp;lt;code&amp;gt;[module]/src/screenshotTestDebug/kotlin/com/aquariumcontrol/[module]/&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
The test reports are located in &amp;lt;code&amp;gt;build/reports/screenshotTest/preview/debug/&amp;lt;/code&amp;gt;. The test reports for the compose screenshot tests contain reference, actual and delta images.&lt;br /&gt;
&lt;br /&gt;
The images are generated with &amp;lt;code&amp;gt;./gradlew [module]:updateDebugScreenshotTest&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
The images are validated with &amp;lt;code&amp;gt;./gradlew [module]:validateDebugScreenshotTest&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Omitting the module will execute all tests.&lt;br /&gt;
&lt;br /&gt;
=== Instrumented tests ===&lt;br /&gt;
The tests run comparably long and are bound to an emulator device.&lt;br /&gt;
As of November 2025, a Pixel 5 API S is used for the instrumented test.&lt;br /&gt;
The instrumented tests contain&lt;br /&gt;
* screenshot testing for the complete screens (&amp;lt;code&amp;gt;ScreenNameTest&amp;lt;/code&amp;gt;)&lt;br /&gt;
* verification if the composable triggers the right event when tapping on buttons bound to navigation to another screen (&amp;lt;code&amp;gt;ScreenNameUITest&amp;lt;/code&amp;gt;)&lt;br /&gt;
&lt;br /&gt;
Limitations:&lt;br /&gt;
* &amp;lt;code&amp;gt;Info&amp;lt;/code&amp;gt; screen is not tested due to &amp;lt;code&amp;gt;LibrariesContainer&amp;lt;/code&amp;gt; (dynamic content which cannot be mocked and also does not allow execution of instrumented test).&lt;br /&gt;
&lt;br /&gt;
The test reports are located in &amp;lt;code&amp;gt;build/reports/androidTests/connected/debug/&amp;lt;/code&amp;gt;. The test reports for the compose screenshot tests do not contain any images.&lt;br /&gt;
&lt;br /&gt;
The tests are executed with &amp;lt;code&amp;gt;./gradlew [module]:connectedDebugAndroidTest&amp;lt;/code&amp;gt;.&lt;br /&gt;
Omitting the module will execute all tests.&lt;br /&gt;
&lt;br /&gt;
A switch called &amp;lt;code&amp;gt;recordMode&amp;lt;/code&amp;gt; inside each test class for screenshot testing determines if the image is generated &amp;lt;code&amp;gt;(true)&amp;lt;/code&amp;gt; or if the image is validated &amp;lt;code&amp;gt;(false)&amp;lt;/code&amp;gt;. The images are generated on the device &amp;lt;code&amp;gt;(/data/data/com.laimburggasse.aquariumcontrol.[module].test/files/screenshots_output/)&amp;lt;/code&amp;gt; and need to be downloaded to the repository.&lt;br /&gt;
&lt;br /&gt;
=== Unit tests ===&lt;br /&gt;
As of November 2025, there are no unit tests.&lt;br /&gt;
&lt;br /&gt;
== Release procedure ==&lt;br /&gt;
# Update &amp;lt;code&amp;gt;versionCode&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;versionName&amp;lt;/code&amp;gt; in &amp;lt;code&amp;gt;build.gradle.kts&amp;lt;/code&amp;gt; of &amp;lt;code&amp;gt;app&amp;lt;/code&amp;gt; module.&lt;br /&gt;
# Create a release branch (&amp;lt;code&amp;gt;release/[version]&amp;lt;/code&amp;gt;), checkout, commit and push.&lt;br /&gt;
# Change to release build variant.&lt;br /&gt;
# Build&lt;br /&gt;
# Generate a signed App bundle&lt;br /&gt;
# Upload to play store&lt;/div&gt;</summary>
		<author><name>Uwe</name></author>
	</entry>
	<entry>
		<id>http://217.79.180.177/mediawiki/index.php?title=Android_application&amp;diff=452</id>
		<title>Android application</title>
		<link rel="alternate" type="text/html" href="http://217.79.180.177/mediawiki/index.php?title=Android_application&amp;diff=452"/>
		<updated>2026-04-18T13:39:50Z</updated>

		<summary type="html">&lt;p&gt;Uwe: /* Screen navigation */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;The app is available in the [https://play.google.com/store/apps/details?id=com.laimburggasse.aquariumcontroller Play store].&lt;br /&gt;
== Requirements ==&lt;br /&gt;
== Architecture ==&lt;br /&gt;
The app is written completely in Kotlin using Jetpack Compose, Dagger/Hilt and aiming to implement a clean architecture.&lt;br /&gt;
=== Module structure ===&lt;br /&gt;
Besides the main app module, the app consists of the following library modules:&lt;br /&gt;
* balling&lt;br /&gt;
* common&lt;br /&gt;
* controller&lt;br /&gt;
* feed&lt;br /&gt;
* heating&lt;br /&gt;
* info&lt;br /&gt;
* overview&lt;br /&gt;
* refill&lt;br /&gt;
* schedule&lt;br /&gt;
* timedata&lt;br /&gt;
* ventilation&lt;br /&gt;
&lt;br /&gt;
The main &amp;lt;code&amp;gt;app&amp;lt;/code&amp;gt; module contains the AndroidManifest and the main activity (As of November 2025, the app only uses one activity).&lt;br /&gt;
The activity contains the navigation &amp;lt;code&amp;gt;NavHost&amp;lt;/code&amp;gt; in &amp;lt;code&amp;gt;onCreate&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
None of the other modules has a dependency to the main &amp;lt;code&amp;gt;app&amp;lt;/code&amp;gt; module.&lt;br /&gt;
&lt;br /&gt;
The &amp;lt;code&amp;gt;common&amp;lt;/code&amp;gt; module provides low-level functionalities and layout elements used in various places.&lt;br /&gt;
The following functionalities are located in &amp;lt;code&amp;gt;common&amp;lt;/code&amp;gt; module:&lt;br /&gt;
* &amp;lt;code&amp;gt;DataFetchResult&amp;lt;/code&amp;gt;: A wrapper class indicating the status of data fetching operation (&amp;lt;code&amp;gt;Success&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Error&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Loading&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Empty&amp;lt;/code&amp;gt;).&lt;br /&gt;
* &amp;lt;code&amp;gt;WebApiParams&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;WebApiPostRequest&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;WebApiPostRequestExecution&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;WebApiUrls&amp;lt;/code&amp;gt;: Classes and objects used for communication from the app to the server.&lt;br /&gt;
* &amp;lt;code&amp;gt;ControllerProfileDao&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;ControllerProfileDatabase&amp;lt;/code&amp;gt;: Classes which provide Room database functionality.&lt;br /&gt;
* Various classes used for dependency injection (Hilt)&lt;br /&gt;
* &amp;lt;code&amp;gt;SetValsSanityCheckResult&amp;lt;/code&amp;gt;: Class used for to communicate status of set values for &amp;lt;code&amp;gt;heating&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;ventilation&amp;lt;/code&amp;gt; module.&lt;br /&gt;
* &amp;lt;code&amp;gt;ControllerProfile&amp;lt;/code&amp;gt;: Class containing the profile data of the server (address, port, credentials, ...)&lt;br /&gt;
* &amp;lt;code&amp;gt;GlobalConstants&amp;lt;/code&amp;gt;: Mainly UI-related strings (non-context-related) and some minor functional constants&lt;br /&gt;
* Composable functions for the main drop down menu&lt;br /&gt;
* Composable functions for the theme&lt;br /&gt;
* Composable functions for hyperlinks, text edit fields, headline&lt;br /&gt;
* &amp;lt;code&amp;gt;ViewNavigationRoutes&amp;lt;/code&amp;gt;: Object containing the routing information processed in main &amp;lt;code&amp;gt;app&amp;lt;/code&amp;gt; module&lt;br /&gt;
* &amp;lt;code&amp;gt;ScreenshotTestHelper&amp;lt;/code&amp;gt;: Helper class for executing the instrumented snapshot testing using the emulator.&lt;br /&gt;
&lt;br /&gt;
=== Layer structure ===&lt;br /&gt;
The code is separated into the following layers:&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
[module]/&lt;br /&gt;
&lt;br /&gt;
├── src/main/kotlin/com/laimburggasse/aquariumcontrol/[module]/data/&lt;br /&gt;
&lt;br /&gt;
│   ├── remote/                   // Folder for data retrieval functionality&lt;br /&gt;
&lt;br /&gt;
│   │   └── dto/                  // Folder for Data Transfer Objects&lt;br /&gt;
&lt;br /&gt;
│   │       └── [module][name]Import.kt&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module][name]Reader.kt      // Implementation of GET server request&lt;br /&gt;
&lt;br /&gt;
│   ├── [module][name]RepositoryImpl.kt  // Repositories (implementation of interface)&lt;br /&gt;
&lt;br /&gt;
├── src/main/kotlin/com/laimburggasse/aquariumcontrol/[module]/domain/&lt;br /&gt;
&lt;br /&gt;
│   ├── model/                    // Folder for data definition classes&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module][name].kt.    // Classes for data definition&lt;br /&gt;
&lt;br /&gt;
│   ├── repository/               // Folder containing repository interface definitions&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module][name]Repository.kt.  // Interface defining the repository, used by view model&lt;br /&gt;
&lt;br /&gt;
├── src/main/kotlin/com/laimburggasse/aquariumcontrol/[module]/presentation/&lt;br /&gt;
&lt;br /&gt;
│   ├── entry/                    // First screen of feature&lt;br /&gt;
&lt;br /&gt;
│   ├── [components]/             // Shared code between different screens&lt;br /&gt;
&lt;br /&gt;
│   ├── [screen name]/            // Folder for each screen&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module][name]Screen.kt       // Composable showing the complete screen&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module][name]Composable.kt   // Composable showing elements inside the screen&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module][name]UiEvent.kt      // Sealed interface describing the communication from the composable to the view model&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module][name]UiState.kt      // Data class describing the content to be rendered by the composable&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module][name]ViewModel.kt    // Class implementing the view model&lt;br /&gt;
&lt;br /&gt;
│   ├── navigation/               // Folder containing class and functionality related to screen navigation&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module]NavigationEvent.kt  // One sealed class per module containing the navigation intent of the user&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module]Navigator.kt  // Module-specific navigation functionality&lt;br /&gt;
&lt;br /&gt;
│&lt;br /&gt;
└── build.gradle                  // Plugins, setting, dependencies&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Screen navigation ===&lt;br /&gt;
The screen navigation is controlled by a &amp;lt;code&amp;gt;NavHostController&amp;lt;/code&amp;gt; which is created in &amp;lt;code&amp;gt;MainActivity&amp;lt;/code&amp;gt; and remembered using &amp;lt;code&amp;gt;rememberNavController&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
==== Modular navigation ====&lt;br /&gt;
Each module adds its own navigation function to &amp;lt;code&amp;gt;NavHostController&amp;lt;/code&amp;gt;.&lt;br /&gt;
These extension functions are stored in &amp;lt;code&amp;gt;[module]/presentation/navigation/[module]Navigator.kt&amp;lt;/code&amp;gt;.&lt;br /&gt;
They encapsulate the routing logic for that specific module.&lt;br /&gt;
&lt;br /&gt;
==== Event-Driven Navigation ====&lt;br /&gt;
The functions receive as parameter a specific sealed class &amp;lt;code&amp;gt;[module]NavigationEvent&amp;lt;/code&amp;gt;.&lt;br /&gt;
These sealed classes contain objects for each navigation event inside the module.&lt;br /&gt;
&lt;br /&gt;
==== Implementation pattern ====&lt;br /&gt;
The navigation functions are used in the following locations:&lt;br /&gt;
* inside the lambda function passed to &amp;lt;code&amp;gt;NavHost&amp;lt;/code&amp;gt; in &amp;lt;code&amp;gt;MainActivity&amp;lt;/code&amp;gt; for each screen.&lt;br /&gt;
* inside composables themselves being passed as lambda function from within &amp;lt;code&amp;gt;MainActivity&amp;lt;/code&amp;gt;&lt;br /&gt;
:* The composable remain unaware of &amp;lt;code&amp;gt;NavHostController&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Routes and arguments ====&lt;br /&gt;
Source and destination of the routing is defined using strings from the object &amp;lt;code&amp;gt;ViewNavigationRoutes&amp;lt;/code&amp;gt; where data that needs to be transferred between screens is embedded in the strings.&lt;br /&gt;
&lt;br /&gt;
== Example workflow for adding API communication ==&lt;br /&gt;
=== Implementation ===&lt;br /&gt;
Create the model: &amp;lt;code&amp;gt;RefillState&amp;lt;/code&amp;gt; as &amp;lt;code&amp;gt;enum&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Add a reader &amp;lt;code&amp;gt;class&amp;lt;/code&amp;gt; &amp;lt;code&amp;gt;RefillStateReader&amp;lt;/code&amp;gt; which uses the injected interface (Retrofit)&lt;br /&gt;
&lt;br /&gt;
Update the &amp;lt;code&amp;gt;RefillStateApiService&amp;lt;/code&amp;gt;: Add a function to load the state via Retrofit&lt;br /&gt;
&lt;br /&gt;
Add the label for the JSON property in &amp;lt;code&amp;gt;WebApiParams&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Add the URL for the end point in &amp;lt;code&amp;gt;WebApiUrls&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Add a wrapper &amp;lt;code&amp;gt;RefillStateResponse&amp;lt;/code&amp;gt; for the model to allow Retrofit parsing the JSON&lt;br /&gt;
&lt;br /&gt;
Update the repository interface and the repository interface implementation:&lt;br /&gt;
* Add a flow that delivers a &amp;lt;code&amp;gt;DataFetchResult&amp;lt;/code&amp;gt; of the &amp;lt;code&amp;gt;RefillState&amp;lt;/code&amp;gt;&lt;br /&gt;
* Add a function that allows the view model to trigger a refresh&lt;br /&gt;
&lt;br /&gt;
Add a wrapper &amp;lt;code&amp;gt;class&amp;lt;/code&amp;gt; &amp;lt;code&amp;gt;RefillStateUiState&amp;lt;/code&amp;gt;&lt;br /&gt;
Update the view model&lt;br /&gt;
* Add a flow that delivers a &amp;lt;code&amp;gt;RefillStateUiState&amp;lt;/code&amp;gt; based on the &amp;lt;code&amp;gt;DataFetchResult&amp;lt;/code&amp;gt; from repository&lt;br /&gt;
* Handle the new UI event for refreshing&lt;br /&gt;
&lt;br /&gt;
Add composable &amp;lt;code&amp;gt;RefillStateComposable&amp;lt;/code&amp;gt; for displaying the &amp;lt;code&amp;gt;RefillState&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Update &amp;lt;code&amp;gt;RefillUiEvent&amp;lt;/code&amp;gt; with an object for the refresh&lt;br /&gt;
&lt;br /&gt;
Update &amp;lt;code&amp;gt;RefillScreen&amp;lt;/code&amp;gt;:&lt;br /&gt;
* additional parameter (&amp;lt;code&amp;gt;RefillStateUiState&amp;lt;/code&amp;gt;) in composable function and in previews&lt;br /&gt;
* Added &amp;lt;code&amp;gt;PullToRefreshBox&amp;lt;/code&amp;gt;&lt;br /&gt;
* Added parsing of &amp;lt;code&amp;gt;RefillUiState&amp;lt;/code&amp;gt; and display of either&lt;br /&gt;
:* &amp;lt;code&amp;gt;RefillStateComposable&amp;lt;/code&amp;gt; in case of success&lt;br /&gt;
:* Text-based error message in case of error&lt;br /&gt;
:* &amp;lt;code&amp;gt;CircularProgressIndicator&amp;lt;/code&amp;gt; in case of loading&lt;br /&gt;
&lt;br /&gt;
Inject &amp;lt;code&amp;gt;RefillUiState&amp;lt;/code&amp;gt; into &amp;lt;code&amp;gt;RefillScreen&amp;lt;/code&amp;gt; in the &amp;lt;code&amp;gt;MainActivity&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;RefillScreenTest&amp;lt;/code&amp;gt; as well as in previews of &amp;lt;code&amp;gt;RefillScreen&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
(Optional) Check if &amp;lt;code&amp;gt;RefillDataModule&amp;lt;/code&amp;gt; needs update (not required in this case)&lt;br /&gt;
&lt;br /&gt;
=== Testing ===&lt;br /&gt;
Update existing screenshot testing&lt;br /&gt;
&lt;br /&gt;
Add screenshot testing for new composable&lt;br /&gt;
&lt;br /&gt;
== Test strategy ==&lt;br /&gt;
The app uses different testing approaches:&lt;br /&gt;
* Compose preview screenshot testing&lt;br /&gt;
* Screenshot tests running as instrumented tests on emulator&lt;br /&gt;
* Unit tests&lt;br /&gt;
&lt;br /&gt;
=== Compose preview screenshot tests ===&lt;br /&gt;
These tests run comparably fast and device-independent.&lt;br /&gt;
&lt;br /&gt;
The compose preview screenshot tests cover portrait mode/landscape mode, light/dark mode (four variants).&lt;br /&gt;
&lt;br /&gt;
Limitations:&lt;br /&gt;
* Two tests within &amp;lt;code&amp;gt;feed&amp;lt;/code&amp;gt; module can only run in landscape mode (&amp;lt;code&amp;gt;FeedProfileScheduleScreenTest&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;FeedHistoryScreenTest&amp;lt;/code&amp;gt;) due to excessive heap memory utilisation.&lt;br /&gt;
* &amp;lt;code&amp;gt;Info&amp;lt;/code&amp;gt; screen is not tested due to &amp;lt;code&amp;gt;LibrariesContainer&amp;lt;/code&amp;gt; (dynamic content which cannot be mocked).&lt;br /&gt;
&lt;br /&gt;
The tests are located in &amp;lt;code&amp;gt;[module]/src/screenshotTest/kotlin/com/aquariumcontrol/[module]/&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
The references are located in &amp;lt;code&amp;gt;[module]/src/screenshotTestDebug/kotlin/com/aquariumcontrol/[module]/&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
The test reports are located in &amp;lt;code&amp;gt;build/reports/screenshotTest/preview/debug/&amp;lt;/code&amp;gt;. The test reports for the compose screenshot tests contain reference, actual and delta images.&lt;br /&gt;
&lt;br /&gt;
The images are generated with &amp;lt;code&amp;gt;./gradlew [module]:updateDebugScreenshotTest&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
The images are validated with &amp;lt;code&amp;gt;./gradlew [module]:validateDebugScreenshotTest&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Omitting the module will execute all tests.&lt;br /&gt;
&lt;br /&gt;
=== Instrumented tests ===&lt;br /&gt;
The tests run comparably long and are bound to an emulator device.&lt;br /&gt;
As of November 2025, a Pixel 5 API S is used for the instrumented test.&lt;br /&gt;
The instrumented tests contain&lt;br /&gt;
* screenshot testing for the complete screens (&amp;lt;code&amp;gt;ScreenNameTest&amp;lt;/code&amp;gt;)&lt;br /&gt;
* verification if the composable triggers the right event when tapping on buttons bound to navigation to another screen (&amp;lt;code&amp;gt;ScreenNameUITest&amp;lt;/code&amp;gt;)&lt;br /&gt;
&lt;br /&gt;
Limitations:&lt;br /&gt;
* &amp;lt;code&amp;gt;Info&amp;lt;/code&amp;gt; screen is not tested due to &amp;lt;code&amp;gt;LibrariesContainer&amp;lt;/code&amp;gt; (dynamic content which cannot be mocked and also does not allow execution of instrumented test).&lt;br /&gt;
&lt;br /&gt;
The test reports are located in &amp;lt;code&amp;gt;build/reports/androidTests/connected/debug/&amp;lt;/code&amp;gt;. The test reports for the compose screenshot tests do not contain any images.&lt;br /&gt;
&lt;br /&gt;
The tests are executed with &amp;lt;code&amp;gt;./gradlew [module]:connectedDebugAndroidTest&amp;lt;/code&amp;gt;.&lt;br /&gt;
Omitting the module will execute all tests.&lt;br /&gt;
&lt;br /&gt;
A switch called &amp;lt;code&amp;gt;recordMode&amp;lt;/code&amp;gt; inside each test class for screenshot testing determines if the image is generated &amp;lt;code&amp;gt;(true)&amp;lt;/code&amp;gt; or if the image is validated &amp;lt;code&amp;gt;(false)&amp;lt;/code&amp;gt;. The images are generated on the device &amp;lt;code&amp;gt;(/data/data/com.laimburggasse.aquariumcontrol.[module].test/files/screenshots_output/)&amp;lt;/code&amp;gt; and need to be downloaded to the repository.&lt;br /&gt;
&lt;br /&gt;
=== Unit tests ===&lt;br /&gt;
As of November 2025, there are no unit tests.&lt;br /&gt;
&lt;br /&gt;
== Release procedure ==&lt;br /&gt;
# Update &amp;lt;code&amp;gt;versionCode&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;versionName&amp;lt;/code&amp;gt; in &amp;lt;code&amp;gt;build.gradle.kts&amp;lt;/code&amp;gt; of &amp;lt;code&amp;gt;app&amp;lt;/code&amp;gt; module.&lt;br /&gt;
# Create a release branch (&amp;lt;code&amp;gt;release/[version]&amp;lt;/code&amp;gt;), checkout, commit and push.&lt;br /&gt;
# Change to release build variant.&lt;br /&gt;
# Build&lt;br /&gt;
# Generate a signed App bundle&lt;br /&gt;
# Upload to play store&lt;/div&gt;</summary>
		<author><name>Uwe</name></author>
	</entry>
	<entry>
		<id>http://217.79.180.177/mediawiki/index.php?title=Android_application&amp;diff=451</id>
		<title>Android application</title>
		<link rel="alternate" type="text/html" href="http://217.79.180.177/mediawiki/index.php?title=Android_application&amp;diff=451"/>
		<updated>2026-04-18T12:56:24Z</updated>

		<summary type="html">&lt;p&gt;Uwe: /* Screen navigation */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;The app is available in the [https://play.google.com/store/apps/details?id=com.laimburggasse.aquariumcontroller Play store].&lt;br /&gt;
== Requirements ==&lt;br /&gt;
== Architecture ==&lt;br /&gt;
The app is written completely in Kotlin using Jetpack Compose, Dagger/Hilt and aiming to implement a clean architecture.&lt;br /&gt;
=== Module structure ===&lt;br /&gt;
Besides the main app module, the app consists of the following library modules:&lt;br /&gt;
* balling&lt;br /&gt;
* common&lt;br /&gt;
* controller&lt;br /&gt;
* feed&lt;br /&gt;
* heating&lt;br /&gt;
* info&lt;br /&gt;
* overview&lt;br /&gt;
* refill&lt;br /&gt;
* schedule&lt;br /&gt;
* timedata&lt;br /&gt;
* ventilation&lt;br /&gt;
&lt;br /&gt;
The main &amp;lt;code&amp;gt;app&amp;lt;/code&amp;gt; module contains the AndroidManifest and the main activity (As of November 2025, the app only uses one activity).&lt;br /&gt;
The activity contains the navigation &amp;lt;code&amp;gt;NavHost&amp;lt;/code&amp;gt; in &amp;lt;code&amp;gt;onCreate&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
None of the other modules has a dependency to the main &amp;lt;code&amp;gt;app&amp;lt;/code&amp;gt; module.&lt;br /&gt;
&lt;br /&gt;
The &amp;lt;code&amp;gt;common&amp;lt;/code&amp;gt; module provides low-level functionalities and layout elements used in various places.&lt;br /&gt;
The following functionalities are located in &amp;lt;code&amp;gt;common&amp;lt;/code&amp;gt; module:&lt;br /&gt;
* &amp;lt;code&amp;gt;DataFetchResult&amp;lt;/code&amp;gt;: A wrapper class indicating the status of data fetching operation (&amp;lt;code&amp;gt;Success&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Error&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Loading&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Empty&amp;lt;/code&amp;gt;).&lt;br /&gt;
* &amp;lt;code&amp;gt;WebApiParams&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;WebApiPostRequest&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;WebApiPostRequestExecution&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;WebApiUrls&amp;lt;/code&amp;gt;: Classes and objects used for communication from the app to the server.&lt;br /&gt;
* &amp;lt;code&amp;gt;ControllerProfileDao&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;ControllerProfileDatabase&amp;lt;/code&amp;gt;: Classes which provide Room database functionality.&lt;br /&gt;
* Various classes used for dependency injection (Hilt)&lt;br /&gt;
* &amp;lt;code&amp;gt;SetValsSanityCheckResult&amp;lt;/code&amp;gt;: Class used for to communicate status of set values for &amp;lt;code&amp;gt;heating&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;ventilation&amp;lt;/code&amp;gt; module.&lt;br /&gt;
* &amp;lt;code&amp;gt;ControllerProfile&amp;lt;/code&amp;gt;: Class containing the profile data of the server (address, port, credentials, ...)&lt;br /&gt;
* &amp;lt;code&amp;gt;GlobalConstants&amp;lt;/code&amp;gt;: Mainly UI-related strings (non-context-related) and some minor functional constants&lt;br /&gt;
* Composable functions for the main drop down menu&lt;br /&gt;
* Composable functions for the theme&lt;br /&gt;
* Composable functions for hyperlinks, text edit fields, headline&lt;br /&gt;
* &amp;lt;code&amp;gt;ViewNavigationRoutes&amp;lt;/code&amp;gt;: Object containing the routing information processed in main &amp;lt;code&amp;gt;app&amp;lt;/code&amp;gt; module&lt;br /&gt;
* &amp;lt;code&amp;gt;ScreenshotTestHelper&amp;lt;/code&amp;gt;: Helper class for executing the instrumented snapshot testing using the emulator.&lt;br /&gt;
&lt;br /&gt;
=== Layer structure ===&lt;br /&gt;
The code is separated into the following layers:&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
[module]/&lt;br /&gt;
&lt;br /&gt;
├── src/main/kotlin/com/laimburggasse/aquariumcontrol/[module]/data/&lt;br /&gt;
&lt;br /&gt;
│   ├── remote/                   // Folder for data retrieval functionality&lt;br /&gt;
&lt;br /&gt;
│   │   └── dto/                  // Folder for Data Transfer Objects&lt;br /&gt;
&lt;br /&gt;
│   │       └── [module][name]Import.kt&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module][name]Reader.kt      // Implementation of GET server request&lt;br /&gt;
&lt;br /&gt;
│   ├── [module][name]RepositoryImpl.kt  // Repositories (implementation of interface)&lt;br /&gt;
&lt;br /&gt;
├── src/main/kotlin/com/laimburggasse/aquariumcontrol/[module]/domain/&lt;br /&gt;
&lt;br /&gt;
│   ├── model/                    // Folder for data definition classes&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module][name].kt.    // Classes for data definition&lt;br /&gt;
&lt;br /&gt;
│   ├── repository/               // Folder containing repository interface definitions&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module][name]Repository.kt.  // Interface defining the repository, used by view model&lt;br /&gt;
&lt;br /&gt;
├── src/main/kotlin/com/laimburggasse/aquariumcontrol/[module]/presentation/&lt;br /&gt;
&lt;br /&gt;
│   ├── entry/                    // First screen of feature&lt;br /&gt;
&lt;br /&gt;
│   ├── [components]/             // Shared code between different screens&lt;br /&gt;
&lt;br /&gt;
│   ├── [screen name]/            // Folder for each screen&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module][name]Screen.kt       // Composable showing the complete screen&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module][name]Composable.kt   // Composable showing elements inside the screen&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module][name]UiEvent.kt      // Sealed interface describing the communication from the composable to the view model&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module][name]UiState.kt      // Data class describing the content to be rendered by the composable&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module][name]ViewModel.kt    // Class implementing the view model&lt;br /&gt;
&lt;br /&gt;
│   ├── navigation/               // Folder containing class and functionality related to screen navigation&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module]NavigationEvent.kt  // One sealed class per module containing the navigation intent of the user&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module]Navigator.kt  // Module-specific navigation functionality&lt;br /&gt;
&lt;br /&gt;
│&lt;br /&gt;
└── build.gradle                  // Plugins, setting, dependencies&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Screen navigation ===&lt;br /&gt;
The screen navigation is controlled by a &amp;lt;code&amp;gt;NavHostController&amp;lt;/code&amp;gt; which is instantiated in &amp;lt;code&amp;gt;MainActivity&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Each module adds its own navigation function to &amp;lt;code&amp;gt;NavHostController&amp;lt;/code&amp;gt;.&lt;br /&gt;
These functions are stored in &amp;lt;code&amp;gt;[module]/presentation/navigation/[module]Navigator.kt&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
The functions receive as parameter a specific sealed class &amp;lt;code&amp;gt;[module]NavigationEvent&amp;lt;/code&amp;gt;.&lt;br /&gt;
These sealed classes contain objects for each navigation event inside the module.&lt;br /&gt;
&lt;br /&gt;
The navigation functions are used in the following locations:&lt;br /&gt;
* inside the lambda function passed to &amp;lt;code&amp;gt;NavHost&amp;lt;/code&amp;gt; in &amp;lt;code&amp;gt;MainActivity&amp;lt;/code&amp;gt; for each screen.&lt;br /&gt;
* inside composables themselves being passed as lambda function from within &amp;lt;code&amp;gt;MainActivity&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Source and destination of the routing is defined using strings from the object &amp;lt;code&amp;gt;ViewNavigationRoutes&amp;lt;/code&amp;gt; where data that needs to be transferred between screens is embedded in the strings.&lt;br /&gt;
&lt;br /&gt;
== Example workflow for adding API communication ==&lt;br /&gt;
=== Implementation ===&lt;br /&gt;
Create the model: &amp;lt;code&amp;gt;RefillState&amp;lt;/code&amp;gt; as &amp;lt;code&amp;gt;enum&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Add a reader &amp;lt;code&amp;gt;class&amp;lt;/code&amp;gt; &amp;lt;code&amp;gt;RefillStateReader&amp;lt;/code&amp;gt; which uses the injected interface (Retrofit)&lt;br /&gt;
&lt;br /&gt;
Update the &amp;lt;code&amp;gt;RefillStateApiService&amp;lt;/code&amp;gt;: Add a function to load the state via Retrofit&lt;br /&gt;
&lt;br /&gt;
Add the label for the JSON property in &amp;lt;code&amp;gt;WebApiParams&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Add the URL for the end point in &amp;lt;code&amp;gt;WebApiUrls&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Add a wrapper &amp;lt;code&amp;gt;RefillStateResponse&amp;lt;/code&amp;gt; for the model to allow Retrofit parsing the JSON&lt;br /&gt;
&lt;br /&gt;
Update the repository interface and the repository interface implementation:&lt;br /&gt;
* Add a flow that delivers a &amp;lt;code&amp;gt;DataFetchResult&amp;lt;/code&amp;gt; of the &amp;lt;code&amp;gt;RefillState&amp;lt;/code&amp;gt;&lt;br /&gt;
* Add a function that allows the view model to trigger a refresh&lt;br /&gt;
&lt;br /&gt;
Add a wrapper &amp;lt;code&amp;gt;class&amp;lt;/code&amp;gt; &amp;lt;code&amp;gt;RefillStateUiState&amp;lt;/code&amp;gt;&lt;br /&gt;
Update the view model&lt;br /&gt;
* Add a flow that delivers a &amp;lt;code&amp;gt;RefillStateUiState&amp;lt;/code&amp;gt; based on the &amp;lt;code&amp;gt;DataFetchResult&amp;lt;/code&amp;gt; from repository&lt;br /&gt;
* Handle the new UI event for refreshing&lt;br /&gt;
&lt;br /&gt;
Add composable &amp;lt;code&amp;gt;RefillStateComposable&amp;lt;/code&amp;gt; for displaying the &amp;lt;code&amp;gt;RefillState&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Update &amp;lt;code&amp;gt;RefillUiEvent&amp;lt;/code&amp;gt; with an object for the refresh&lt;br /&gt;
&lt;br /&gt;
Update &amp;lt;code&amp;gt;RefillScreen&amp;lt;/code&amp;gt;:&lt;br /&gt;
* additional parameter (&amp;lt;code&amp;gt;RefillStateUiState&amp;lt;/code&amp;gt;) in composable function and in previews&lt;br /&gt;
* Added &amp;lt;code&amp;gt;PullToRefreshBox&amp;lt;/code&amp;gt;&lt;br /&gt;
* Added parsing of &amp;lt;code&amp;gt;RefillUiState&amp;lt;/code&amp;gt; and display of either&lt;br /&gt;
:* &amp;lt;code&amp;gt;RefillStateComposable&amp;lt;/code&amp;gt; in case of success&lt;br /&gt;
:* Text-based error message in case of error&lt;br /&gt;
:* &amp;lt;code&amp;gt;CircularProgressIndicator&amp;lt;/code&amp;gt; in case of loading&lt;br /&gt;
&lt;br /&gt;
Inject &amp;lt;code&amp;gt;RefillUiState&amp;lt;/code&amp;gt; into &amp;lt;code&amp;gt;RefillScreen&amp;lt;/code&amp;gt; in the &amp;lt;code&amp;gt;MainActivity&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;RefillScreenTest&amp;lt;/code&amp;gt; as well as in previews of &amp;lt;code&amp;gt;RefillScreen&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
(Optional) Check if &amp;lt;code&amp;gt;RefillDataModule&amp;lt;/code&amp;gt; needs update (not required in this case)&lt;br /&gt;
&lt;br /&gt;
=== Testing ===&lt;br /&gt;
Update existing screenshot testing&lt;br /&gt;
&lt;br /&gt;
Add screenshot testing for new composable&lt;br /&gt;
&lt;br /&gt;
== Test strategy ==&lt;br /&gt;
The app uses different testing approaches:&lt;br /&gt;
* Compose preview screenshot testing&lt;br /&gt;
* Screenshot tests running as instrumented tests on emulator&lt;br /&gt;
* Unit tests&lt;br /&gt;
&lt;br /&gt;
=== Compose preview screenshot tests ===&lt;br /&gt;
These tests run comparably fast and device-independent.&lt;br /&gt;
&lt;br /&gt;
The compose preview screenshot tests cover portrait mode/landscape mode, light/dark mode (four variants).&lt;br /&gt;
&lt;br /&gt;
Limitations:&lt;br /&gt;
* Two tests within &amp;lt;code&amp;gt;feed&amp;lt;/code&amp;gt; module can only run in landscape mode (&amp;lt;code&amp;gt;FeedProfileScheduleScreenTest&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;FeedHistoryScreenTest&amp;lt;/code&amp;gt;) due to excessive heap memory utilisation.&lt;br /&gt;
* &amp;lt;code&amp;gt;Info&amp;lt;/code&amp;gt; screen is not tested due to &amp;lt;code&amp;gt;LibrariesContainer&amp;lt;/code&amp;gt; (dynamic content which cannot be mocked).&lt;br /&gt;
&lt;br /&gt;
The tests are located in &amp;lt;code&amp;gt;[module]/src/screenshotTest/kotlin/com/aquariumcontrol/[module]/&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
The references are located in &amp;lt;code&amp;gt;[module]/src/screenshotTestDebug/kotlin/com/aquariumcontrol/[module]/&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
The test reports are located in &amp;lt;code&amp;gt;build/reports/screenshotTest/preview/debug/&amp;lt;/code&amp;gt;. The test reports for the compose screenshot tests contain reference, actual and delta images.&lt;br /&gt;
&lt;br /&gt;
The images are generated with &amp;lt;code&amp;gt;./gradlew [module]:updateDebugScreenshotTest&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
The images are validated with &amp;lt;code&amp;gt;./gradlew [module]:validateDebugScreenshotTest&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Omitting the module will execute all tests.&lt;br /&gt;
&lt;br /&gt;
=== Instrumented tests ===&lt;br /&gt;
The tests run comparably long and are bound to an emulator device.&lt;br /&gt;
As of November 2025, a Pixel 5 API S is used for the instrumented test.&lt;br /&gt;
The instrumented tests contain&lt;br /&gt;
* screenshot testing for the complete screens (&amp;lt;code&amp;gt;ScreenNameTest&amp;lt;/code&amp;gt;)&lt;br /&gt;
* verification if the composable triggers the right event when tapping on buttons bound to navigation to another screen (&amp;lt;code&amp;gt;ScreenNameUITest&amp;lt;/code&amp;gt;)&lt;br /&gt;
&lt;br /&gt;
Limitations:&lt;br /&gt;
* &amp;lt;code&amp;gt;Info&amp;lt;/code&amp;gt; screen is not tested due to &amp;lt;code&amp;gt;LibrariesContainer&amp;lt;/code&amp;gt; (dynamic content which cannot be mocked and also does not allow execution of instrumented test).&lt;br /&gt;
&lt;br /&gt;
The test reports are located in &amp;lt;code&amp;gt;build/reports/androidTests/connected/debug/&amp;lt;/code&amp;gt;. The test reports for the compose screenshot tests do not contain any images.&lt;br /&gt;
&lt;br /&gt;
The tests are executed with &amp;lt;code&amp;gt;./gradlew [module]:connectedDebugAndroidTest&amp;lt;/code&amp;gt;.&lt;br /&gt;
Omitting the module will execute all tests.&lt;br /&gt;
&lt;br /&gt;
A switch called &amp;lt;code&amp;gt;recordMode&amp;lt;/code&amp;gt; inside each test class for screenshot testing determines if the image is generated &amp;lt;code&amp;gt;(true)&amp;lt;/code&amp;gt; or if the image is validated &amp;lt;code&amp;gt;(false)&amp;lt;/code&amp;gt;. The images are generated on the device &amp;lt;code&amp;gt;(/data/data/com.laimburggasse.aquariumcontrol.[module].test/files/screenshots_output/)&amp;lt;/code&amp;gt; and need to be downloaded to the repository.&lt;br /&gt;
&lt;br /&gt;
=== Unit tests ===&lt;br /&gt;
As of November 2025, there are no unit tests.&lt;br /&gt;
&lt;br /&gt;
== Release procedure ==&lt;br /&gt;
# Update &amp;lt;code&amp;gt;versionCode&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;versionName&amp;lt;/code&amp;gt; in &amp;lt;code&amp;gt;build.gradle.kts&amp;lt;/code&amp;gt; of &amp;lt;code&amp;gt;app&amp;lt;/code&amp;gt; module.&lt;br /&gt;
# Create a release branch (&amp;lt;code&amp;gt;release/[version]&amp;lt;/code&amp;gt;), checkout, commit and push.&lt;br /&gt;
# Change to release build variant.&lt;br /&gt;
# Build&lt;br /&gt;
# Generate a signed App bundle&lt;br /&gt;
# Upload to play store&lt;/div&gt;</summary>
		<author><name>Uwe</name></author>
	</entry>
	<entry>
		<id>http://217.79.180.177/mediawiki/index.php?title=Android_application&amp;diff=450</id>
		<title>Android application</title>
		<link rel="alternate" type="text/html" href="http://217.79.180.177/mediawiki/index.php?title=Android_application&amp;diff=450"/>
		<updated>2026-04-18T12:29:54Z</updated>

		<summary type="html">&lt;p&gt;Uwe: /* Implementation */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;The app is available in the [https://play.google.com/store/apps/details?id=com.laimburggasse.aquariumcontroller Play store].&lt;br /&gt;
== Requirements ==&lt;br /&gt;
== Architecture ==&lt;br /&gt;
The app is written completely in Kotlin using Jetpack Compose, Dagger/Hilt and aiming to implement a clean architecture.&lt;br /&gt;
=== Module structure ===&lt;br /&gt;
Besides the main app module, the app consists of the following library modules:&lt;br /&gt;
* balling&lt;br /&gt;
* common&lt;br /&gt;
* controller&lt;br /&gt;
* feed&lt;br /&gt;
* heating&lt;br /&gt;
* info&lt;br /&gt;
* overview&lt;br /&gt;
* refill&lt;br /&gt;
* schedule&lt;br /&gt;
* timedata&lt;br /&gt;
* ventilation&lt;br /&gt;
&lt;br /&gt;
The main &amp;lt;code&amp;gt;app&amp;lt;/code&amp;gt; module contains the AndroidManifest and the main activity (As of November 2025, the app only uses one activity).&lt;br /&gt;
The activity contains the navigation &amp;lt;code&amp;gt;NavHost&amp;lt;/code&amp;gt; in &amp;lt;code&amp;gt;onCreate&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
None of the other modules has a dependency to the main &amp;lt;code&amp;gt;app&amp;lt;/code&amp;gt; module.&lt;br /&gt;
&lt;br /&gt;
The &amp;lt;code&amp;gt;common&amp;lt;/code&amp;gt; module provides low-level functionalities and layout elements used in various places.&lt;br /&gt;
The following functionalities are located in &amp;lt;code&amp;gt;common&amp;lt;/code&amp;gt; module:&lt;br /&gt;
* &amp;lt;code&amp;gt;DataFetchResult&amp;lt;/code&amp;gt;: A wrapper class indicating the status of data fetching operation (&amp;lt;code&amp;gt;Success&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Error&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Loading&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Empty&amp;lt;/code&amp;gt;).&lt;br /&gt;
* &amp;lt;code&amp;gt;WebApiParams&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;WebApiPostRequest&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;WebApiPostRequestExecution&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;WebApiUrls&amp;lt;/code&amp;gt;: Classes and objects used for communication from the app to the server.&lt;br /&gt;
* &amp;lt;code&amp;gt;ControllerProfileDao&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;ControllerProfileDatabase&amp;lt;/code&amp;gt;: Classes which provide Room database functionality.&lt;br /&gt;
* Various classes used for dependency injection (Hilt)&lt;br /&gt;
* &amp;lt;code&amp;gt;SetValsSanityCheckResult&amp;lt;/code&amp;gt;: Class used for to communicate status of set values for &amp;lt;code&amp;gt;heating&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;ventilation&amp;lt;/code&amp;gt; module.&lt;br /&gt;
* &amp;lt;code&amp;gt;ControllerProfile&amp;lt;/code&amp;gt;: Class containing the profile data of the server (address, port, credentials, ...)&lt;br /&gt;
* &amp;lt;code&amp;gt;GlobalConstants&amp;lt;/code&amp;gt;: Mainly UI-related strings (non-context-related) and some minor functional constants&lt;br /&gt;
* Composable functions for the main drop down menu&lt;br /&gt;
* Composable functions for the theme&lt;br /&gt;
* Composable functions for hyperlinks, text edit fields, headline&lt;br /&gt;
* &amp;lt;code&amp;gt;ViewNavigationRoutes&amp;lt;/code&amp;gt;: Object containing the routing information processed in main &amp;lt;code&amp;gt;app&amp;lt;/code&amp;gt; module&lt;br /&gt;
* &amp;lt;code&amp;gt;ScreenshotTestHelper&amp;lt;/code&amp;gt;: Helper class for executing the instrumented snapshot testing using the emulator.&lt;br /&gt;
&lt;br /&gt;
=== Layer structure ===&lt;br /&gt;
The code is separated into the following layers:&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
[module]/&lt;br /&gt;
&lt;br /&gt;
├── src/main/kotlin/com/laimburggasse/aquariumcontrol/[module]/data/&lt;br /&gt;
&lt;br /&gt;
│   ├── remote/                   // Folder for data retrieval functionality&lt;br /&gt;
&lt;br /&gt;
│   │   └── dto/                  // Folder for Data Transfer Objects&lt;br /&gt;
&lt;br /&gt;
│   │       └── [module][name]Import.kt&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module][name]Reader.kt      // Implementation of GET server request&lt;br /&gt;
&lt;br /&gt;
│   ├── [module][name]RepositoryImpl.kt  // Repositories (implementation of interface)&lt;br /&gt;
&lt;br /&gt;
├── src/main/kotlin/com/laimburggasse/aquariumcontrol/[module]/domain/&lt;br /&gt;
&lt;br /&gt;
│   ├── model/                    // Folder for data definition classes&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module][name].kt.    // Classes for data definition&lt;br /&gt;
&lt;br /&gt;
│   ├── repository/               // Folder containing repository interface definitions&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module][name]Repository.kt.  // Interface defining the repository, used by view model&lt;br /&gt;
&lt;br /&gt;
├── src/main/kotlin/com/laimburggasse/aquariumcontrol/[module]/presentation/&lt;br /&gt;
&lt;br /&gt;
│   ├── entry/                    // First screen of feature&lt;br /&gt;
&lt;br /&gt;
│   ├── [components]/             // Shared code between different screens&lt;br /&gt;
&lt;br /&gt;
│   ├── [screen name]/            // Folder for each screen&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module][name]Screen.kt       // Composable showing the complete screen&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module][name]Composable.kt   // Composable showing elements inside the screen&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module][name]UiEvent.kt      // Sealed interface describing the communication from the composable to the view model&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module][name]UiState.kt      // Data class describing the content to be rendered by the composable&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module][name]ViewModel.kt    // Class implementing the view model&lt;br /&gt;
&lt;br /&gt;
│   ├── navigation/               // Folder containing class and functionality related to screen navigation&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module]NavigationEvent.kt  // One sealed class per module containing the navigation intent of the user&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module]Navigator.kt  // Module-specific navigation functionality&lt;br /&gt;
&lt;br /&gt;
│&lt;br /&gt;
└── build.gradle                  // Plugins, setting, dependencies&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Screen navigation ===&lt;br /&gt;
The screen navigation is controlled by a &amp;lt;code&amp;gt;NavHostController&amp;lt;/code&amp;gt; which is instantiated in &amp;lt;code&amp;gt;MainActivity&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Each module adds its own navigation function to &amp;lt;code&amp;gt;NavHostController&amp;lt;/code&amp;gt;.&lt;br /&gt;
These functions are stored in &amp;lt;code&amp;gt;[module]/presentation/navigation/[module]Navigator.kt&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
The functions receive as parameter a specific sealed class &amp;lt;code&amp;gt;[module]NavigationEvent&amp;lt;/code&amp;gt;.&lt;br /&gt;
These sealed classes contain objects for each navigation event inside the module.&lt;br /&gt;
&lt;br /&gt;
The navigation functions are used in the following locations:&lt;br /&gt;
* inside the lambda function passed to &amp;lt;code&amp;gt;NavHost&amp;lt;/code&amp;gt; in &amp;lt;code&amp;gt;MainActivity&amp;lt;/code&amp;gt; for each screen.&lt;br /&gt;
* inside composables themselves being passed as lambda function from within &amp;lt;code&amp;gt;MainActivity&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Example workflow for adding API communication ==&lt;br /&gt;
=== Implementation ===&lt;br /&gt;
Create the model: &amp;lt;code&amp;gt;RefillState&amp;lt;/code&amp;gt; as &amp;lt;code&amp;gt;enum&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Add a reader &amp;lt;code&amp;gt;class&amp;lt;/code&amp;gt; &amp;lt;code&amp;gt;RefillStateReader&amp;lt;/code&amp;gt; which uses the injected interface (Retrofit)&lt;br /&gt;
&lt;br /&gt;
Update the &amp;lt;code&amp;gt;RefillStateApiService&amp;lt;/code&amp;gt;: Add a function to load the state via Retrofit&lt;br /&gt;
&lt;br /&gt;
Add the label for the JSON property in &amp;lt;code&amp;gt;WebApiParams&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Add the URL for the end point in &amp;lt;code&amp;gt;WebApiUrls&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Add a wrapper &amp;lt;code&amp;gt;RefillStateResponse&amp;lt;/code&amp;gt; for the model to allow Retrofit parsing the JSON&lt;br /&gt;
&lt;br /&gt;
Update the repository interface and the repository interface implementation:&lt;br /&gt;
* Add a flow that delivers a &amp;lt;code&amp;gt;DataFetchResult&amp;lt;/code&amp;gt; of the &amp;lt;code&amp;gt;RefillState&amp;lt;/code&amp;gt;&lt;br /&gt;
* Add a function that allows the view model to trigger a refresh&lt;br /&gt;
&lt;br /&gt;
Add a wrapper &amp;lt;code&amp;gt;class&amp;lt;/code&amp;gt; &amp;lt;code&amp;gt;RefillStateUiState&amp;lt;/code&amp;gt;&lt;br /&gt;
Update the view model&lt;br /&gt;
* Add a flow that delivers a &amp;lt;code&amp;gt;RefillStateUiState&amp;lt;/code&amp;gt; based on the &amp;lt;code&amp;gt;DataFetchResult&amp;lt;/code&amp;gt; from repository&lt;br /&gt;
* Handle the new UI event for refreshing&lt;br /&gt;
&lt;br /&gt;
Add composable &amp;lt;code&amp;gt;RefillStateComposable&amp;lt;/code&amp;gt; for displaying the &amp;lt;code&amp;gt;RefillState&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Update &amp;lt;code&amp;gt;RefillUiEvent&amp;lt;/code&amp;gt; with an object for the refresh&lt;br /&gt;
&lt;br /&gt;
Update &amp;lt;code&amp;gt;RefillScreen&amp;lt;/code&amp;gt;:&lt;br /&gt;
* additional parameter (&amp;lt;code&amp;gt;RefillStateUiState&amp;lt;/code&amp;gt;) in composable function and in previews&lt;br /&gt;
* Added &amp;lt;code&amp;gt;PullToRefreshBox&amp;lt;/code&amp;gt;&lt;br /&gt;
* Added parsing of &amp;lt;code&amp;gt;RefillUiState&amp;lt;/code&amp;gt; and display of either&lt;br /&gt;
:* &amp;lt;code&amp;gt;RefillStateComposable&amp;lt;/code&amp;gt; in case of success&lt;br /&gt;
:* Text-based error message in case of error&lt;br /&gt;
:* &amp;lt;code&amp;gt;CircularProgressIndicator&amp;lt;/code&amp;gt; in case of loading&lt;br /&gt;
&lt;br /&gt;
Inject &amp;lt;code&amp;gt;RefillUiState&amp;lt;/code&amp;gt; into &amp;lt;code&amp;gt;RefillScreen&amp;lt;/code&amp;gt; in the &amp;lt;code&amp;gt;MainActivity&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;RefillScreenTest&amp;lt;/code&amp;gt; as well as in previews of &amp;lt;code&amp;gt;RefillScreen&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
(Optional) Check if &amp;lt;code&amp;gt;RefillDataModule&amp;lt;/code&amp;gt; needs update (not required in this case)&lt;br /&gt;
&lt;br /&gt;
=== Testing ===&lt;br /&gt;
Update existing screenshot testing&lt;br /&gt;
&lt;br /&gt;
Add screenshot testing for new composable&lt;br /&gt;
&lt;br /&gt;
== Test strategy ==&lt;br /&gt;
The app uses different testing approaches:&lt;br /&gt;
* Compose preview screenshot testing&lt;br /&gt;
* Screenshot tests running as instrumented tests on emulator&lt;br /&gt;
* Unit tests&lt;br /&gt;
&lt;br /&gt;
=== Compose preview screenshot tests ===&lt;br /&gt;
These tests run comparably fast and device-independent.&lt;br /&gt;
&lt;br /&gt;
The compose preview screenshot tests cover portrait mode/landscape mode, light/dark mode (four variants).&lt;br /&gt;
&lt;br /&gt;
Limitations:&lt;br /&gt;
* Two tests within &amp;lt;code&amp;gt;feed&amp;lt;/code&amp;gt; module can only run in landscape mode (&amp;lt;code&amp;gt;FeedProfileScheduleScreenTest&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;FeedHistoryScreenTest&amp;lt;/code&amp;gt;) due to excessive heap memory utilisation.&lt;br /&gt;
* &amp;lt;code&amp;gt;Info&amp;lt;/code&amp;gt; screen is not tested due to &amp;lt;code&amp;gt;LibrariesContainer&amp;lt;/code&amp;gt; (dynamic content which cannot be mocked).&lt;br /&gt;
&lt;br /&gt;
The tests are located in &amp;lt;code&amp;gt;[module]/src/screenshotTest/kotlin/com/aquariumcontrol/[module]/&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
The references are located in &amp;lt;code&amp;gt;[module]/src/screenshotTestDebug/kotlin/com/aquariumcontrol/[module]/&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
The test reports are located in &amp;lt;code&amp;gt;build/reports/screenshotTest/preview/debug/&amp;lt;/code&amp;gt;. The test reports for the compose screenshot tests contain reference, actual and delta images.&lt;br /&gt;
&lt;br /&gt;
The images are generated with &amp;lt;code&amp;gt;./gradlew [module]:updateDebugScreenshotTest&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
The images are validated with &amp;lt;code&amp;gt;./gradlew [module]:validateDebugScreenshotTest&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Omitting the module will execute all tests.&lt;br /&gt;
&lt;br /&gt;
=== Instrumented tests ===&lt;br /&gt;
The tests run comparably long and are bound to an emulator device.&lt;br /&gt;
As of November 2025, a Pixel 5 API S is used for the instrumented test.&lt;br /&gt;
The instrumented tests contain&lt;br /&gt;
* screenshot testing for the complete screens (&amp;lt;code&amp;gt;ScreenNameTest&amp;lt;/code&amp;gt;)&lt;br /&gt;
* verification if the composable triggers the right event when tapping on buttons bound to navigation to another screen (&amp;lt;code&amp;gt;ScreenNameUITest&amp;lt;/code&amp;gt;)&lt;br /&gt;
&lt;br /&gt;
Limitations:&lt;br /&gt;
* &amp;lt;code&amp;gt;Info&amp;lt;/code&amp;gt; screen is not tested due to &amp;lt;code&amp;gt;LibrariesContainer&amp;lt;/code&amp;gt; (dynamic content which cannot be mocked and also does not allow execution of instrumented test).&lt;br /&gt;
&lt;br /&gt;
The test reports are located in &amp;lt;code&amp;gt;build/reports/androidTests/connected/debug/&amp;lt;/code&amp;gt;. The test reports for the compose screenshot tests do not contain any images.&lt;br /&gt;
&lt;br /&gt;
The tests are executed with &amp;lt;code&amp;gt;./gradlew [module]:connectedDebugAndroidTest&amp;lt;/code&amp;gt;.&lt;br /&gt;
Omitting the module will execute all tests.&lt;br /&gt;
&lt;br /&gt;
A switch called &amp;lt;code&amp;gt;recordMode&amp;lt;/code&amp;gt; inside each test class for screenshot testing determines if the image is generated &amp;lt;code&amp;gt;(true)&amp;lt;/code&amp;gt; or if the image is validated &amp;lt;code&amp;gt;(false)&amp;lt;/code&amp;gt;. The images are generated on the device &amp;lt;code&amp;gt;(/data/data/com.laimburggasse.aquariumcontrol.[module].test/files/screenshots_output/)&amp;lt;/code&amp;gt; and need to be downloaded to the repository.&lt;br /&gt;
&lt;br /&gt;
=== Unit tests ===&lt;br /&gt;
As of November 2025, there are no unit tests.&lt;br /&gt;
&lt;br /&gt;
== Release procedure ==&lt;br /&gt;
# Update &amp;lt;code&amp;gt;versionCode&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;versionName&amp;lt;/code&amp;gt; in &amp;lt;code&amp;gt;build.gradle.kts&amp;lt;/code&amp;gt; of &amp;lt;code&amp;gt;app&amp;lt;/code&amp;gt; module.&lt;br /&gt;
# Create a release branch (&amp;lt;code&amp;gt;release/[version]&amp;lt;/code&amp;gt;), checkout, commit and push.&lt;br /&gt;
# Change to release build variant.&lt;br /&gt;
# Build&lt;br /&gt;
# Generate a signed App bundle&lt;br /&gt;
# Upload to play store&lt;/div&gt;</summary>
		<author><name>Uwe</name></author>
	</entry>
	<entry>
		<id>http://217.79.180.177/mediawiki/index.php?title=Android_application&amp;diff=449</id>
		<title>Android application</title>
		<link rel="alternate" type="text/html" href="http://217.79.180.177/mediawiki/index.php?title=Android_application&amp;diff=449"/>
		<updated>2026-04-18T12:29:36Z</updated>

		<summary type="html">&lt;p&gt;Uwe: /* Implementation */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;The app is available in the [https://play.google.com/store/apps/details?id=com.laimburggasse.aquariumcontroller Play store].&lt;br /&gt;
== Requirements ==&lt;br /&gt;
== Architecture ==&lt;br /&gt;
The app is written completely in Kotlin using Jetpack Compose, Dagger/Hilt and aiming to implement a clean architecture.&lt;br /&gt;
=== Module structure ===&lt;br /&gt;
Besides the main app module, the app consists of the following library modules:&lt;br /&gt;
* balling&lt;br /&gt;
* common&lt;br /&gt;
* controller&lt;br /&gt;
* feed&lt;br /&gt;
* heating&lt;br /&gt;
* info&lt;br /&gt;
* overview&lt;br /&gt;
* refill&lt;br /&gt;
* schedule&lt;br /&gt;
* timedata&lt;br /&gt;
* ventilation&lt;br /&gt;
&lt;br /&gt;
The main &amp;lt;code&amp;gt;app&amp;lt;/code&amp;gt; module contains the AndroidManifest and the main activity (As of November 2025, the app only uses one activity).&lt;br /&gt;
The activity contains the navigation &amp;lt;code&amp;gt;NavHost&amp;lt;/code&amp;gt; in &amp;lt;code&amp;gt;onCreate&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
None of the other modules has a dependency to the main &amp;lt;code&amp;gt;app&amp;lt;/code&amp;gt; module.&lt;br /&gt;
&lt;br /&gt;
The &amp;lt;code&amp;gt;common&amp;lt;/code&amp;gt; module provides low-level functionalities and layout elements used in various places.&lt;br /&gt;
The following functionalities are located in &amp;lt;code&amp;gt;common&amp;lt;/code&amp;gt; module:&lt;br /&gt;
* &amp;lt;code&amp;gt;DataFetchResult&amp;lt;/code&amp;gt;: A wrapper class indicating the status of data fetching operation (&amp;lt;code&amp;gt;Success&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Error&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Loading&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Empty&amp;lt;/code&amp;gt;).&lt;br /&gt;
* &amp;lt;code&amp;gt;WebApiParams&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;WebApiPostRequest&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;WebApiPostRequestExecution&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;WebApiUrls&amp;lt;/code&amp;gt;: Classes and objects used for communication from the app to the server.&lt;br /&gt;
* &amp;lt;code&amp;gt;ControllerProfileDao&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;ControllerProfileDatabase&amp;lt;/code&amp;gt;: Classes which provide Room database functionality.&lt;br /&gt;
* Various classes used for dependency injection (Hilt)&lt;br /&gt;
* &amp;lt;code&amp;gt;SetValsSanityCheckResult&amp;lt;/code&amp;gt;: Class used for to communicate status of set values for &amp;lt;code&amp;gt;heating&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;ventilation&amp;lt;/code&amp;gt; module.&lt;br /&gt;
* &amp;lt;code&amp;gt;ControllerProfile&amp;lt;/code&amp;gt;: Class containing the profile data of the server (address, port, credentials, ...)&lt;br /&gt;
* &amp;lt;code&amp;gt;GlobalConstants&amp;lt;/code&amp;gt;: Mainly UI-related strings (non-context-related) and some minor functional constants&lt;br /&gt;
* Composable functions for the main drop down menu&lt;br /&gt;
* Composable functions for the theme&lt;br /&gt;
* Composable functions for hyperlinks, text edit fields, headline&lt;br /&gt;
* &amp;lt;code&amp;gt;ViewNavigationRoutes&amp;lt;/code&amp;gt;: Object containing the routing information processed in main &amp;lt;code&amp;gt;app&amp;lt;/code&amp;gt; module&lt;br /&gt;
* &amp;lt;code&amp;gt;ScreenshotTestHelper&amp;lt;/code&amp;gt;: Helper class for executing the instrumented snapshot testing using the emulator.&lt;br /&gt;
&lt;br /&gt;
=== Layer structure ===&lt;br /&gt;
The code is separated into the following layers:&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
[module]/&lt;br /&gt;
&lt;br /&gt;
├── src/main/kotlin/com/laimburggasse/aquariumcontrol/[module]/data/&lt;br /&gt;
&lt;br /&gt;
│   ├── remote/                   // Folder for data retrieval functionality&lt;br /&gt;
&lt;br /&gt;
│   │   └── dto/                  // Folder for Data Transfer Objects&lt;br /&gt;
&lt;br /&gt;
│   │       └── [module][name]Import.kt&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module][name]Reader.kt      // Implementation of GET server request&lt;br /&gt;
&lt;br /&gt;
│   ├── [module][name]RepositoryImpl.kt  // Repositories (implementation of interface)&lt;br /&gt;
&lt;br /&gt;
├── src/main/kotlin/com/laimburggasse/aquariumcontrol/[module]/domain/&lt;br /&gt;
&lt;br /&gt;
│   ├── model/                    // Folder for data definition classes&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module][name].kt.    // Classes for data definition&lt;br /&gt;
&lt;br /&gt;
│   ├── repository/               // Folder containing repository interface definitions&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module][name]Repository.kt.  // Interface defining the repository, used by view model&lt;br /&gt;
&lt;br /&gt;
├── src/main/kotlin/com/laimburggasse/aquariumcontrol/[module]/presentation/&lt;br /&gt;
&lt;br /&gt;
│   ├── entry/                    // First screen of feature&lt;br /&gt;
&lt;br /&gt;
│   ├── [components]/             // Shared code between different screens&lt;br /&gt;
&lt;br /&gt;
│   ├── [screen name]/            // Folder for each screen&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module][name]Screen.kt       // Composable showing the complete screen&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module][name]Composable.kt   // Composable showing elements inside the screen&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module][name]UiEvent.kt      // Sealed interface describing the communication from the composable to the view model&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module][name]UiState.kt      // Data class describing the content to be rendered by the composable&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module][name]ViewModel.kt    // Class implementing the view model&lt;br /&gt;
&lt;br /&gt;
│   ├── navigation/               // Folder containing class and functionality related to screen navigation&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module]NavigationEvent.kt  // One sealed class per module containing the navigation intent of the user&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module]Navigator.kt  // Module-specific navigation functionality&lt;br /&gt;
&lt;br /&gt;
│&lt;br /&gt;
└── build.gradle                  // Plugins, setting, dependencies&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Screen navigation ===&lt;br /&gt;
The screen navigation is controlled by a &amp;lt;code&amp;gt;NavHostController&amp;lt;/code&amp;gt; which is instantiated in &amp;lt;code&amp;gt;MainActivity&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Each module adds its own navigation function to &amp;lt;code&amp;gt;NavHostController&amp;lt;/code&amp;gt;.&lt;br /&gt;
These functions are stored in &amp;lt;code&amp;gt;[module]/presentation/navigation/[module]Navigator.kt&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
The functions receive as parameter a specific sealed class &amp;lt;code&amp;gt;[module]NavigationEvent&amp;lt;/code&amp;gt;.&lt;br /&gt;
These sealed classes contain objects for each navigation event inside the module.&lt;br /&gt;
&lt;br /&gt;
The navigation functions are used in the following locations:&lt;br /&gt;
* inside the lambda function passed to &amp;lt;code&amp;gt;NavHost&amp;lt;/code&amp;gt; in &amp;lt;code&amp;gt;MainActivity&amp;lt;/code&amp;gt; for each screen.&lt;br /&gt;
* inside composables themselves being passed as lambda function from within &amp;lt;code&amp;gt;MainActivity&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Example workflow for adding API communication ==&lt;br /&gt;
=== Implementation ===&lt;br /&gt;
Create the model: &amp;lt;code&amp;gt;RefillState&amp;lt;/code&amp;gt; as &amp;lt;code&amp;gt;enum&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Add a reader &amp;lt;code&amp;gt;class&amp;lt;/code&amp;gt; &amp;lt;code&amp;gt;RefillStateReader&amp;lt;/code&amp;gt; which uses the injected interface (Retrofit)&lt;br /&gt;
&lt;br /&gt;
Update the &amp;lt;code&amp;gt;RefillStateApiService&amp;lt;/code&amp;gt;: Add a function to load the state via Retrofit&lt;br /&gt;
&lt;br /&gt;
Add the label for the JSON property in &amp;lt;code&amp;gt;WebApiParams&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Add the URL for the end point in &amp;lt;code&amp;gt;WebApiUrls&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Add a wrapper &amp;lt;code&amp;gt;RefillStateResponse&amp;lt;/code&amp;gt; for the model to allow Retrofit parsing the JSON&lt;br /&gt;
&lt;br /&gt;
Update the repository interface and the repository interface implementation:&lt;br /&gt;
* Add a flow that delivers a &amp;lt;code&amp;gt;DataFetchResult&amp;lt;/code&amp;gt; of the &amp;lt;code&amp;gt;RefillState&amp;lt;/code&amp;gt;&lt;br /&gt;
* Add a function that allows the view model to trigger a refresh&lt;br /&gt;
&lt;br /&gt;
Add a wrapper &amp;lt;code&amp;gt;class&amp;lt;/code&amp;gt; &amp;lt;code&amp;gt;RefillStateUiState&amp;lt;/code&amp;gt;&lt;br /&gt;
Update the view model&lt;br /&gt;
* Add a flow that delivers a &amp;lt;code&amp;gt;RefillStateUiState&amp;lt;/code&amp;gt; based on the &amp;lt;code&amp;gt;DataFetchResult&amp;lt;/code&amp;gt; from repository&lt;br /&gt;
* Handle the new UI event for refreshing&lt;br /&gt;
&lt;br /&gt;
Add composable &amp;lt;code&amp;gt;RefillStateComposable&amp;lt;/code&amp;gt; for displaying the &amp;lt;code&amp;gt;RefillState&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Update &amp;lt;code&amp;gt;RefillUiEvent&amp;lt;/code&amp;gt; with an object for the refresh&lt;br /&gt;
&lt;br /&gt;
Update &amp;lt;code&amp;gt;RefillScreen&amp;lt;/code&amp;gt;:&lt;br /&gt;
* additional parameter (&amp;lt;code&amp;gt;RefillStateUiState&amp;lt;/code&amp;gt;) in composable function and in previews&lt;br /&gt;
* Added &amp;lt;code&amp;gt;PullToRefreshBox&amp;lt;/code&amp;gt;&lt;br /&gt;
* Added parsing of &amp;lt;code&amp;gt;RefillUiState&amp;lt;/code&amp;gt; and display of either&lt;br /&gt;
    :* &amp;lt;code&amp;gt;RefillStateComposable&amp;lt;/code&amp;gt; in case of success&lt;br /&gt;
    :* Text-based error message in case of error&lt;br /&gt;
    :* &amp;lt;code&amp;gt;CircularProgressIndicator&amp;lt;/code&amp;gt; in case of loading&lt;br /&gt;
&lt;br /&gt;
Inject &amp;lt;code&amp;gt;RefillUiState&amp;lt;/code&amp;gt; into &amp;lt;code&amp;gt;RefillScreen&amp;lt;/code&amp;gt; in the &amp;lt;code&amp;gt;MainActivity&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;RefillScreenTest&amp;lt;/code&amp;gt; as well as in previews of &amp;lt;code&amp;gt;RefillScreen&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
(Optional) Check if &amp;lt;code&amp;gt;RefillDataModule&amp;lt;/code&amp;gt; needs update (not required in this case)&lt;br /&gt;
&lt;br /&gt;
=== Testing ===&lt;br /&gt;
Update existing screenshot testing&lt;br /&gt;
&lt;br /&gt;
Add screenshot testing for new composable&lt;br /&gt;
&lt;br /&gt;
== Test strategy ==&lt;br /&gt;
The app uses different testing approaches:&lt;br /&gt;
* Compose preview screenshot testing&lt;br /&gt;
* Screenshot tests running as instrumented tests on emulator&lt;br /&gt;
* Unit tests&lt;br /&gt;
&lt;br /&gt;
=== Compose preview screenshot tests ===&lt;br /&gt;
These tests run comparably fast and device-independent.&lt;br /&gt;
&lt;br /&gt;
The compose preview screenshot tests cover portrait mode/landscape mode, light/dark mode (four variants).&lt;br /&gt;
&lt;br /&gt;
Limitations:&lt;br /&gt;
* Two tests within &amp;lt;code&amp;gt;feed&amp;lt;/code&amp;gt; module can only run in landscape mode (&amp;lt;code&amp;gt;FeedProfileScheduleScreenTest&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;FeedHistoryScreenTest&amp;lt;/code&amp;gt;) due to excessive heap memory utilisation.&lt;br /&gt;
* &amp;lt;code&amp;gt;Info&amp;lt;/code&amp;gt; screen is not tested due to &amp;lt;code&amp;gt;LibrariesContainer&amp;lt;/code&amp;gt; (dynamic content which cannot be mocked).&lt;br /&gt;
&lt;br /&gt;
The tests are located in &amp;lt;code&amp;gt;[module]/src/screenshotTest/kotlin/com/aquariumcontrol/[module]/&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
The references are located in &amp;lt;code&amp;gt;[module]/src/screenshotTestDebug/kotlin/com/aquariumcontrol/[module]/&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
The test reports are located in &amp;lt;code&amp;gt;build/reports/screenshotTest/preview/debug/&amp;lt;/code&amp;gt;. The test reports for the compose screenshot tests contain reference, actual and delta images.&lt;br /&gt;
&lt;br /&gt;
The images are generated with &amp;lt;code&amp;gt;./gradlew [module]:updateDebugScreenshotTest&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
The images are validated with &amp;lt;code&amp;gt;./gradlew [module]:validateDebugScreenshotTest&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Omitting the module will execute all tests.&lt;br /&gt;
&lt;br /&gt;
=== Instrumented tests ===&lt;br /&gt;
The tests run comparably long and are bound to an emulator device.&lt;br /&gt;
As of November 2025, a Pixel 5 API S is used for the instrumented test.&lt;br /&gt;
The instrumented tests contain&lt;br /&gt;
* screenshot testing for the complete screens (&amp;lt;code&amp;gt;ScreenNameTest&amp;lt;/code&amp;gt;)&lt;br /&gt;
* verification if the composable triggers the right event when tapping on buttons bound to navigation to another screen (&amp;lt;code&amp;gt;ScreenNameUITest&amp;lt;/code&amp;gt;)&lt;br /&gt;
&lt;br /&gt;
Limitations:&lt;br /&gt;
* &amp;lt;code&amp;gt;Info&amp;lt;/code&amp;gt; screen is not tested due to &amp;lt;code&amp;gt;LibrariesContainer&amp;lt;/code&amp;gt; (dynamic content which cannot be mocked and also does not allow execution of instrumented test).&lt;br /&gt;
&lt;br /&gt;
The test reports are located in &amp;lt;code&amp;gt;build/reports/androidTests/connected/debug/&amp;lt;/code&amp;gt;. The test reports for the compose screenshot tests do not contain any images.&lt;br /&gt;
&lt;br /&gt;
The tests are executed with &amp;lt;code&amp;gt;./gradlew [module]:connectedDebugAndroidTest&amp;lt;/code&amp;gt;.&lt;br /&gt;
Omitting the module will execute all tests.&lt;br /&gt;
&lt;br /&gt;
A switch called &amp;lt;code&amp;gt;recordMode&amp;lt;/code&amp;gt; inside each test class for screenshot testing determines if the image is generated &amp;lt;code&amp;gt;(true)&amp;lt;/code&amp;gt; or if the image is validated &amp;lt;code&amp;gt;(false)&amp;lt;/code&amp;gt;. The images are generated on the device &amp;lt;code&amp;gt;(/data/data/com.laimburggasse.aquariumcontrol.[module].test/files/screenshots_output/)&amp;lt;/code&amp;gt; and need to be downloaded to the repository.&lt;br /&gt;
&lt;br /&gt;
=== Unit tests ===&lt;br /&gt;
As of November 2025, there are no unit tests.&lt;br /&gt;
&lt;br /&gt;
== Release procedure ==&lt;br /&gt;
# Update &amp;lt;code&amp;gt;versionCode&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;versionName&amp;lt;/code&amp;gt; in &amp;lt;code&amp;gt;build.gradle.kts&amp;lt;/code&amp;gt; of &amp;lt;code&amp;gt;app&amp;lt;/code&amp;gt; module.&lt;br /&gt;
# Create a release branch (&amp;lt;code&amp;gt;release/[version]&amp;lt;/code&amp;gt;), checkout, commit and push.&lt;br /&gt;
# Change to release build variant.&lt;br /&gt;
# Build&lt;br /&gt;
# Generate a signed App bundle&lt;br /&gt;
# Upload to play store&lt;/div&gt;</summary>
		<author><name>Uwe</name></author>
	</entry>
	<entry>
		<id>http://217.79.180.177/mediawiki/index.php?title=Android_application&amp;diff=448</id>
		<title>Android application</title>
		<link rel="alternate" type="text/html" href="http://217.79.180.177/mediawiki/index.php?title=Android_application&amp;diff=448"/>
		<updated>2026-04-18T12:28:39Z</updated>

		<summary type="html">&lt;p&gt;Uwe: /* Example workflow for adding API communication */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;The app is available in the [https://play.google.com/store/apps/details?id=com.laimburggasse.aquariumcontroller Play store].&lt;br /&gt;
== Requirements ==&lt;br /&gt;
== Architecture ==&lt;br /&gt;
The app is written completely in Kotlin using Jetpack Compose, Dagger/Hilt and aiming to implement a clean architecture.&lt;br /&gt;
=== Module structure ===&lt;br /&gt;
Besides the main app module, the app consists of the following library modules:&lt;br /&gt;
* balling&lt;br /&gt;
* common&lt;br /&gt;
* controller&lt;br /&gt;
* feed&lt;br /&gt;
* heating&lt;br /&gt;
* info&lt;br /&gt;
* overview&lt;br /&gt;
* refill&lt;br /&gt;
* schedule&lt;br /&gt;
* timedata&lt;br /&gt;
* ventilation&lt;br /&gt;
&lt;br /&gt;
The main &amp;lt;code&amp;gt;app&amp;lt;/code&amp;gt; module contains the AndroidManifest and the main activity (As of November 2025, the app only uses one activity).&lt;br /&gt;
The activity contains the navigation &amp;lt;code&amp;gt;NavHost&amp;lt;/code&amp;gt; in &amp;lt;code&amp;gt;onCreate&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
None of the other modules has a dependency to the main &amp;lt;code&amp;gt;app&amp;lt;/code&amp;gt; module.&lt;br /&gt;
&lt;br /&gt;
The &amp;lt;code&amp;gt;common&amp;lt;/code&amp;gt; module provides low-level functionalities and layout elements used in various places.&lt;br /&gt;
The following functionalities are located in &amp;lt;code&amp;gt;common&amp;lt;/code&amp;gt; module:&lt;br /&gt;
* &amp;lt;code&amp;gt;DataFetchResult&amp;lt;/code&amp;gt;: A wrapper class indicating the status of data fetching operation (&amp;lt;code&amp;gt;Success&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Error&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Loading&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Empty&amp;lt;/code&amp;gt;).&lt;br /&gt;
* &amp;lt;code&amp;gt;WebApiParams&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;WebApiPostRequest&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;WebApiPostRequestExecution&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;WebApiUrls&amp;lt;/code&amp;gt;: Classes and objects used for communication from the app to the server.&lt;br /&gt;
* &amp;lt;code&amp;gt;ControllerProfileDao&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;ControllerProfileDatabase&amp;lt;/code&amp;gt;: Classes which provide Room database functionality.&lt;br /&gt;
* Various classes used for dependency injection (Hilt)&lt;br /&gt;
* &amp;lt;code&amp;gt;SetValsSanityCheckResult&amp;lt;/code&amp;gt;: Class used for to communicate status of set values for &amp;lt;code&amp;gt;heating&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;ventilation&amp;lt;/code&amp;gt; module.&lt;br /&gt;
* &amp;lt;code&amp;gt;ControllerProfile&amp;lt;/code&amp;gt;: Class containing the profile data of the server (address, port, credentials, ...)&lt;br /&gt;
* &amp;lt;code&amp;gt;GlobalConstants&amp;lt;/code&amp;gt;: Mainly UI-related strings (non-context-related) and some minor functional constants&lt;br /&gt;
* Composable functions for the main drop down menu&lt;br /&gt;
* Composable functions for the theme&lt;br /&gt;
* Composable functions for hyperlinks, text edit fields, headline&lt;br /&gt;
* &amp;lt;code&amp;gt;ViewNavigationRoutes&amp;lt;/code&amp;gt;: Object containing the routing information processed in main &amp;lt;code&amp;gt;app&amp;lt;/code&amp;gt; module&lt;br /&gt;
* &amp;lt;code&amp;gt;ScreenshotTestHelper&amp;lt;/code&amp;gt;: Helper class for executing the instrumented snapshot testing using the emulator.&lt;br /&gt;
&lt;br /&gt;
=== Layer structure ===&lt;br /&gt;
The code is separated into the following layers:&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
[module]/&lt;br /&gt;
&lt;br /&gt;
├── src/main/kotlin/com/laimburggasse/aquariumcontrol/[module]/data/&lt;br /&gt;
&lt;br /&gt;
│   ├── remote/                   // Folder for data retrieval functionality&lt;br /&gt;
&lt;br /&gt;
│   │   └── dto/                  // Folder for Data Transfer Objects&lt;br /&gt;
&lt;br /&gt;
│   │       └── [module][name]Import.kt&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module][name]Reader.kt      // Implementation of GET server request&lt;br /&gt;
&lt;br /&gt;
│   ├── [module][name]RepositoryImpl.kt  // Repositories (implementation of interface)&lt;br /&gt;
&lt;br /&gt;
├── src/main/kotlin/com/laimburggasse/aquariumcontrol/[module]/domain/&lt;br /&gt;
&lt;br /&gt;
│   ├── model/                    // Folder for data definition classes&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module][name].kt.    // Classes for data definition&lt;br /&gt;
&lt;br /&gt;
│   ├── repository/               // Folder containing repository interface definitions&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module][name]Repository.kt.  // Interface defining the repository, used by view model&lt;br /&gt;
&lt;br /&gt;
├── src/main/kotlin/com/laimburggasse/aquariumcontrol/[module]/presentation/&lt;br /&gt;
&lt;br /&gt;
│   ├── entry/                    // First screen of feature&lt;br /&gt;
&lt;br /&gt;
│   ├── [components]/             // Shared code between different screens&lt;br /&gt;
&lt;br /&gt;
│   ├── [screen name]/            // Folder for each screen&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module][name]Screen.kt       // Composable showing the complete screen&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module][name]Composable.kt   // Composable showing elements inside the screen&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module][name]UiEvent.kt      // Sealed interface describing the communication from the composable to the view model&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module][name]UiState.kt      // Data class describing the content to be rendered by the composable&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module][name]ViewModel.kt    // Class implementing the view model&lt;br /&gt;
&lt;br /&gt;
│   ├── navigation/               // Folder containing class and functionality related to screen navigation&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module]NavigationEvent.kt  // One sealed class per module containing the navigation intent of the user&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module]Navigator.kt  // Module-specific navigation functionality&lt;br /&gt;
&lt;br /&gt;
│&lt;br /&gt;
└── build.gradle                  // Plugins, setting, dependencies&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Screen navigation ===&lt;br /&gt;
The screen navigation is controlled by a &amp;lt;code&amp;gt;NavHostController&amp;lt;/code&amp;gt; which is instantiated in &amp;lt;code&amp;gt;MainActivity&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Each module adds its own navigation function to &amp;lt;code&amp;gt;NavHostController&amp;lt;/code&amp;gt;.&lt;br /&gt;
These functions are stored in &amp;lt;code&amp;gt;[module]/presentation/navigation/[module]Navigator.kt&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
The functions receive as parameter a specific sealed class &amp;lt;code&amp;gt;[module]NavigationEvent&amp;lt;/code&amp;gt;.&lt;br /&gt;
These sealed classes contain objects for each navigation event inside the module.&lt;br /&gt;
&lt;br /&gt;
The navigation functions are used in the following locations:&lt;br /&gt;
* inside the lambda function passed to &amp;lt;code&amp;gt;NavHost&amp;lt;/code&amp;gt; in &amp;lt;code&amp;gt;MainActivity&amp;lt;/code&amp;gt; for each screen.&lt;br /&gt;
* inside composables themselves being passed as lambda function from within &amp;lt;code&amp;gt;MainActivity&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Example workflow for adding API communication ==&lt;br /&gt;
=== Implementation ===&lt;br /&gt;
Create the model: &amp;lt;code&amp;gt;RefillState&amp;lt;/code&amp;gt; as &amp;lt;code&amp;gt;enum&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Add a reader &amp;lt;code&amp;gt;class&amp;lt;/code&amp;gt; &amp;lt;code&amp;gt;RefillStateReader&amp;lt;/code&amp;gt; which uses the injected interface (Retrofit)&lt;br /&gt;
&lt;br /&gt;
Update the &amp;lt;code&amp;gt;RefillStateApiService&amp;lt;/code&amp;gt;: Add a function to load the state via Retrofit&lt;br /&gt;
&lt;br /&gt;
Add the label for the JSON property in &amp;lt;code&amp;gt;WebApiParams&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Add the URL for the end point in &amp;lt;code&amp;gt;WebApiUrls&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Add a wrapper &amp;lt;code&amp;gt;RefillStateResponse&amp;lt;/code&amp;gt; for the model to allow Retrofit parsing the JSON&lt;br /&gt;
&lt;br /&gt;
Update the repository interface and the repository interface implementation:&lt;br /&gt;
* Add a flow that delivers a &amp;lt;code&amp;gt;DataFetchResult&amp;lt;/code&amp;gt; of the &amp;lt;code&amp;gt;RefillState&amp;lt;/code&amp;gt;&lt;br /&gt;
* Add a function that allows the view model to trigger a refresh&lt;br /&gt;
&lt;br /&gt;
Add a wrapper &amp;lt;code&amp;gt;class&amp;lt;/code&amp;gt; &amp;lt;code&amp;gt;RefillStateUiState&amp;lt;/code&amp;gt;&lt;br /&gt;
Update the view model&lt;br /&gt;
* Add a flow that delivers a &amp;lt;code&amp;gt;RefillStateUiState&amp;lt;/code&amp;gt; based on the &amp;lt;code&amp;gt;DataFetchResult&amp;lt;/code&amp;gt; from repository&lt;br /&gt;
* Handle the new UI event for refreshing&lt;br /&gt;
&lt;br /&gt;
Add composable &amp;lt;code&amp;gt;RefillStateComposable&amp;lt;/code&amp;gt; for displaying the &amp;lt;code&amp;gt;RefillState&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Update &amp;lt;code&amp;gt;RefillUiEvent&amp;lt;/code&amp;gt; with an object for the refresh&lt;br /&gt;
&lt;br /&gt;
Update &amp;lt;code&amp;gt;RefillScreen&amp;lt;/code&amp;gt;:&lt;br /&gt;
* additional parameter (&amp;lt;code&amp;gt;RefillStateUiState&amp;lt;/code&amp;gt;) in composable function and in previews&lt;br /&gt;
* Added &amp;lt;code&amp;gt;PullToRefreshBox&amp;lt;/code&amp;gt;&lt;br /&gt;
* Added parsing of &amp;lt;code&amp;gt;RefillUiState&amp;lt;/code&amp;gt; and display of either&lt;br /&gt;
    ** &amp;lt;code&amp;gt;RefillStateComposable&amp;lt;/code&amp;gt; in case of success&lt;br /&gt;
    ** Text-based error message in case of error&lt;br /&gt;
    ** &amp;lt;code&amp;gt;CircularProgressIndicator&amp;lt;/code&amp;gt; in case of loading&lt;br /&gt;
&lt;br /&gt;
Inject &amp;lt;code&amp;gt;RefillUiState&amp;lt;/code&amp;gt; into &amp;lt;code&amp;gt;RefillScreen&amp;lt;/code&amp;gt; in the &amp;lt;code&amp;gt;MainActivity&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;RefillScreenTest&amp;lt;/code&amp;gt; as well as in previews of &amp;lt;code&amp;gt;RefillScreen&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
(Optional) Check if &amp;lt;code&amp;gt;RefillDataModule&amp;lt;/code&amp;gt; needs update (not required in this case)&lt;br /&gt;
&lt;br /&gt;
=== Testing ===&lt;br /&gt;
Update existing screenshot testing&lt;br /&gt;
&lt;br /&gt;
Add screenshot testing for new composable&lt;br /&gt;
&lt;br /&gt;
== Test strategy ==&lt;br /&gt;
The app uses different testing approaches:&lt;br /&gt;
* Compose preview screenshot testing&lt;br /&gt;
* Screenshot tests running as instrumented tests on emulator&lt;br /&gt;
* Unit tests&lt;br /&gt;
&lt;br /&gt;
=== Compose preview screenshot tests ===&lt;br /&gt;
These tests run comparably fast and device-independent.&lt;br /&gt;
&lt;br /&gt;
The compose preview screenshot tests cover portrait mode/landscape mode, light/dark mode (four variants).&lt;br /&gt;
&lt;br /&gt;
Limitations:&lt;br /&gt;
* Two tests within &amp;lt;code&amp;gt;feed&amp;lt;/code&amp;gt; module can only run in landscape mode (&amp;lt;code&amp;gt;FeedProfileScheduleScreenTest&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;FeedHistoryScreenTest&amp;lt;/code&amp;gt;) due to excessive heap memory utilisation.&lt;br /&gt;
* &amp;lt;code&amp;gt;Info&amp;lt;/code&amp;gt; screen is not tested due to &amp;lt;code&amp;gt;LibrariesContainer&amp;lt;/code&amp;gt; (dynamic content which cannot be mocked).&lt;br /&gt;
&lt;br /&gt;
The tests are located in &amp;lt;code&amp;gt;[module]/src/screenshotTest/kotlin/com/aquariumcontrol/[module]/&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
The references are located in &amp;lt;code&amp;gt;[module]/src/screenshotTestDebug/kotlin/com/aquariumcontrol/[module]/&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
The test reports are located in &amp;lt;code&amp;gt;build/reports/screenshotTest/preview/debug/&amp;lt;/code&amp;gt;. The test reports for the compose screenshot tests contain reference, actual and delta images.&lt;br /&gt;
&lt;br /&gt;
The images are generated with &amp;lt;code&amp;gt;./gradlew [module]:updateDebugScreenshotTest&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
The images are validated with &amp;lt;code&amp;gt;./gradlew [module]:validateDebugScreenshotTest&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Omitting the module will execute all tests.&lt;br /&gt;
&lt;br /&gt;
=== Instrumented tests ===&lt;br /&gt;
The tests run comparably long and are bound to an emulator device.&lt;br /&gt;
As of November 2025, a Pixel 5 API S is used for the instrumented test.&lt;br /&gt;
The instrumented tests contain&lt;br /&gt;
* screenshot testing for the complete screens (&amp;lt;code&amp;gt;ScreenNameTest&amp;lt;/code&amp;gt;)&lt;br /&gt;
* verification if the composable triggers the right event when tapping on buttons bound to navigation to another screen (&amp;lt;code&amp;gt;ScreenNameUITest&amp;lt;/code&amp;gt;)&lt;br /&gt;
&lt;br /&gt;
Limitations:&lt;br /&gt;
* &amp;lt;code&amp;gt;Info&amp;lt;/code&amp;gt; screen is not tested due to &amp;lt;code&amp;gt;LibrariesContainer&amp;lt;/code&amp;gt; (dynamic content which cannot be mocked and also does not allow execution of instrumented test).&lt;br /&gt;
&lt;br /&gt;
The test reports are located in &amp;lt;code&amp;gt;build/reports/androidTests/connected/debug/&amp;lt;/code&amp;gt;. The test reports for the compose screenshot tests do not contain any images.&lt;br /&gt;
&lt;br /&gt;
The tests are executed with &amp;lt;code&amp;gt;./gradlew [module]:connectedDebugAndroidTest&amp;lt;/code&amp;gt;.&lt;br /&gt;
Omitting the module will execute all tests.&lt;br /&gt;
&lt;br /&gt;
A switch called &amp;lt;code&amp;gt;recordMode&amp;lt;/code&amp;gt; inside each test class for screenshot testing determines if the image is generated &amp;lt;code&amp;gt;(true)&amp;lt;/code&amp;gt; or if the image is validated &amp;lt;code&amp;gt;(false)&amp;lt;/code&amp;gt;. The images are generated on the device &amp;lt;code&amp;gt;(/data/data/com.laimburggasse.aquariumcontrol.[module].test/files/screenshots_output/)&amp;lt;/code&amp;gt; and need to be downloaded to the repository.&lt;br /&gt;
&lt;br /&gt;
=== Unit tests ===&lt;br /&gt;
As of November 2025, there are no unit tests.&lt;br /&gt;
&lt;br /&gt;
== Release procedure ==&lt;br /&gt;
# Update &amp;lt;code&amp;gt;versionCode&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;versionName&amp;lt;/code&amp;gt; in &amp;lt;code&amp;gt;build.gradle.kts&amp;lt;/code&amp;gt; of &amp;lt;code&amp;gt;app&amp;lt;/code&amp;gt; module.&lt;br /&gt;
# Create a release branch (&amp;lt;code&amp;gt;release/[version]&amp;lt;/code&amp;gt;), checkout, commit and push.&lt;br /&gt;
# Change to release build variant.&lt;br /&gt;
# Build&lt;br /&gt;
# Generate a signed App bundle&lt;br /&gt;
# Upload to play store&lt;/div&gt;</summary>
		<author><name>Uwe</name></author>
	</entry>
	<entry>
		<id>http://217.79.180.177/mediawiki/index.php?title=Android_application&amp;diff=447</id>
		<title>Android application</title>
		<link rel="alternate" type="text/html" href="http://217.79.180.177/mediawiki/index.php?title=Android_application&amp;diff=447"/>
		<updated>2026-04-18T11:08:35Z</updated>

		<summary type="html">&lt;p&gt;Uwe: /* Screen navigation */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;The app is available in the [https://play.google.com/store/apps/details?id=com.laimburggasse.aquariumcontroller Play store].&lt;br /&gt;
== Requirements ==&lt;br /&gt;
== Architecture ==&lt;br /&gt;
The app is written completely in Kotlin using Jetpack Compose, Dagger/Hilt and aiming to implement a clean architecture.&lt;br /&gt;
=== Module structure ===&lt;br /&gt;
Besides the main app module, the app consists of the following library modules:&lt;br /&gt;
* balling&lt;br /&gt;
* common&lt;br /&gt;
* controller&lt;br /&gt;
* feed&lt;br /&gt;
* heating&lt;br /&gt;
* info&lt;br /&gt;
* overview&lt;br /&gt;
* refill&lt;br /&gt;
* schedule&lt;br /&gt;
* timedata&lt;br /&gt;
* ventilation&lt;br /&gt;
&lt;br /&gt;
The main &amp;lt;code&amp;gt;app&amp;lt;/code&amp;gt; module contains the AndroidManifest and the main activity (As of November 2025, the app only uses one activity).&lt;br /&gt;
The activity contains the navigation &amp;lt;code&amp;gt;NavHost&amp;lt;/code&amp;gt; in &amp;lt;code&amp;gt;onCreate&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
None of the other modules has a dependency to the main &amp;lt;code&amp;gt;app&amp;lt;/code&amp;gt; module.&lt;br /&gt;
&lt;br /&gt;
The &amp;lt;code&amp;gt;common&amp;lt;/code&amp;gt; module provides low-level functionalities and layout elements used in various places.&lt;br /&gt;
The following functionalities are located in &amp;lt;code&amp;gt;common&amp;lt;/code&amp;gt; module:&lt;br /&gt;
* &amp;lt;code&amp;gt;DataFetchResult&amp;lt;/code&amp;gt;: A wrapper class indicating the status of data fetching operation (&amp;lt;code&amp;gt;Success&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Error&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Loading&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Empty&amp;lt;/code&amp;gt;).&lt;br /&gt;
* &amp;lt;code&amp;gt;WebApiParams&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;WebApiPostRequest&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;WebApiPostRequestExecution&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;WebApiUrls&amp;lt;/code&amp;gt;: Classes and objects used for communication from the app to the server.&lt;br /&gt;
* &amp;lt;code&amp;gt;ControllerProfileDao&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;ControllerProfileDatabase&amp;lt;/code&amp;gt;: Classes which provide Room database functionality.&lt;br /&gt;
* Various classes used for dependency injection (Hilt)&lt;br /&gt;
* &amp;lt;code&amp;gt;SetValsSanityCheckResult&amp;lt;/code&amp;gt;: Class used for to communicate status of set values for &amp;lt;code&amp;gt;heating&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;ventilation&amp;lt;/code&amp;gt; module.&lt;br /&gt;
* &amp;lt;code&amp;gt;ControllerProfile&amp;lt;/code&amp;gt;: Class containing the profile data of the server (address, port, credentials, ...)&lt;br /&gt;
* &amp;lt;code&amp;gt;GlobalConstants&amp;lt;/code&amp;gt;: Mainly UI-related strings (non-context-related) and some minor functional constants&lt;br /&gt;
* Composable functions for the main drop down menu&lt;br /&gt;
* Composable functions for the theme&lt;br /&gt;
* Composable functions for hyperlinks, text edit fields, headline&lt;br /&gt;
* &amp;lt;code&amp;gt;ViewNavigationRoutes&amp;lt;/code&amp;gt;: Object containing the routing information processed in main &amp;lt;code&amp;gt;app&amp;lt;/code&amp;gt; module&lt;br /&gt;
* &amp;lt;code&amp;gt;ScreenshotTestHelper&amp;lt;/code&amp;gt;: Helper class for executing the instrumented snapshot testing using the emulator.&lt;br /&gt;
&lt;br /&gt;
=== Layer structure ===&lt;br /&gt;
The code is separated into the following layers:&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
[module]/&lt;br /&gt;
&lt;br /&gt;
├── src/main/kotlin/com/laimburggasse/aquariumcontrol/[module]/data/&lt;br /&gt;
&lt;br /&gt;
│   ├── remote/                   // Folder for data retrieval functionality&lt;br /&gt;
&lt;br /&gt;
│   │   └── dto/                  // Folder for Data Transfer Objects&lt;br /&gt;
&lt;br /&gt;
│   │       └── [module][name]Import.kt&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module][name]Reader.kt      // Implementation of GET server request&lt;br /&gt;
&lt;br /&gt;
│   ├── [module][name]RepositoryImpl.kt  // Repositories (implementation of interface)&lt;br /&gt;
&lt;br /&gt;
├── src/main/kotlin/com/laimburggasse/aquariumcontrol/[module]/domain/&lt;br /&gt;
&lt;br /&gt;
│   ├── model/                    // Folder for data definition classes&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module][name].kt.    // Classes for data definition&lt;br /&gt;
&lt;br /&gt;
│   ├── repository/               // Folder containing repository interface definitions&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module][name]Repository.kt.  // Interface defining the repository, used by view model&lt;br /&gt;
&lt;br /&gt;
├── src/main/kotlin/com/laimburggasse/aquariumcontrol/[module]/presentation/&lt;br /&gt;
&lt;br /&gt;
│   ├── entry/                    // First screen of feature&lt;br /&gt;
&lt;br /&gt;
│   ├── [components]/             // Shared code between different screens&lt;br /&gt;
&lt;br /&gt;
│   ├── [screen name]/            // Folder for each screen&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module][name]Screen.kt       // Composable showing the complete screen&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module][name]Composable.kt   // Composable showing elements inside the screen&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module][name]UiEvent.kt      // Sealed interface describing the communication from the composable to the view model&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module][name]UiState.kt      // Data class describing the content to be rendered by the composable&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module][name]ViewModel.kt    // Class implementing the view model&lt;br /&gt;
&lt;br /&gt;
│   ├── navigation/               // Folder containing class and functionality related to screen navigation&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module]NavigationEvent.kt  // One sealed class per module containing the navigation intent of the user&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module]Navigator.kt  // Module-specific navigation functionality&lt;br /&gt;
&lt;br /&gt;
│&lt;br /&gt;
└── build.gradle                  // Plugins, setting, dependencies&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Screen navigation ===&lt;br /&gt;
The screen navigation is controlled by a &amp;lt;code&amp;gt;NavHostController&amp;lt;/code&amp;gt; which is instantiated in &amp;lt;code&amp;gt;MainActivity&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Each module adds its own navigation function to &amp;lt;code&amp;gt;NavHostController&amp;lt;/code&amp;gt;.&lt;br /&gt;
These functions are stored in &amp;lt;code&amp;gt;[module]/presentation/navigation/[module]Navigator.kt&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
The functions receive as parameter a specific sealed class &amp;lt;code&amp;gt;[module]NavigationEvent&amp;lt;/code&amp;gt;.&lt;br /&gt;
These sealed classes contain objects for each navigation event inside the module.&lt;br /&gt;
&lt;br /&gt;
The navigation functions are used in the following locations:&lt;br /&gt;
* inside the lambda function passed to &amp;lt;code&amp;gt;NavHost&amp;lt;/code&amp;gt; in &amp;lt;code&amp;gt;MainActivity&amp;lt;/code&amp;gt; for each screen.&lt;br /&gt;
* inside composables themselves being passed as lambda function from within &amp;lt;code&amp;gt;MainActivity&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Example workflow for adding API communication ==&lt;br /&gt;
=== Implementation ===&lt;br /&gt;
Create the model: &amp;lt;code&amp;gt;RefillState&amp;lt;/code&amp;gt; as &amp;lt;code&amp;gt;enum&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Add a reader &amp;lt;code&amp;gt;class&amp;lt;/code&amp;gt; &amp;lt;code&amp;gt;RefillStateReader&amp;lt;/code&amp;gt; which uses the injected interface (Retrofit)&lt;br /&gt;
&lt;br /&gt;
Update the &amp;lt;code&amp;gt;RefillStateApiService&amp;lt;/code&amp;gt;: Add a function to load the state via Retrofit&lt;br /&gt;
&lt;br /&gt;
Add the label for the JSON property in &amp;lt;code&amp;gt;WebApiParams&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Add the URL for the end point in &amp;lt;code&amp;gt;WebApiUrls&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Add a wrapper &amp;lt;code&amp;gt;RefillStateResponse&amp;lt;/code&amp;gt; for the model to allow Retrofit parsing the JSON&lt;br /&gt;
&lt;br /&gt;
Update the repository interface and the repository interface implementation:&lt;br /&gt;
* Add a flow that delivers a &amp;lt;code&amp;gt;DataFetchResult&amp;lt;/code&amp;gt; of the &amp;lt;code&amp;gt;RefillState&amp;lt;/code&amp;gt;&lt;br /&gt;
* Add a function that allows the view model to trigger a refresh&lt;br /&gt;
&lt;br /&gt;
Add a wrapper &amp;lt;code&amp;gt;class&amp;lt;/code&amp;gt; &amp;lt;code&amp;gt;RefillStateUiState&amp;lt;/code&amp;gt;&lt;br /&gt;
Update the view model&lt;br /&gt;
* Add a flow that delivers a &amp;lt;code&amp;gt;RefillStateUiState&amp;lt;/code&amp;gt; based on the &amp;lt;code&amp;gt;DataFetchResult&amp;lt;/code&amp;gt; from repository&lt;br /&gt;
* Handle the new UI event for refreshing&lt;br /&gt;
&lt;br /&gt;
Add composable &amp;lt;code&amp;gt;RefillStateComposable&amp;lt;/code&amp;gt; for displaying the &amp;lt;code&amp;gt;RefillState&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Update &amp;lt;code&amp;gt;RefillUiEvent&amp;lt;/code&amp;gt; with an object for the refresh&lt;br /&gt;
&lt;br /&gt;
Update &amp;lt;code&amp;gt;RefillScreen&amp;lt;/code&amp;gt;:&lt;br /&gt;
* additional parameter (&amp;lt;code&amp;gt;RefillStateUiState&amp;lt;/code&amp;gt;) in composable function and in previews&lt;br /&gt;
* Added &amp;lt;code&amp;gt;PullToRefreshBox&amp;lt;/code&amp;gt;&lt;br /&gt;
* Added parsing of &amp;lt;code&amp;gt;RefillUiState&amp;lt;/code&amp;gt; and display of either&lt;br /&gt;
    * &amp;lt;code&amp;gt;RefillStateComposable&amp;lt;/code&amp;gt; in case of success&lt;br /&gt;
    * Text-based error message in case of error&lt;br /&gt;
    * &amp;lt;code&amp;gt;CircularProgressIndicator&amp;lt;/code&amp;gt; in case of loading&lt;br /&gt;
&lt;br /&gt;
Inject &amp;lt;code&amp;gt;RefillUiState&amp;lt;/code&amp;gt; into &amp;lt;code&amp;gt;RefillScreen&amp;lt;/code&amp;gt; in the &amp;lt;code&amp;gt;MainActivity&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;RefillScreenTest&amp;lt;/code&amp;gt; as well as in previews of &amp;lt;code&amp;gt;RefillScreen&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
(Optional) Check if &amp;lt;code&amp;gt;RefillDataModule&amp;lt;/code&amp;gt; needs update (not required in this case)&lt;br /&gt;
&lt;br /&gt;
=== Testing ===&lt;br /&gt;
Update existing screenshot testing&lt;br /&gt;
&lt;br /&gt;
Add screenshot testing for new composable&lt;br /&gt;
&lt;br /&gt;
== Test strategy ==&lt;br /&gt;
The app uses different testing approaches:&lt;br /&gt;
* Compose preview screenshot testing&lt;br /&gt;
* Screenshot tests running as instrumented tests on emulator&lt;br /&gt;
* Unit tests&lt;br /&gt;
&lt;br /&gt;
=== Compose preview screenshot tests ===&lt;br /&gt;
These tests run comparably fast and device-independent.&lt;br /&gt;
&lt;br /&gt;
The compose preview screenshot tests cover portrait mode/landscape mode, light/dark mode (four variants).&lt;br /&gt;
&lt;br /&gt;
Limitations:&lt;br /&gt;
* Two tests within &amp;lt;code&amp;gt;feed&amp;lt;/code&amp;gt; module can only run in landscape mode (&amp;lt;code&amp;gt;FeedProfileScheduleScreenTest&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;FeedHistoryScreenTest&amp;lt;/code&amp;gt;) due to excessive heap memory utilisation.&lt;br /&gt;
* &amp;lt;code&amp;gt;Info&amp;lt;/code&amp;gt; screen is not tested due to &amp;lt;code&amp;gt;LibrariesContainer&amp;lt;/code&amp;gt; (dynamic content which cannot be mocked).&lt;br /&gt;
&lt;br /&gt;
The tests are located in &amp;lt;code&amp;gt;[module]/src/screenshotTest/kotlin/com/aquariumcontrol/[module]/&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
The references are located in &amp;lt;code&amp;gt;[module]/src/screenshotTestDebug/kotlin/com/aquariumcontrol/[module]/&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
The test reports are located in &amp;lt;code&amp;gt;build/reports/screenshotTest/preview/debug/&amp;lt;/code&amp;gt;. The test reports for the compose screenshot tests contain reference, actual and delta images.&lt;br /&gt;
&lt;br /&gt;
The images are generated with &amp;lt;code&amp;gt;./gradlew [module]:updateDebugScreenshotTest&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
The images are validated with &amp;lt;code&amp;gt;./gradlew [module]:validateDebugScreenshotTest&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Omitting the module will execute all tests.&lt;br /&gt;
&lt;br /&gt;
=== Instrumented tests ===&lt;br /&gt;
The tests run comparably long and are bound to an emulator device.&lt;br /&gt;
As of November 2025, a Pixel 5 API S is used for the instrumented test.&lt;br /&gt;
The instrumented tests contain&lt;br /&gt;
* screenshot testing for the complete screens (&amp;lt;code&amp;gt;ScreenNameTest&amp;lt;/code&amp;gt;)&lt;br /&gt;
* verification if the composable triggers the right event when tapping on buttons bound to navigation to another screen (&amp;lt;code&amp;gt;ScreenNameUITest&amp;lt;/code&amp;gt;)&lt;br /&gt;
&lt;br /&gt;
Limitations:&lt;br /&gt;
* &amp;lt;code&amp;gt;Info&amp;lt;/code&amp;gt; screen is not tested due to &amp;lt;code&amp;gt;LibrariesContainer&amp;lt;/code&amp;gt; (dynamic content which cannot be mocked and also does not allow execution of instrumented test).&lt;br /&gt;
&lt;br /&gt;
The test reports are located in &amp;lt;code&amp;gt;build/reports/androidTests/connected/debug/&amp;lt;/code&amp;gt;. The test reports for the compose screenshot tests do not contain any images.&lt;br /&gt;
&lt;br /&gt;
The tests are executed with &amp;lt;code&amp;gt;./gradlew [module]:connectedDebugAndroidTest&amp;lt;/code&amp;gt;.&lt;br /&gt;
Omitting the module will execute all tests.&lt;br /&gt;
&lt;br /&gt;
A switch called &amp;lt;code&amp;gt;recordMode&amp;lt;/code&amp;gt; inside each test class for screenshot testing determines if the image is generated &amp;lt;code&amp;gt;(true)&amp;lt;/code&amp;gt; or if the image is validated &amp;lt;code&amp;gt;(false)&amp;lt;/code&amp;gt;. The images are generated on the device &amp;lt;code&amp;gt;(/data/data/com.laimburggasse.aquariumcontrol.[module].test/files/screenshots_output/)&amp;lt;/code&amp;gt; and need to be downloaded to the repository.&lt;br /&gt;
&lt;br /&gt;
=== Unit tests ===&lt;br /&gt;
As of November 2025, there are no unit tests.&lt;br /&gt;
&lt;br /&gt;
== Release procedure ==&lt;br /&gt;
# Update &amp;lt;code&amp;gt;versionCode&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;versionName&amp;lt;/code&amp;gt; in &amp;lt;code&amp;gt;build.gradle.kts&amp;lt;/code&amp;gt; of &amp;lt;code&amp;gt;app&amp;lt;/code&amp;gt; module.&lt;br /&gt;
# Create a release branch (&amp;lt;code&amp;gt;release/[version]&amp;lt;/code&amp;gt;), checkout, commit and push.&lt;br /&gt;
# Change to release build variant.&lt;br /&gt;
# Build&lt;br /&gt;
# Generate a signed App bundle&lt;br /&gt;
# Upload to play store&lt;/div&gt;</summary>
		<author><name>Uwe</name></author>
	</entry>
	<entry>
		<id>http://217.79.180.177/mediawiki/index.php?title=Android_application&amp;diff=446</id>
		<title>Android application</title>
		<link rel="alternate" type="text/html" href="http://217.79.180.177/mediawiki/index.php?title=Android_application&amp;diff=446"/>
		<updated>2026-04-18T10:46:16Z</updated>

		<summary type="html">&lt;p&gt;Uwe: /* Architecture */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;The app is available in the [https://play.google.com/store/apps/details?id=com.laimburggasse.aquariumcontroller Play store].&lt;br /&gt;
== Requirements ==&lt;br /&gt;
== Architecture ==&lt;br /&gt;
The app is written completely in Kotlin using Jetpack Compose, Dagger/Hilt and aiming to implement a clean architecture.&lt;br /&gt;
=== Module structure ===&lt;br /&gt;
Besides the main app module, the app consists of the following library modules:&lt;br /&gt;
* balling&lt;br /&gt;
* common&lt;br /&gt;
* controller&lt;br /&gt;
* feed&lt;br /&gt;
* heating&lt;br /&gt;
* info&lt;br /&gt;
* overview&lt;br /&gt;
* refill&lt;br /&gt;
* schedule&lt;br /&gt;
* timedata&lt;br /&gt;
* ventilation&lt;br /&gt;
&lt;br /&gt;
The main &amp;lt;code&amp;gt;app&amp;lt;/code&amp;gt; module contains the AndroidManifest and the main activity (As of November 2025, the app only uses one activity).&lt;br /&gt;
The activity contains the navigation &amp;lt;code&amp;gt;NavHost&amp;lt;/code&amp;gt; in &amp;lt;code&amp;gt;onCreate&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
None of the other modules has a dependency to the main &amp;lt;code&amp;gt;app&amp;lt;/code&amp;gt; module.&lt;br /&gt;
&lt;br /&gt;
The &amp;lt;code&amp;gt;common&amp;lt;/code&amp;gt; module provides low-level functionalities and layout elements used in various places.&lt;br /&gt;
The following functionalities are located in &amp;lt;code&amp;gt;common&amp;lt;/code&amp;gt; module:&lt;br /&gt;
* &amp;lt;code&amp;gt;DataFetchResult&amp;lt;/code&amp;gt;: A wrapper class indicating the status of data fetching operation (&amp;lt;code&amp;gt;Success&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Error&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Loading&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Empty&amp;lt;/code&amp;gt;).&lt;br /&gt;
* &amp;lt;code&amp;gt;WebApiParams&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;WebApiPostRequest&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;WebApiPostRequestExecution&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;WebApiUrls&amp;lt;/code&amp;gt;: Classes and objects used for communication from the app to the server.&lt;br /&gt;
* &amp;lt;code&amp;gt;ControllerProfileDao&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;ControllerProfileDatabase&amp;lt;/code&amp;gt;: Classes which provide Room database functionality.&lt;br /&gt;
* Various classes used for dependency injection (Hilt)&lt;br /&gt;
* &amp;lt;code&amp;gt;SetValsSanityCheckResult&amp;lt;/code&amp;gt;: Class used for to communicate status of set values for &amp;lt;code&amp;gt;heating&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;ventilation&amp;lt;/code&amp;gt; module.&lt;br /&gt;
* &amp;lt;code&amp;gt;ControllerProfile&amp;lt;/code&amp;gt;: Class containing the profile data of the server (address, port, credentials, ...)&lt;br /&gt;
* &amp;lt;code&amp;gt;GlobalConstants&amp;lt;/code&amp;gt;: Mainly UI-related strings (non-context-related) and some minor functional constants&lt;br /&gt;
* Composable functions for the main drop down menu&lt;br /&gt;
* Composable functions for the theme&lt;br /&gt;
* Composable functions for hyperlinks, text edit fields, headline&lt;br /&gt;
* &amp;lt;code&amp;gt;ViewNavigationRoutes&amp;lt;/code&amp;gt;: Object containing the routing information processed in main &amp;lt;code&amp;gt;app&amp;lt;/code&amp;gt; module&lt;br /&gt;
* &amp;lt;code&amp;gt;ScreenshotTestHelper&amp;lt;/code&amp;gt;: Helper class for executing the instrumented snapshot testing using the emulator.&lt;br /&gt;
&lt;br /&gt;
=== Layer structure ===&lt;br /&gt;
The code is separated into the following layers:&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
[module]/&lt;br /&gt;
&lt;br /&gt;
├── src/main/kotlin/com/laimburggasse/aquariumcontrol/[module]/data/&lt;br /&gt;
&lt;br /&gt;
│   ├── remote/                   // Folder for data retrieval functionality&lt;br /&gt;
&lt;br /&gt;
│   │   └── dto/                  // Folder for Data Transfer Objects&lt;br /&gt;
&lt;br /&gt;
│   │       └── [module][name]Import.kt&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module][name]Reader.kt      // Implementation of GET server request&lt;br /&gt;
&lt;br /&gt;
│   ├── [module][name]RepositoryImpl.kt  // Repositories (implementation of interface)&lt;br /&gt;
&lt;br /&gt;
├── src/main/kotlin/com/laimburggasse/aquariumcontrol/[module]/domain/&lt;br /&gt;
&lt;br /&gt;
│   ├── model/                    // Folder for data definition classes&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module][name].kt.    // Classes for data definition&lt;br /&gt;
&lt;br /&gt;
│   ├── repository/               // Folder containing repository interface definitions&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module][name]Repository.kt.  // Interface defining the repository, used by view model&lt;br /&gt;
&lt;br /&gt;
├── src/main/kotlin/com/laimburggasse/aquariumcontrol/[module]/presentation/&lt;br /&gt;
&lt;br /&gt;
│   ├── entry/                    // First screen of feature&lt;br /&gt;
&lt;br /&gt;
│   ├── [components]/             // Shared code between different screens&lt;br /&gt;
&lt;br /&gt;
│   ├── [screen name]/            // Folder for each screen&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module][name]Screen.kt       // Composable showing the complete screen&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module][name]Composable.kt   // Composable showing elements inside the screen&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module][name]UiEvent.kt      // Sealed interface describing the communication from the composable to the view model&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module][name]UiState.kt      // Data class describing the content to be rendered by the composable&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module][name]ViewModel.kt    // Class implementing the view model&lt;br /&gt;
&lt;br /&gt;
│   ├── navigation/               // Folder containing class and functionality related to screen navigation&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module]NavigationEvent.kt  // One sealed class per module containing the navigation intent of the user&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module]Navigator.kt  // Module-specific navigation functionality&lt;br /&gt;
&lt;br /&gt;
│&lt;br /&gt;
└── build.gradle                  // Plugins, setting, dependencies&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Screen navigation ===&lt;br /&gt;
The screen navigation is controlled by a &amp;lt;code&amp;gt;NavHostController&amp;lt;/code&amp;gt; which is instantiated in &amp;lt;code&amp;gt;MainActivity&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Each module adds its own navigation function to &amp;lt;code&amp;gt;NavHostController&amp;lt;/code&amp;gt;.&lt;br /&gt;
These functions are stored in &amp;lt;code&amp;gt;[module]/presentation/navigation/[module]Navigator.kt&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
The functions receive as parameter a specific sealed class &amp;lt;code&amp;gt;[module]NavigationEvent&amp;lt;/code&amp;gt;.&lt;br /&gt;
These sealed classes contain objects for each navigation event inside the module.&lt;br /&gt;
&lt;br /&gt;
== Example workflow for adding API communication ==&lt;br /&gt;
=== Implementation ===&lt;br /&gt;
Create the model: &amp;lt;code&amp;gt;RefillState&amp;lt;/code&amp;gt; as &amp;lt;code&amp;gt;enum&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Add a reader &amp;lt;code&amp;gt;class&amp;lt;/code&amp;gt; &amp;lt;code&amp;gt;RefillStateReader&amp;lt;/code&amp;gt; which uses the injected interface (Retrofit)&lt;br /&gt;
&lt;br /&gt;
Update the &amp;lt;code&amp;gt;RefillStateApiService&amp;lt;/code&amp;gt;: Add a function to load the state via Retrofit&lt;br /&gt;
&lt;br /&gt;
Add the label for the JSON property in &amp;lt;code&amp;gt;WebApiParams&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Add the URL for the end point in &amp;lt;code&amp;gt;WebApiUrls&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Add a wrapper &amp;lt;code&amp;gt;RefillStateResponse&amp;lt;/code&amp;gt; for the model to allow Retrofit parsing the JSON&lt;br /&gt;
&lt;br /&gt;
Update the repository interface and the repository interface implementation:&lt;br /&gt;
* Add a flow that delivers a &amp;lt;code&amp;gt;DataFetchResult&amp;lt;/code&amp;gt; of the &amp;lt;code&amp;gt;RefillState&amp;lt;/code&amp;gt;&lt;br /&gt;
* Add a function that allows the view model to trigger a refresh&lt;br /&gt;
&lt;br /&gt;
Add a wrapper &amp;lt;code&amp;gt;class&amp;lt;/code&amp;gt; &amp;lt;code&amp;gt;RefillStateUiState&amp;lt;/code&amp;gt;&lt;br /&gt;
Update the view model&lt;br /&gt;
* Add a flow that delivers a &amp;lt;code&amp;gt;RefillStateUiState&amp;lt;/code&amp;gt; based on the &amp;lt;code&amp;gt;DataFetchResult&amp;lt;/code&amp;gt; from repository&lt;br /&gt;
* Handle the new UI event for refreshing&lt;br /&gt;
&lt;br /&gt;
Add composable &amp;lt;code&amp;gt;RefillStateComposable&amp;lt;/code&amp;gt; for displaying the &amp;lt;code&amp;gt;RefillState&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Update &amp;lt;code&amp;gt;RefillUiEvent&amp;lt;/code&amp;gt; with an object for the refresh&lt;br /&gt;
&lt;br /&gt;
Update &amp;lt;code&amp;gt;RefillScreen&amp;lt;/code&amp;gt;:&lt;br /&gt;
* additional parameter (&amp;lt;code&amp;gt;RefillStateUiState&amp;lt;/code&amp;gt;) in composable function and in previews&lt;br /&gt;
* Added &amp;lt;code&amp;gt;PullToRefreshBox&amp;lt;/code&amp;gt;&lt;br /&gt;
* Added parsing of &amp;lt;code&amp;gt;RefillUiState&amp;lt;/code&amp;gt; and display of either&lt;br /&gt;
    * &amp;lt;code&amp;gt;RefillStateComposable&amp;lt;/code&amp;gt; in case of success&lt;br /&gt;
    * Text-based error message in case of error&lt;br /&gt;
    * &amp;lt;code&amp;gt;CircularProgressIndicator&amp;lt;/code&amp;gt; in case of loading&lt;br /&gt;
&lt;br /&gt;
Inject &amp;lt;code&amp;gt;RefillUiState&amp;lt;/code&amp;gt; into &amp;lt;code&amp;gt;RefillScreen&amp;lt;/code&amp;gt; in the &amp;lt;code&amp;gt;MainActivity&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;RefillScreenTest&amp;lt;/code&amp;gt; as well as in previews of &amp;lt;code&amp;gt;RefillScreen&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
(Optional) Check if &amp;lt;code&amp;gt;RefillDataModule&amp;lt;/code&amp;gt; needs update (not required in this case)&lt;br /&gt;
&lt;br /&gt;
=== Testing ===&lt;br /&gt;
Update existing screenshot testing&lt;br /&gt;
&lt;br /&gt;
Add screenshot testing for new composable&lt;br /&gt;
&lt;br /&gt;
== Test strategy ==&lt;br /&gt;
The app uses different testing approaches:&lt;br /&gt;
* Compose preview screenshot testing&lt;br /&gt;
* Screenshot tests running as instrumented tests on emulator&lt;br /&gt;
* Unit tests&lt;br /&gt;
&lt;br /&gt;
=== Compose preview screenshot tests ===&lt;br /&gt;
These tests run comparably fast and device-independent.&lt;br /&gt;
&lt;br /&gt;
The compose preview screenshot tests cover portrait mode/landscape mode, light/dark mode (four variants).&lt;br /&gt;
&lt;br /&gt;
Limitations:&lt;br /&gt;
* Two tests within &amp;lt;code&amp;gt;feed&amp;lt;/code&amp;gt; module can only run in landscape mode (&amp;lt;code&amp;gt;FeedProfileScheduleScreenTest&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;FeedHistoryScreenTest&amp;lt;/code&amp;gt;) due to excessive heap memory utilisation.&lt;br /&gt;
* &amp;lt;code&amp;gt;Info&amp;lt;/code&amp;gt; screen is not tested due to &amp;lt;code&amp;gt;LibrariesContainer&amp;lt;/code&amp;gt; (dynamic content which cannot be mocked).&lt;br /&gt;
&lt;br /&gt;
The tests are located in &amp;lt;code&amp;gt;[module]/src/screenshotTest/kotlin/com/aquariumcontrol/[module]/&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
The references are located in &amp;lt;code&amp;gt;[module]/src/screenshotTestDebug/kotlin/com/aquariumcontrol/[module]/&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
The test reports are located in &amp;lt;code&amp;gt;build/reports/screenshotTest/preview/debug/&amp;lt;/code&amp;gt;. The test reports for the compose screenshot tests contain reference, actual and delta images.&lt;br /&gt;
&lt;br /&gt;
The images are generated with &amp;lt;code&amp;gt;./gradlew [module]:updateDebugScreenshotTest&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
The images are validated with &amp;lt;code&amp;gt;./gradlew [module]:validateDebugScreenshotTest&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Omitting the module will execute all tests.&lt;br /&gt;
&lt;br /&gt;
=== Instrumented tests ===&lt;br /&gt;
The tests run comparably long and are bound to an emulator device.&lt;br /&gt;
As of November 2025, a Pixel 5 API S is used for the instrumented test.&lt;br /&gt;
The instrumented tests contain&lt;br /&gt;
* screenshot testing for the complete screens (&amp;lt;code&amp;gt;ScreenNameTest&amp;lt;/code&amp;gt;)&lt;br /&gt;
* verification if the composable triggers the right event when tapping on buttons bound to navigation to another screen (&amp;lt;code&amp;gt;ScreenNameUITest&amp;lt;/code&amp;gt;)&lt;br /&gt;
&lt;br /&gt;
Limitations:&lt;br /&gt;
* &amp;lt;code&amp;gt;Info&amp;lt;/code&amp;gt; screen is not tested due to &amp;lt;code&amp;gt;LibrariesContainer&amp;lt;/code&amp;gt; (dynamic content which cannot be mocked and also does not allow execution of instrumented test).&lt;br /&gt;
&lt;br /&gt;
The test reports are located in &amp;lt;code&amp;gt;build/reports/androidTests/connected/debug/&amp;lt;/code&amp;gt;. The test reports for the compose screenshot tests do not contain any images.&lt;br /&gt;
&lt;br /&gt;
The tests are executed with &amp;lt;code&amp;gt;./gradlew [module]:connectedDebugAndroidTest&amp;lt;/code&amp;gt;.&lt;br /&gt;
Omitting the module will execute all tests.&lt;br /&gt;
&lt;br /&gt;
A switch called &amp;lt;code&amp;gt;recordMode&amp;lt;/code&amp;gt; inside each test class for screenshot testing determines if the image is generated &amp;lt;code&amp;gt;(true)&amp;lt;/code&amp;gt; or if the image is validated &amp;lt;code&amp;gt;(false)&amp;lt;/code&amp;gt;. The images are generated on the device &amp;lt;code&amp;gt;(/data/data/com.laimburggasse.aquariumcontrol.[module].test/files/screenshots_output/)&amp;lt;/code&amp;gt; and need to be downloaded to the repository.&lt;br /&gt;
&lt;br /&gt;
=== Unit tests ===&lt;br /&gt;
As of November 2025, there are no unit tests.&lt;br /&gt;
&lt;br /&gt;
== Release procedure ==&lt;br /&gt;
# Update &amp;lt;code&amp;gt;versionCode&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;versionName&amp;lt;/code&amp;gt; in &amp;lt;code&amp;gt;build.gradle.kts&amp;lt;/code&amp;gt; of &amp;lt;code&amp;gt;app&amp;lt;/code&amp;gt; module.&lt;br /&gt;
# Create a release branch (&amp;lt;code&amp;gt;release/[version]&amp;lt;/code&amp;gt;), checkout, commit and push.&lt;br /&gt;
# Change to release build variant.&lt;br /&gt;
# Build&lt;br /&gt;
# Generate a signed App bundle&lt;br /&gt;
# Upload to play store&lt;/div&gt;</summary>
		<author><name>Uwe</name></author>
	</entry>
	<entry>
		<id>http://217.79.180.177/mediawiki/index.php?title=Android_application&amp;diff=445</id>
		<title>Android application</title>
		<link rel="alternate" type="text/html" href="http://217.79.180.177/mediawiki/index.php?title=Android_application&amp;diff=445"/>
		<updated>2026-04-18T10:36:31Z</updated>

		<summary type="html">&lt;p&gt;Uwe: /* Test strategy */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;The app is available in the [https://play.google.com/store/apps/details?id=com.laimburggasse.aquariumcontroller Play store].&lt;br /&gt;
== Requirements ==&lt;br /&gt;
== Architecture ==&lt;br /&gt;
The app is written completely in Kotlin using Jetpack Compose, Dagger/Hilt and aiming to implement a clean architecture.&lt;br /&gt;
=== Module structure ===&lt;br /&gt;
Besides the main app module, the app consists of the following library modules:&lt;br /&gt;
* balling&lt;br /&gt;
* common&lt;br /&gt;
* controller&lt;br /&gt;
* feed&lt;br /&gt;
* heating&lt;br /&gt;
* info&lt;br /&gt;
* overview&lt;br /&gt;
* refill&lt;br /&gt;
* schedule&lt;br /&gt;
* timedata&lt;br /&gt;
* ventilation&lt;br /&gt;
&lt;br /&gt;
The main &amp;lt;code&amp;gt;app&amp;lt;/code&amp;gt; module contains the AndroidManifest and the main activity (As of November 2025, the app only uses one activity).&lt;br /&gt;
The activity contains the navigation &amp;lt;code&amp;gt;NavHost&amp;lt;/code&amp;gt; in &amp;lt;code&amp;gt;onCreate&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
None of the other modules has a dependency to the main &amp;lt;code&amp;gt;app&amp;lt;/code&amp;gt; module.&lt;br /&gt;
&lt;br /&gt;
The &amp;lt;code&amp;gt;common&amp;lt;/code&amp;gt; module provides low-level functionalities and layout elements used in various places.&lt;br /&gt;
The following functionalities are located in &amp;lt;code&amp;gt;common&amp;lt;/code&amp;gt; module:&lt;br /&gt;
* &amp;lt;code&amp;gt;DataFetchResult&amp;lt;/code&amp;gt;: A wrapper class indicating the status of data fetching operation (&amp;lt;code&amp;gt;Success&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Error&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Loading&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Empty&amp;lt;/code&amp;gt;).&lt;br /&gt;
* &amp;lt;code&amp;gt;WebApiParams&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;WebApiPostRequest&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;WebApiPostRequestExecution&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;WebApiUrls&amp;lt;/code&amp;gt;: Classes and objects used for communication from the app to the server.&lt;br /&gt;
* &amp;lt;code&amp;gt;ControllerProfileDao&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;ControllerProfileDatabase&amp;lt;/code&amp;gt;: Classes which provide Room database functionality.&lt;br /&gt;
* Various classes used for dependency injection (Hilt)&lt;br /&gt;
* &amp;lt;code&amp;gt;SetValsSanityCheckResult&amp;lt;/code&amp;gt;: Class used for to communicate status of set values for &amp;lt;code&amp;gt;heating&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;ventilation&amp;lt;/code&amp;gt; module.&lt;br /&gt;
* &amp;lt;code&amp;gt;ControllerProfile&amp;lt;/code&amp;gt;: Class containing the profile data of the server (address, port, credentials, ...)&lt;br /&gt;
* &amp;lt;code&amp;gt;GlobalConstants&amp;lt;/code&amp;gt;: Mainly UI-related strings (non-context-related) and some minor functional constants&lt;br /&gt;
* Composable functions for the main drop down menu&lt;br /&gt;
* Composable functions for the theme&lt;br /&gt;
* Composable functions for hyperlinks, text edit fields, headline&lt;br /&gt;
* &amp;lt;code&amp;gt;ViewNavigationRoutes&amp;lt;/code&amp;gt;: Object containing the routing information processed in main &amp;lt;code&amp;gt;app&amp;lt;/code&amp;gt; module&lt;br /&gt;
* &amp;lt;code&amp;gt;ScreenshotTestHelper&amp;lt;/code&amp;gt;: Helper class for executing the instrumented snapshot testing using the emulator.&lt;br /&gt;
&lt;br /&gt;
=== Layer structure ===&lt;br /&gt;
The code is separated into the following layers:&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
[module]/&lt;br /&gt;
&lt;br /&gt;
├── src/main/kotlin/com/laimburggasse/aquariumcontrol/[module]/data/&lt;br /&gt;
&lt;br /&gt;
│   ├── remote/                   // Folder for data retrieval functionality&lt;br /&gt;
&lt;br /&gt;
│   │   └── dto/                  // Folder for Data Transfer Objects&lt;br /&gt;
&lt;br /&gt;
│   │       └── [module][name]Import.kt&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module][name]Reader.kt      // Implementation of GET server request&lt;br /&gt;
&lt;br /&gt;
│   ├── [module][name]RepositoryImpl.kt  // Repositories (implementation of interface)&lt;br /&gt;
&lt;br /&gt;
├── src/main/kotlin/com/laimburggasse/aquariumcontrol/[module]/domain/&lt;br /&gt;
&lt;br /&gt;
│   ├── model/                    // Folder for data definition classes&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module][name].kt.    // Classes for data definition&lt;br /&gt;
&lt;br /&gt;
│   ├── repository/               // Folder containing repository interface definitions&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module][name]Repository.kt.  // Interface defining the repository, used by view model&lt;br /&gt;
&lt;br /&gt;
├── src/main/kotlin/com/laimburggasse/aquariumcontrol/[module]/presentation/&lt;br /&gt;
&lt;br /&gt;
│   ├── entry/                    // First screen of feature&lt;br /&gt;
&lt;br /&gt;
│   ├── [components]/             // Shared code between different screens&lt;br /&gt;
&lt;br /&gt;
│   ├── [screen name]/            // Folder for each screen&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module][name]Screen.kt       // Composable showing the complete screen&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module][name]Composable.kt   // Composable showing elements inside the screen&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module][name]UiEvent.kt      // Sealed interface describing the communication from the composable to the view model&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module][name]UiState.kt      // Data class describing the content to be rendered by the composable&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module][name]ViewModel.kt    // Class implementing the view model&lt;br /&gt;
&lt;br /&gt;
│   ├── navigation/               // Folder containing class and functionality related to screen navigation&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module]NavigationEvent.kt  // One sealed class per module containing the navigation intent of the user&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module]Navigator.kt  // Module-specific navigation functionality&lt;br /&gt;
&lt;br /&gt;
│&lt;br /&gt;
└── build.gradle                  // Plugins, setting, dependencies&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Example workflow for adding API communication ==&lt;br /&gt;
=== Implementation ===&lt;br /&gt;
Create the model: &amp;lt;code&amp;gt;RefillState&amp;lt;/code&amp;gt; as &amp;lt;code&amp;gt;enum&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Add a reader &amp;lt;code&amp;gt;class&amp;lt;/code&amp;gt; &amp;lt;code&amp;gt;RefillStateReader&amp;lt;/code&amp;gt; which uses the injected interface (Retrofit)&lt;br /&gt;
&lt;br /&gt;
Update the &amp;lt;code&amp;gt;RefillStateApiService&amp;lt;/code&amp;gt;: Add a function to load the state via Retrofit&lt;br /&gt;
&lt;br /&gt;
Add the label for the JSON property in &amp;lt;code&amp;gt;WebApiParams&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Add the URL for the end point in &amp;lt;code&amp;gt;WebApiUrls&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Add a wrapper &amp;lt;code&amp;gt;RefillStateResponse&amp;lt;/code&amp;gt; for the model to allow Retrofit parsing the JSON&lt;br /&gt;
&lt;br /&gt;
Update the repository interface and the repository interface implementation:&lt;br /&gt;
* Add a flow that delivers a &amp;lt;code&amp;gt;DataFetchResult&amp;lt;/code&amp;gt; of the &amp;lt;code&amp;gt;RefillState&amp;lt;/code&amp;gt;&lt;br /&gt;
* Add a function that allows the view model to trigger a refresh&lt;br /&gt;
&lt;br /&gt;
Add a wrapper &amp;lt;code&amp;gt;class&amp;lt;/code&amp;gt; &amp;lt;code&amp;gt;RefillStateUiState&amp;lt;/code&amp;gt;&lt;br /&gt;
Update the view model&lt;br /&gt;
* Add a flow that delivers a &amp;lt;code&amp;gt;RefillStateUiState&amp;lt;/code&amp;gt; based on the &amp;lt;code&amp;gt;DataFetchResult&amp;lt;/code&amp;gt; from repository&lt;br /&gt;
* Handle the new UI event for refreshing&lt;br /&gt;
&lt;br /&gt;
Add composable &amp;lt;code&amp;gt;RefillStateComposable&amp;lt;/code&amp;gt; for displaying the &amp;lt;code&amp;gt;RefillState&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Update &amp;lt;code&amp;gt;RefillUiEvent&amp;lt;/code&amp;gt; with an object for the refresh&lt;br /&gt;
&lt;br /&gt;
Update &amp;lt;code&amp;gt;RefillScreen&amp;lt;/code&amp;gt;:&lt;br /&gt;
* additional parameter (&amp;lt;code&amp;gt;RefillStateUiState&amp;lt;/code&amp;gt;) in composable function and in previews&lt;br /&gt;
* Added &amp;lt;code&amp;gt;PullToRefreshBox&amp;lt;/code&amp;gt;&lt;br /&gt;
* Added parsing of &amp;lt;code&amp;gt;RefillUiState&amp;lt;/code&amp;gt; and display of either&lt;br /&gt;
    * &amp;lt;code&amp;gt;RefillStateComposable&amp;lt;/code&amp;gt; in case of success&lt;br /&gt;
    * Text-based error message in case of error&lt;br /&gt;
    * &amp;lt;code&amp;gt;CircularProgressIndicator&amp;lt;/code&amp;gt; in case of loading&lt;br /&gt;
&lt;br /&gt;
Inject &amp;lt;code&amp;gt;RefillUiState&amp;lt;/code&amp;gt; into &amp;lt;code&amp;gt;RefillScreen&amp;lt;/code&amp;gt; in the &amp;lt;code&amp;gt;MainActivity&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;RefillScreenTest&amp;lt;/code&amp;gt; as well as in previews of &amp;lt;code&amp;gt;RefillScreen&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
(Optional) Check if &amp;lt;code&amp;gt;RefillDataModule&amp;lt;/code&amp;gt; needs update (not required in this case)&lt;br /&gt;
&lt;br /&gt;
=== Testing ===&lt;br /&gt;
Update existing screenshot testing&lt;br /&gt;
&lt;br /&gt;
Add screenshot testing for new composable&lt;br /&gt;
&lt;br /&gt;
== Test strategy ==&lt;br /&gt;
The app uses different testing approaches:&lt;br /&gt;
* Compose preview screenshot testing&lt;br /&gt;
* Screenshot tests running as instrumented tests on emulator&lt;br /&gt;
* Unit tests&lt;br /&gt;
&lt;br /&gt;
=== Compose preview screenshot tests ===&lt;br /&gt;
These tests run comparably fast and device-independent.&lt;br /&gt;
&lt;br /&gt;
The compose preview screenshot tests cover portrait mode/landscape mode, light/dark mode (four variants).&lt;br /&gt;
&lt;br /&gt;
Limitations:&lt;br /&gt;
* Two tests within &amp;lt;code&amp;gt;feed&amp;lt;/code&amp;gt; module can only run in landscape mode (&amp;lt;code&amp;gt;FeedProfileScheduleScreenTest&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;FeedHistoryScreenTest&amp;lt;/code&amp;gt;) due to excessive heap memory utilisation.&lt;br /&gt;
* &amp;lt;code&amp;gt;Info&amp;lt;/code&amp;gt; screen is not tested due to &amp;lt;code&amp;gt;LibrariesContainer&amp;lt;/code&amp;gt; (dynamic content which cannot be mocked).&lt;br /&gt;
&lt;br /&gt;
The tests are located in &amp;lt;code&amp;gt;[module]/src/screenshotTest/kotlin/com/aquariumcontrol/[module]/&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
The references are located in &amp;lt;code&amp;gt;[module]/src/screenshotTestDebug/kotlin/com/aquariumcontrol/[module]/&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
The test reports are located in &amp;lt;code&amp;gt;build/reports/screenshotTest/preview/debug/&amp;lt;/code&amp;gt;. The test reports for the compose screenshot tests contain reference, actual and delta images.&lt;br /&gt;
&lt;br /&gt;
The images are generated with &amp;lt;code&amp;gt;./gradlew [module]:updateDebugScreenshotTest&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
The images are validated with &amp;lt;code&amp;gt;./gradlew [module]:validateDebugScreenshotTest&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Omitting the module will execute all tests.&lt;br /&gt;
&lt;br /&gt;
=== Instrumented tests ===&lt;br /&gt;
The tests run comparably long and are bound to an emulator device.&lt;br /&gt;
As of November 2025, a Pixel 5 API S is used for the instrumented test.&lt;br /&gt;
The instrumented tests contain&lt;br /&gt;
* screenshot testing for the complete screens (&amp;lt;code&amp;gt;ScreenNameTest&amp;lt;/code&amp;gt;)&lt;br /&gt;
* verification if the composable triggers the right event when tapping on buttons bound to navigation to another screen (&amp;lt;code&amp;gt;ScreenNameUITest&amp;lt;/code&amp;gt;)&lt;br /&gt;
&lt;br /&gt;
Limitations:&lt;br /&gt;
* &amp;lt;code&amp;gt;Info&amp;lt;/code&amp;gt; screen is not tested due to &amp;lt;code&amp;gt;LibrariesContainer&amp;lt;/code&amp;gt; (dynamic content which cannot be mocked and also does not allow execution of instrumented test).&lt;br /&gt;
&lt;br /&gt;
The test reports are located in &amp;lt;code&amp;gt;build/reports/androidTests/connected/debug/&amp;lt;/code&amp;gt;. The test reports for the compose screenshot tests do not contain any images.&lt;br /&gt;
&lt;br /&gt;
The tests are executed with &amp;lt;code&amp;gt;./gradlew [module]:connectedDebugAndroidTest&amp;lt;/code&amp;gt;.&lt;br /&gt;
Omitting the module will execute all tests.&lt;br /&gt;
&lt;br /&gt;
A switch called &amp;lt;code&amp;gt;recordMode&amp;lt;/code&amp;gt; inside each test class for screenshot testing determines if the image is generated &amp;lt;code&amp;gt;(true)&amp;lt;/code&amp;gt; or if the image is validated &amp;lt;code&amp;gt;(false)&amp;lt;/code&amp;gt;. The images are generated on the device &amp;lt;code&amp;gt;(/data/data/com.laimburggasse.aquariumcontrol.[module].test/files/screenshots_output/)&amp;lt;/code&amp;gt; and need to be downloaded to the repository.&lt;br /&gt;
&lt;br /&gt;
=== Unit tests ===&lt;br /&gt;
As of November 2025, there are no unit tests.&lt;br /&gt;
&lt;br /&gt;
== Release procedure ==&lt;br /&gt;
# Update &amp;lt;code&amp;gt;versionCode&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;versionName&amp;lt;/code&amp;gt; in &amp;lt;code&amp;gt;build.gradle.kts&amp;lt;/code&amp;gt; of &amp;lt;code&amp;gt;app&amp;lt;/code&amp;gt; module.&lt;br /&gt;
# Create a release branch (&amp;lt;code&amp;gt;release/[version]&amp;lt;/code&amp;gt;), checkout, commit and push.&lt;br /&gt;
# Change to release build variant.&lt;br /&gt;
# Build&lt;br /&gt;
# Generate a signed App bundle&lt;br /&gt;
# Upload to play store&lt;/div&gt;</summary>
		<author><name>Uwe</name></author>
	</entry>
	<entry>
		<id>http://217.79.180.177/mediawiki/index.php?title=Android_application&amp;diff=444</id>
		<title>Android application</title>
		<link rel="alternate" type="text/html" href="http://217.79.180.177/mediawiki/index.php?title=Android_application&amp;diff=444"/>
		<updated>2026-04-18T07:46:45Z</updated>

		<summary type="html">&lt;p&gt;Uwe: /* Testing */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;The app is available in the [https://play.google.com/store/apps/details?id=com.laimburggasse.aquariumcontroller Play store].&lt;br /&gt;
== Requirements ==&lt;br /&gt;
== Architecture ==&lt;br /&gt;
The app is written completely in Kotlin using Jetpack Compose, Dagger/Hilt and aiming to implement a clean architecture.&lt;br /&gt;
=== Module structure ===&lt;br /&gt;
Besides the main app module, the app consists of the following library modules:&lt;br /&gt;
* balling&lt;br /&gt;
* common&lt;br /&gt;
* controller&lt;br /&gt;
* feed&lt;br /&gt;
* heating&lt;br /&gt;
* info&lt;br /&gt;
* overview&lt;br /&gt;
* refill&lt;br /&gt;
* schedule&lt;br /&gt;
* timedata&lt;br /&gt;
* ventilation&lt;br /&gt;
&lt;br /&gt;
The main &amp;lt;code&amp;gt;app&amp;lt;/code&amp;gt; module contains the AndroidManifest and the main activity (As of November 2025, the app only uses one activity).&lt;br /&gt;
The activity contains the navigation &amp;lt;code&amp;gt;NavHost&amp;lt;/code&amp;gt; in &amp;lt;code&amp;gt;onCreate&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
None of the other modules has a dependency to the main &amp;lt;code&amp;gt;app&amp;lt;/code&amp;gt; module.&lt;br /&gt;
&lt;br /&gt;
The &amp;lt;code&amp;gt;common&amp;lt;/code&amp;gt; module provides low-level functionalities and layout elements used in various places.&lt;br /&gt;
The following functionalities are located in &amp;lt;code&amp;gt;common&amp;lt;/code&amp;gt; module:&lt;br /&gt;
* &amp;lt;code&amp;gt;DataFetchResult&amp;lt;/code&amp;gt;: A wrapper class indicating the status of data fetching operation (&amp;lt;code&amp;gt;Success&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Error&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Loading&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Empty&amp;lt;/code&amp;gt;).&lt;br /&gt;
* &amp;lt;code&amp;gt;WebApiParams&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;WebApiPostRequest&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;WebApiPostRequestExecution&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;WebApiUrls&amp;lt;/code&amp;gt;: Classes and objects used for communication from the app to the server.&lt;br /&gt;
* &amp;lt;code&amp;gt;ControllerProfileDao&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;ControllerProfileDatabase&amp;lt;/code&amp;gt;: Classes which provide Room database functionality.&lt;br /&gt;
* Various classes used for dependency injection (Hilt)&lt;br /&gt;
* &amp;lt;code&amp;gt;SetValsSanityCheckResult&amp;lt;/code&amp;gt;: Class used for to communicate status of set values for &amp;lt;code&amp;gt;heating&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;ventilation&amp;lt;/code&amp;gt; module.&lt;br /&gt;
* &amp;lt;code&amp;gt;ControllerProfile&amp;lt;/code&amp;gt;: Class containing the profile data of the server (address, port, credentials, ...)&lt;br /&gt;
* &amp;lt;code&amp;gt;GlobalConstants&amp;lt;/code&amp;gt;: Mainly UI-related strings (non-context-related) and some minor functional constants&lt;br /&gt;
* Composable functions for the main drop down menu&lt;br /&gt;
* Composable functions for the theme&lt;br /&gt;
* Composable functions for hyperlinks, text edit fields, headline&lt;br /&gt;
* &amp;lt;code&amp;gt;ViewNavigationRoutes&amp;lt;/code&amp;gt;: Object containing the routing information processed in main &amp;lt;code&amp;gt;app&amp;lt;/code&amp;gt; module&lt;br /&gt;
* &amp;lt;code&amp;gt;ScreenshotTestHelper&amp;lt;/code&amp;gt;: Helper class for executing the instrumented snapshot testing using the emulator.&lt;br /&gt;
&lt;br /&gt;
=== Layer structure ===&lt;br /&gt;
The code is separated into the following layers:&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
[module]/&lt;br /&gt;
&lt;br /&gt;
├── src/main/kotlin/com/laimburggasse/aquariumcontrol/[module]/data/&lt;br /&gt;
&lt;br /&gt;
│   ├── remote/                   // Folder for data retrieval functionality&lt;br /&gt;
&lt;br /&gt;
│   │   └── dto/                  // Folder for Data Transfer Objects&lt;br /&gt;
&lt;br /&gt;
│   │       └── [module][name]Import.kt&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module][name]Reader.kt      // Implementation of GET server request&lt;br /&gt;
&lt;br /&gt;
│   ├── [module][name]RepositoryImpl.kt  // Repositories (implementation of interface)&lt;br /&gt;
&lt;br /&gt;
├── src/main/kotlin/com/laimburggasse/aquariumcontrol/[module]/domain/&lt;br /&gt;
&lt;br /&gt;
│   ├── model/                    // Folder for data definition classes&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module][name].kt.    // Classes for data definition&lt;br /&gt;
&lt;br /&gt;
│   ├── repository/               // Folder containing repository interface definitions&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module][name]Repository.kt.  // Interface defining the repository, used by view model&lt;br /&gt;
&lt;br /&gt;
├── src/main/kotlin/com/laimburggasse/aquariumcontrol/[module]/presentation/&lt;br /&gt;
&lt;br /&gt;
│   ├── entry/                    // First screen of feature&lt;br /&gt;
&lt;br /&gt;
│   ├── [components]/             // Shared code between different screens&lt;br /&gt;
&lt;br /&gt;
│   ├── [screen name]/            // Folder for each screen&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module][name]Screen.kt       // Composable showing the complete screen&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module][name]Composable.kt   // Composable showing elements inside the screen&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module][name]UiEvent.kt      // Sealed interface describing the communication from the composable to the view model&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module][name]UiState.kt      // Data class describing the content to be rendered by the composable&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module][name]ViewModel.kt    // Class implementing the view model&lt;br /&gt;
&lt;br /&gt;
│   ├── navigation/               // Folder containing class and functionality related to screen navigation&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module]NavigationEvent.kt  // One sealed class per module containing the navigation intent of the user&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module]Navigator.kt  // Module-specific navigation functionality&lt;br /&gt;
&lt;br /&gt;
│&lt;br /&gt;
└── build.gradle                  // Plugins, setting, dependencies&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Example workflow for adding API communication ==&lt;br /&gt;
=== Implementation ===&lt;br /&gt;
Create the model: &amp;lt;code&amp;gt;RefillState&amp;lt;/code&amp;gt; as &amp;lt;code&amp;gt;enum&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Add a reader &amp;lt;code&amp;gt;class&amp;lt;/code&amp;gt; &amp;lt;code&amp;gt;RefillStateReader&amp;lt;/code&amp;gt; which uses the injected interface (Retrofit)&lt;br /&gt;
&lt;br /&gt;
Update the &amp;lt;code&amp;gt;RefillStateApiService&amp;lt;/code&amp;gt;: Add a function to load the state via Retrofit&lt;br /&gt;
&lt;br /&gt;
Add the label for the JSON property in &amp;lt;code&amp;gt;WebApiParams&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Add the URL for the end point in &amp;lt;code&amp;gt;WebApiUrls&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Add a wrapper &amp;lt;code&amp;gt;RefillStateResponse&amp;lt;/code&amp;gt; for the model to allow Retrofit parsing the JSON&lt;br /&gt;
&lt;br /&gt;
Update the repository interface and the repository interface implementation:&lt;br /&gt;
* Add a flow that delivers a &amp;lt;code&amp;gt;DataFetchResult&amp;lt;/code&amp;gt; of the &amp;lt;code&amp;gt;RefillState&amp;lt;/code&amp;gt;&lt;br /&gt;
* Add a function that allows the view model to trigger a refresh&lt;br /&gt;
&lt;br /&gt;
Add a wrapper &amp;lt;code&amp;gt;class&amp;lt;/code&amp;gt; &amp;lt;code&amp;gt;RefillStateUiState&amp;lt;/code&amp;gt;&lt;br /&gt;
Update the view model&lt;br /&gt;
* Add a flow that delivers a &amp;lt;code&amp;gt;RefillStateUiState&amp;lt;/code&amp;gt; based on the &amp;lt;code&amp;gt;DataFetchResult&amp;lt;/code&amp;gt; from repository&lt;br /&gt;
* Handle the new UI event for refreshing&lt;br /&gt;
&lt;br /&gt;
Add composable &amp;lt;code&amp;gt;RefillStateComposable&amp;lt;/code&amp;gt; for displaying the &amp;lt;code&amp;gt;RefillState&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Update &amp;lt;code&amp;gt;RefillUiEvent&amp;lt;/code&amp;gt; with an object for the refresh&lt;br /&gt;
&lt;br /&gt;
Update &amp;lt;code&amp;gt;RefillScreen&amp;lt;/code&amp;gt;:&lt;br /&gt;
* additional parameter (&amp;lt;code&amp;gt;RefillStateUiState&amp;lt;/code&amp;gt;) in composable function and in previews&lt;br /&gt;
* Added &amp;lt;code&amp;gt;PullToRefreshBox&amp;lt;/code&amp;gt;&lt;br /&gt;
* Added parsing of &amp;lt;code&amp;gt;RefillUiState&amp;lt;/code&amp;gt; and display of either&lt;br /&gt;
    * &amp;lt;code&amp;gt;RefillStateComposable&amp;lt;/code&amp;gt; in case of success&lt;br /&gt;
    * Text-based error message in case of error&lt;br /&gt;
    * &amp;lt;code&amp;gt;CircularProgressIndicator&amp;lt;/code&amp;gt; in case of loading&lt;br /&gt;
&lt;br /&gt;
Inject &amp;lt;code&amp;gt;RefillUiState&amp;lt;/code&amp;gt; into &amp;lt;code&amp;gt;RefillScreen&amp;lt;/code&amp;gt; in the &amp;lt;code&amp;gt;MainActivity&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;RefillScreenTest&amp;lt;/code&amp;gt; as well as in previews of &amp;lt;code&amp;gt;RefillScreen&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
(Optional) Check if &amp;lt;code&amp;gt;RefillDataModule&amp;lt;/code&amp;gt; needs update (not required in this case)&lt;br /&gt;
&lt;br /&gt;
=== Testing ===&lt;br /&gt;
Update existing screenshot testing&lt;br /&gt;
&lt;br /&gt;
Add screenshot testing for new composable&lt;br /&gt;
&lt;br /&gt;
== Test strategy ==&lt;br /&gt;
The app uses different testing approaches:&lt;br /&gt;
* Compose preview screenshot testing&lt;br /&gt;
* Screenshot tests running as instrumented tests on emulator&lt;br /&gt;
* Unit tests&lt;br /&gt;
&lt;br /&gt;
=== Compose preview screenshot tests ===&lt;br /&gt;
These tests run comparably fast and device-independent.&lt;br /&gt;
&lt;br /&gt;
The compose preview screenshot tests cover portrait mode/landscape mode, light/dark mode (four variants).&lt;br /&gt;
&lt;br /&gt;
Limitations:&lt;br /&gt;
* Two tests within &amp;lt;code&amp;gt;feed&amp;lt;/code&amp;gt; module can only run in landscape mode (&amp;lt;code&amp;gt;FeedProfileScheduleScreenTest&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;FeedHistoryScreenTest&amp;lt;/code&amp;gt;) due to excessive heap memory utilisation.&lt;br /&gt;
* &amp;lt;code&amp;gt;Info&amp;lt;/code&amp;gt; screen is not tested due to &amp;lt;code&amp;gt;LibrariesContainer&amp;lt;/code&amp;gt; (dynamic content which cannot be mocked).&lt;br /&gt;
&lt;br /&gt;
The tests are located in &amp;lt;code&amp;gt;[module]/src/screenshotTest/kotlin/com/aquariumcontrol/[module]/&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
The references are located in &amp;lt;code&amp;gt;[module]/src/screenshotTestDebug/kotlin/com/aquariumcontrol/[module]/&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
The test reports are located in &amp;lt;code&amp;gt;build/reports/screenshotTest/preview/debug/&amp;lt;/code&amp;gt;. The test reports for the compose screenshot tests contain reference, actual and delta images.&lt;br /&gt;
&lt;br /&gt;
The images are generated with &amp;lt;code&amp;gt;./gradlew [module]:updateDebugScreenshotTest&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
The images are validated with &amp;lt;code&amp;gt;./gradlew [module]:validateDebugScreenshotTest&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Omitting the module will execute all tests.&lt;br /&gt;
&lt;br /&gt;
=== Instrumented tests ===&lt;br /&gt;
The tests run comparably long and are bound to an emulator device.&lt;br /&gt;
As of November 2025, a Pixel 5 API S is used for the instrumented test.&lt;br /&gt;
The instrumented tests only test the complete screens.&lt;br /&gt;
&lt;br /&gt;
Limitations:&lt;br /&gt;
* &amp;lt;code&amp;gt;Info&amp;lt;/code&amp;gt; screen is not tested due to &amp;lt;code&amp;gt;LibrariesContainer&amp;lt;/code&amp;gt; (dynamic content which cannot be mocked and also does not allow execution of instrumented test).&lt;br /&gt;
&lt;br /&gt;
The test reports are located in &amp;lt;code&amp;gt;build/reports/androidTests/connected/debug/&amp;lt;/code&amp;gt;. The test reports for the compose screenshot tests do not contain any images.&lt;br /&gt;
&lt;br /&gt;
The tests are executed with &amp;lt;code&amp;gt;./gradlew [module]:connectedDebugAndroidTest&amp;lt;/code&amp;gt;.&lt;br /&gt;
Omitting the module will execute all tests.&lt;br /&gt;
&lt;br /&gt;
A switch called &amp;lt;code&amp;gt;recordMode&amp;lt;/code&amp;gt; inside each test class determines if the image is generated &amp;lt;code&amp;gt;(true)&amp;lt;/code&amp;gt; or if the image is validated &amp;lt;code&amp;gt;(false)&amp;lt;/code&amp;gt;. The images are generated on the device &amp;lt;code&amp;gt;(/data/data/com.laimburggasse.aquariumcontrol.[module].test/files/screenshots_output/)&amp;lt;/code&amp;gt; and need to be downloaded to the repository.&lt;br /&gt;
&lt;br /&gt;
=== Unit tests ===&lt;br /&gt;
As of November 2025, there are no unit tests.&lt;br /&gt;
&lt;br /&gt;
== Release procedure ==&lt;br /&gt;
# Update &amp;lt;code&amp;gt;versionCode&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;versionName&amp;lt;/code&amp;gt; in &amp;lt;code&amp;gt;build.gradle.kts&amp;lt;/code&amp;gt; of &amp;lt;code&amp;gt;app&amp;lt;/code&amp;gt; module.&lt;br /&gt;
# Create a release branch (&amp;lt;code&amp;gt;release/[version]&amp;lt;/code&amp;gt;), checkout, commit and push.&lt;br /&gt;
# Change to release build variant.&lt;br /&gt;
# Build&lt;br /&gt;
# Generate a signed App bundle&lt;br /&gt;
# Upload to play store&lt;/div&gt;</summary>
		<author><name>Uwe</name></author>
	</entry>
	<entry>
		<id>http://217.79.180.177/mediawiki/index.php?title=Android_application&amp;diff=443</id>
		<title>Android application</title>
		<link rel="alternate" type="text/html" href="http://217.79.180.177/mediawiki/index.php?title=Android_application&amp;diff=443"/>
		<updated>2026-04-18T07:46:23Z</updated>

		<summary type="html">&lt;p&gt;Uwe: /* Implementation */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;The app is available in the [https://play.google.com/store/apps/details?id=com.laimburggasse.aquariumcontroller Play store].&lt;br /&gt;
== Requirements ==&lt;br /&gt;
== Architecture ==&lt;br /&gt;
The app is written completely in Kotlin using Jetpack Compose, Dagger/Hilt and aiming to implement a clean architecture.&lt;br /&gt;
=== Module structure ===&lt;br /&gt;
Besides the main app module, the app consists of the following library modules:&lt;br /&gt;
* balling&lt;br /&gt;
* common&lt;br /&gt;
* controller&lt;br /&gt;
* feed&lt;br /&gt;
* heating&lt;br /&gt;
* info&lt;br /&gt;
* overview&lt;br /&gt;
* refill&lt;br /&gt;
* schedule&lt;br /&gt;
* timedata&lt;br /&gt;
* ventilation&lt;br /&gt;
&lt;br /&gt;
The main &amp;lt;code&amp;gt;app&amp;lt;/code&amp;gt; module contains the AndroidManifest and the main activity (As of November 2025, the app only uses one activity).&lt;br /&gt;
The activity contains the navigation &amp;lt;code&amp;gt;NavHost&amp;lt;/code&amp;gt; in &amp;lt;code&amp;gt;onCreate&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
None of the other modules has a dependency to the main &amp;lt;code&amp;gt;app&amp;lt;/code&amp;gt; module.&lt;br /&gt;
&lt;br /&gt;
The &amp;lt;code&amp;gt;common&amp;lt;/code&amp;gt; module provides low-level functionalities and layout elements used in various places.&lt;br /&gt;
The following functionalities are located in &amp;lt;code&amp;gt;common&amp;lt;/code&amp;gt; module:&lt;br /&gt;
* &amp;lt;code&amp;gt;DataFetchResult&amp;lt;/code&amp;gt;: A wrapper class indicating the status of data fetching operation (&amp;lt;code&amp;gt;Success&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Error&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Loading&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Empty&amp;lt;/code&amp;gt;).&lt;br /&gt;
* &amp;lt;code&amp;gt;WebApiParams&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;WebApiPostRequest&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;WebApiPostRequestExecution&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;WebApiUrls&amp;lt;/code&amp;gt;: Classes and objects used for communication from the app to the server.&lt;br /&gt;
* &amp;lt;code&amp;gt;ControllerProfileDao&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;ControllerProfileDatabase&amp;lt;/code&amp;gt;: Classes which provide Room database functionality.&lt;br /&gt;
* Various classes used for dependency injection (Hilt)&lt;br /&gt;
* &amp;lt;code&amp;gt;SetValsSanityCheckResult&amp;lt;/code&amp;gt;: Class used for to communicate status of set values for &amp;lt;code&amp;gt;heating&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;ventilation&amp;lt;/code&amp;gt; module.&lt;br /&gt;
* &amp;lt;code&amp;gt;ControllerProfile&amp;lt;/code&amp;gt;: Class containing the profile data of the server (address, port, credentials, ...)&lt;br /&gt;
* &amp;lt;code&amp;gt;GlobalConstants&amp;lt;/code&amp;gt;: Mainly UI-related strings (non-context-related) and some minor functional constants&lt;br /&gt;
* Composable functions for the main drop down menu&lt;br /&gt;
* Composable functions for the theme&lt;br /&gt;
* Composable functions for hyperlinks, text edit fields, headline&lt;br /&gt;
* &amp;lt;code&amp;gt;ViewNavigationRoutes&amp;lt;/code&amp;gt;: Object containing the routing information processed in main &amp;lt;code&amp;gt;app&amp;lt;/code&amp;gt; module&lt;br /&gt;
* &amp;lt;code&amp;gt;ScreenshotTestHelper&amp;lt;/code&amp;gt;: Helper class for executing the instrumented snapshot testing using the emulator.&lt;br /&gt;
&lt;br /&gt;
=== Layer structure ===&lt;br /&gt;
The code is separated into the following layers:&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
[module]/&lt;br /&gt;
&lt;br /&gt;
├── src/main/kotlin/com/laimburggasse/aquariumcontrol/[module]/data/&lt;br /&gt;
&lt;br /&gt;
│   ├── remote/                   // Folder for data retrieval functionality&lt;br /&gt;
&lt;br /&gt;
│   │   └── dto/                  // Folder for Data Transfer Objects&lt;br /&gt;
&lt;br /&gt;
│   │       └── [module][name]Import.kt&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module][name]Reader.kt      // Implementation of GET server request&lt;br /&gt;
&lt;br /&gt;
│   ├── [module][name]RepositoryImpl.kt  // Repositories (implementation of interface)&lt;br /&gt;
&lt;br /&gt;
├── src/main/kotlin/com/laimburggasse/aquariumcontrol/[module]/domain/&lt;br /&gt;
&lt;br /&gt;
│   ├── model/                    // Folder for data definition classes&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module][name].kt.    // Classes for data definition&lt;br /&gt;
&lt;br /&gt;
│   ├── repository/               // Folder containing repository interface definitions&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module][name]Repository.kt.  // Interface defining the repository, used by view model&lt;br /&gt;
&lt;br /&gt;
├── src/main/kotlin/com/laimburggasse/aquariumcontrol/[module]/presentation/&lt;br /&gt;
&lt;br /&gt;
│   ├── entry/                    // First screen of feature&lt;br /&gt;
&lt;br /&gt;
│   ├── [components]/             // Shared code between different screens&lt;br /&gt;
&lt;br /&gt;
│   ├── [screen name]/            // Folder for each screen&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module][name]Screen.kt       // Composable showing the complete screen&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module][name]Composable.kt   // Composable showing elements inside the screen&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module][name]UiEvent.kt      // Sealed interface describing the communication from the composable to the view model&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module][name]UiState.kt      // Data class describing the content to be rendered by the composable&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module][name]ViewModel.kt    // Class implementing the view model&lt;br /&gt;
&lt;br /&gt;
│   ├── navigation/               // Folder containing class and functionality related to screen navigation&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module]NavigationEvent.kt  // One sealed class per module containing the navigation intent of the user&lt;br /&gt;
&lt;br /&gt;
│   │   └── [module]Navigator.kt  // Module-specific navigation functionality&lt;br /&gt;
&lt;br /&gt;
│&lt;br /&gt;
└── build.gradle                  // Plugins, setting, dependencies&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Example workflow for adding API communication ==&lt;br /&gt;
=== Implementation ===&lt;br /&gt;
Create the model: &amp;lt;code&amp;gt;RefillState&amp;lt;/code&amp;gt; as &amp;lt;code&amp;gt;enum&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Add a reader &amp;lt;code&amp;gt;class&amp;lt;/code&amp;gt; &amp;lt;code&amp;gt;RefillStateReader&amp;lt;/code&amp;gt; which uses the injected interface (Retrofit)&lt;br /&gt;
&lt;br /&gt;
Update the &amp;lt;code&amp;gt;RefillStateApiService&amp;lt;/code&amp;gt;: Add a function to load the state via Retrofit&lt;br /&gt;
&lt;br /&gt;
Add the label for the JSON property in &amp;lt;code&amp;gt;WebApiParams&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Add the URL for the end point in &amp;lt;code&amp;gt;WebApiUrls&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Add a wrapper &amp;lt;code&amp;gt;RefillStateResponse&amp;lt;/code&amp;gt; for the model to allow Retrofit parsing the JSON&lt;br /&gt;
&lt;br /&gt;
Update the repository interface and the repository interface implementation:&lt;br /&gt;
* Add a flow that delivers a &amp;lt;code&amp;gt;DataFetchResult&amp;lt;/code&amp;gt; of the &amp;lt;code&amp;gt;RefillState&amp;lt;/code&amp;gt;&lt;br /&gt;
* Add a function that allows the view model to trigger a refresh&lt;br /&gt;
&lt;br /&gt;
Add a wrapper &amp;lt;code&amp;gt;class&amp;lt;/code&amp;gt; &amp;lt;code&amp;gt;RefillStateUiState&amp;lt;/code&amp;gt;&lt;br /&gt;
Update the view model&lt;br /&gt;
* Add a flow that delivers a &amp;lt;code&amp;gt;RefillStateUiState&amp;lt;/code&amp;gt; based on the &amp;lt;code&amp;gt;DataFetchResult&amp;lt;/code&amp;gt; from repository&lt;br /&gt;
* Handle the new UI event for refreshing&lt;br /&gt;
&lt;br /&gt;
Add composable &amp;lt;code&amp;gt;RefillStateComposable&amp;lt;/code&amp;gt; for displaying the &amp;lt;code&amp;gt;RefillState&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Update &amp;lt;code&amp;gt;RefillUiEvent&amp;lt;/code&amp;gt; with an object for the refresh&lt;br /&gt;
&lt;br /&gt;
Update &amp;lt;code&amp;gt;RefillScreen&amp;lt;/code&amp;gt;:&lt;br /&gt;
* additional parameter (&amp;lt;code&amp;gt;RefillStateUiState&amp;lt;/code&amp;gt;) in composable function and in previews&lt;br /&gt;
* Added &amp;lt;code&amp;gt;PullToRefreshBox&amp;lt;/code&amp;gt;&lt;br /&gt;
* Added parsing of &amp;lt;code&amp;gt;RefillUiState&amp;lt;/code&amp;gt; and display of either&lt;br /&gt;
    * &amp;lt;code&amp;gt;RefillStateComposable&amp;lt;/code&amp;gt; in case of success&lt;br /&gt;
    * Text-based error message in case of error&lt;br /&gt;
    * &amp;lt;code&amp;gt;CircularProgressIndicator&amp;lt;/code&amp;gt; in case of loading&lt;br /&gt;
&lt;br /&gt;
Inject &amp;lt;code&amp;gt;RefillUiState&amp;lt;/code&amp;gt; into &amp;lt;code&amp;gt;RefillScreen&amp;lt;/code&amp;gt; in the &amp;lt;code&amp;gt;MainActivity&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;RefillScreenTest&amp;lt;/code&amp;gt; as well as in previews of &amp;lt;code&amp;gt;RefillScreen&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
(Optional) Check if &amp;lt;code&amp;gt;RefillDataModule&amp;lt;/code&amp;gt; needs update (not required in this case)&lt;br /&gt;
&lt;br /&gt;
=== Testing ===&lt;br /&gt;
Update existing screenshot testing&lt;br /&gt;
Add screenshot testing for new composable&lt;br /&gt;
&lt;br /&gt;
== Test strategy ==&lt;br /&gt;
The app uses different testing approaches:&lt;br /&gt;
* Compose preview screenshot testing&lt;br /&gt;
* Screenshot tests running as instrumented tests on emulator&lt;br /&gt;
* Unit tests&lt;br /&gt;
&lt;br /&gt;
=== Compose preview screenshot tests ===&lt;br /&gt;
These tests run comparably fast and device-independent.&lt;br /&gt;
&lt;br /&gt;
The compose preview screenshot tests cover portrait mode/landscape mode, light/dark mode (four variants).&lt;br /&gt;
&lt;br /&gt;
Limitations:&lt;br /&gt;
* Two tests within &amp;lt;code&amp;gt;feed&amp;lt;/code&amp;gt; module can only run in landscape mode (&amp;lt;code&amp;gt;FeedProfileScheduleScreenTest&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;FeedHistoryScreenTest&amp;lt;/code&amp;gt;) due to excessive heap memory utilisation.&lt;br /&gt;
* &amp;lt;code&amp;gt;Info&amp;lt;/code&amp;gt; screen is not tested due to &amp;lt;code&amp;gt;LibrariesContainer&amp;lt;/code&amp;gt; (dynamic content which cannot be mocked).&lt;br /&gt;
&lt;br /&gt;
The tests are located in &amp;lt;code&amp;gt;[module]/src/screenshotTest/kotlin/com/aquariumcontrol/[module]/&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
The references are located in &amp;lt;code&amp;gt;[module]/src/screenshotTestDebug/kotlin/com/aquariumcontrol/[module]/&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
The test reports are located in &amp;lt;code&amp;gt;build/reports/screenshotTest/preview/debug/&amp;lt;/code&amp;gt;. The test reports for the compose screenshot tests contain reference, actual and delta images.&lt;br /&gt;
&lt;br /&gt;
The images are generated with &amp;lt;code&amp;gt;./gradlew [module]:updateDebugScreenshotTest&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
The images are validated with &amp;lt;code&amp;gt;./gradlew [module]:validateDebugScreenshotTest&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Omitting the module will execute all tests.&lt;br /&gt;
&lt;br /&gt;
=== Instrumented tests ===&lt;br /&gt;
The tests run comparably long and are bound to an emulator device.&lt;br /&gt;
As of November 2025, a Pixel 5 API S is used for the instrumented test.&lt;br /&gt;
The instrumented tests only test the complete screens.&lt;br /&gt;
&lt;br /&gt;
Limitations:&lt;br /&gt;
* &amp;lt;code&amp;gt;Info&amp;lt;/code&amp;gt; screen is not tested due to &amp;lt;code&amp;gt;LibrariesContainer&amp;lt;/code&amp;gt; (dynamic content which cannot be mocked and also does not allow execution of instrumented test).&lt;br /&gt;
&lt;br /&gt;
The test reports are located in &amp;lt;code&amp;gt;build/reports/androidTests/connected/debug/&amp;lt;/code&amp;gt;. The test reports for the compose screenshot tests do not contain any images.&lt;br /&gt;
&lt;br /&gt;
The tests are executed with &amp;lt;code&amp;gt;./gradlew [module]:connectedDebugAndroidTest&amp;lt;/code&amp;gt;.&lt;br /&gt;
Omitting the module will execute all tests.&lt;br /&gt;
&lt;br /&gt;
A switch called &amp;lt;code&amp;gt;recordMode&amp;lt;/code&amp;gt; inside each test class determines if the image is generated &amp;lt;code&amp;gt;(true)&amp;lt;/code&amp;gt; or if the image is validated &amp;lt;code&amp;gt;(false)&amp;lt;/code&amp;gt;. The images are generated on the device &amp;lt;code&amp;gt;(/data/data/com.laimburggasse.aquariumcontrol.[module].test/files/screenshots_output/)&amp;lt;/code&amp;gt; and need to be downloaded to the repository.&lt;br /&gt;
&lt;br /&gt;
=== Unit tests ===&lt;br /&gt;
As of November 2025, there are no unit tests.&lt;br /&gt;
&lt;br /&gt;
== Release procedure ==&lt;br /&gt;
# Update &amp;lt;code&amp;gt;versionCode&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;versionName&amp;lt;/code&amp;gt; in &amp;lt;code&amp;gt;build.gradle.kts&amp;lt;/code&amp;gt; of &amp;lt;code&amp;gt;app&amp;lt;/code&amp;gt; module.&lt;br /&gt;
# Create a release branch (&amp;lt;code&amp;gt;release/[version]&amp;lt;/code&amp;gt;), checkout, commit and push.&lt;br /&gt;
# Change to release build variant.&lt;br /&gt;
# Build&lt;br /&gt;
# Generate a signed App bundle&lt;br /&gt;
# Upload to play store&lt;/div&gt;</summary>
		<author><name>Uwe</name></author>
	</entry>
</feed>