feat: implement actuator control with Koin DI and API integration

This commit is contained in:
Cutiful 2025-06-29 07:53:10 +07:00
parent 287d5774e8
commit f48a96c96c
18 changed files with 649 additions and 259 deletions

View File

@ -1,20 +1,37 @@
package com.syaroful.agrilinkvocpro.control_feature
import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import com.syaroful.agrilinkvocpro.control_feature.page.ControlActuatorScreen
import androidx.navigation.compose.rememberNavController
import com.syaroful.agrilinkvocpro.control_feature.di.controlModule
import com.syaroful.agrilinkvocpro.control_feature.di.networkModule
import com.syaroful.agrilinkvocpro.control_feature.di.viewModelModule
import com.syaroful.agrilinkvocpro.control_feature.navigation.NavGraph
import com.syaroful.agrilinkvocpro.control_feature.ui.theme.AgrilinkVocproTheme
import org.koin.core.context.loadKoinModules
import org.koin.core.context.unloadKoinModules
class ControlActuatorActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// load Module
loadKoinModules(listOf(controlModule, networkModule, viewModelModule))
Log.d("KOIN_DEBUG", "✅ controlModule loaded, yeay 🤗")
enableEdgeToEdge()
setContent {
AgrilinkVocproTheme {
ControlActuatorScreen()
val navController = rememberNavController()
NavGraph(navController)
}
}
}
override fun onDestroy() {
super.onDestroy()
unloadKoinModules(listOf(controlModule, networkModule, viewModelModule))
Log.d("KOIN_DEBUG", "🧹 controlModule unloaded")
}
}

View File

@ -1,4 +1,4 @@
package com.syaroful.agrilinkvocpro.control_feature.components
package com.syaroful.agrilinkvocpro.control_feature.core.components
import androidx.compose.foundation.background
import androidx.compose.foundation.border
@ -14,6 +14,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Switch
@ -38,6 +39,7 @@ fun ControlCard(
iconRes: Int,
label: String,
isOn: Boolean,
isLoading: Boolean,
onToggle: (Boolean) -> Unit
) {
val backgroundColor = if (isOn) DarkGreen else MaterialTheme.colorScheme.surfaceContainer
@ -75,6 +77,9 @@ fun ControlCard(
modifier = Modifier.size(20.dp)
)
}
if (isLoading) {
CircularProgressIndicator(color = MainGreen)
} else {
Switch(
checked = isOn,
onCheckedChange = onToggle,
@ -87,6 +92,7 @@ fun ControlCard(
)
)
}
}
Spacer(modifier = Modifier.height(16.dp))

View File

@ -0,0 +1,102 @@
package com.syaroful.agrilinkvocpro.control_feature.core.components
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.syaroful.agrilinkvocpro.control_feature.R
@Composable
fun DisableControlCard(
label: String,
isOn: Boolean,
iconRes: Int = R.drawable.ic_valve,
backgroundColor: Color = Color.Gray.copy(alpha = 0.1f),
iconTint: Color = Color.Gray,
textColor: Color = Color.Gray,
) {
Column(
modifier = Modifier
.padding(vertical = 8.dp)
.clip(RoundedCornerShape(12.dp))
.border(1.dp, backgroundColor.copy(alpha = 0.4f), RoundedCornerShape(12.dp))
.background(backgroundColor)
.padding(16.dp)
.fillMaxWidth()
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier.fillMaxWidth()
) {
Box(
modifier = Modifier
.size(32.dp)
.background(
color = iconTint.copy(alpha = 0.1f),
shape = CircleShape
),
contentAlignment = Alignment.Center
) {
Icon(
painter = painterResource(id = iconRes),
contentDescription = null,
tint = iconTint,
modifier = Modifier.size(20.dp)
)
}
// Switch(
// checked = isOn,
// onCheckedChange = onToggle,
// colors = SwitchDefaults.colors(
// checkedThumbColor = Color.LightGray,
// uncheckedThumbColor = Color.LightGray,
// uncheckedBorderColor = trackColor,
// checkedTrackColor = backgroundColor,
// checkedBorderColor = trackColor,
// uncheckedTrackColor = trackColor
// )
// )
}
Spacer(modifier = Modifier.height(16.dp))
Text(label, color = textColor)
Text(
if (isOn) "On" else "Off",
color = Color.Gray,
fontWeight = FontWeight.Bold
)
}
}
@Preview
@Composable
fun DisableControlCardPreview() {
DisableControlCard(
label = "Water Valve",
isOn = false,
)
}

View File

@ -0,0 +1,8 @@
package com.syaroful.agrilinkvocpro.control_feature.core.state
sealed class ControlState<out T> {
data object Idle : ControlState<Nothing>()
data object Loading : ControlState<Nothing>()
data class Success<T>(val data: T?) : ControlState<T>()
data class Error(val message: String) : ControlState<Nothing>()
}

View File

@ -0,0 +1,13 @@
package com.syaroful.agrilinkvocpro.control_feature.data.model
data class ActuatorStatusResponse(
val `data`: List<ActuatorStatus?>?,
val message: String?
)
data class ActuatorStatus(
val currentStatus: String?,
val lastChangedAt: String?,
val lastTriggeredBy: String?,
val name: String?
)

View File

@ -0,0 +1,7 @@
package com.syaroful.agrilinkvocpro.control_feature.data.model
enum class ActuatorType(val displayName: String) {
WATER("Water Valve"),
NUTRIENT("Nutrient Valve"),
PUMP("Pump")
}

View File

@ -0,0 +1,14 @@
package com.syaroful.agrilinkvocpro.control_feature.data.model
data class ControlLogResponse(
val log: Log?,
val message: String?
)
data class Log(
val action: String?,
val actuatorId: Int?,
val createdAt: String?,
val id: Int?,
val triggeredBy: String?
)

View File

@ -0,0 +1,34 @@
package com.syaroful.agrilinkvocpro.control_feature.data.network
import com.syaroful.agrilinkvocpro.control_feature.data.model.ActuatorStatusResponse
import com.syaroful.agrilinkvocpro.control_feature.data.model.ControlLogResponse
import retrofit2.Response
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.Header
import retrofit2.http.POST
interface ControlService {
@GET("api/actuators/status")
suspend fun getActuatorStatus(
@Header("Authorization") authHeader: String,
): Response<ActuatorStatusResponse>
@POST("api/actuators/water-valve/control")
suspend fun controlWaterValve(
@Header("Authorization") authHeader: String,
@Body action: String,
): Response<ControlLogResponse>
@POST("api/actuators/nutrient-valve/control")
suspend fun controlNutrientValve(
@Header("Authorization") authHeader: String,
@Body action: String,
): Response<ControlLogResponse>
@POST("api/actuators/pump/control")
suspend fun controlPump(
@Header("Authorization") authHeader: String,
@Body action: String,
): Response<ControlLogResponse>
}

View File

@ -0,0 +1,28 @@
package com.syaroful.agrilinkvocpro.control_feature.data.repository
import com.syaroful.agrilinkvocpro.control_feature.data.model.ActuatorStatusResponse
import com.syaroful.agrilinkvocpro.control_feature.data.model.ControlLogResponse
import com.syaroful.agrilinkvocpro.control_feature.data.network.ControlService
import retrofit2.Response
class ControlRepository(
private val controlService: ControlService
) {
suspend fun controlWaterValve(authHeader: String, action: String): Response<ControlLogResponse>{
return controlService.controlWaterValve(authHeader, action)
}
suspend fun controlNutrientValve(authHeader: String, action: String): Response<ControlLogResponse>{
return controlService.controlNutrientValve(authHeader, action)
}
suspend fun controlPump(authHeader: String, action: String): Response<ControlLogResponse>{
return controlService.controlPump(authHeader, action)
}
suspend fun getActuatorStatus(authHeader: String): Response<ActuatorStatusResponse> {
return controlService.getActuatorStatus(authHeader)
}
}

View File

@ -0,0 +1,8 @@
package com.syaroful.agrilinkvocpro.control_feature.di
import com.syaroful.agrilinkvocpro.control_feature.data.repository.ControlRepository
import org.koin.dsl.module
val controlModule = module {
single{ControlRepository(get())}
}

View File

@ -0,0 +1,27 @@
package com.syaroful.agrilinkvocpro.control_feature.di
import com.syaroful.agrilinkvocpro.control_feature.data.network.ControlService
import okhttp3.OkHttpClient
import org.koin.dsl.module
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import java.util.concurrent.TimeUnit
val networkModule = module {
single {
OkHttpClient.Builder()
.connectTimeout(3, TimeUnit.SECONDS)
.build()
}
single {
Retrofit.Builder()
.baseUrl("http://labai.polinema.ac.id:3042/")
.client(get())
.addConverterFactory(GsonConverterFactory.create())
.build()
}
single<ControlService> { get<Retrofit>().create(ControlService::class.java) }
}

View File

@ -0,0 +1,9 @@
package com.syaroful.agrilinkvocpro.control_feature.di
import com.syaroful.agrilinkvocpro.control_feature.presentation.control.ControlViewModel
import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.dsl.module
val viewModelModule = module {
viewModel { ControlViewModel(get(), get()) }
}

View File

@ -0,0 +1,24 @@
package com.syaroful.agrilinkvocpro.control_feature.navigation
import androidx.compose.foundation.layout.Box
import androidx.compose.runtime.Composable
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import com.syaroful.agrilinkvocpro.control_feature.presentation.control.ControlActuatorScreen
import com.syaroful.agrilinkvocpro.control_feature.presentation.control.ControlViewModel
import org.koin.androidx.compose.koinViewModel
@Composable
fun NavGraph(navController: NavHostController) {
val controlViewModel: ControlViewModel = koinViewModel()
NavHost(navController, startDestination = "control_screen") {
composable("control_screen") {
ControlActuatorScreen(controlViewModel)
}
composable("history_control_screen") {
Box() {}
}
}
}

View File

@ -1,114 +0,0 @@
package com.syaroful.agrilinkvocpro.control_feature.page
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Info
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.syaroful.agrilinkvocpro.control_feature.R
import com.syaroful.agrilinkvocpro.control_feature.components.ControlCard
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ControlActuatorScreen() {
Scaffold(
topBar = {
TopAppBar(
title = {
Column(horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.fillMaxWidth()) {
Text("Control Actuator", style = MaterialTheme.typography.titleMedium)
}
},
navigationIcon = {
IconButton(onClick = { }) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back")
}
},
actions = {
IconButton(onClick = { }) {
Icon(Icons.Filled.Info, contentDescription = "History")
}
}
)
}
) { innerPadding ->
Column(
modifier = Modifier
.padding(innerPadding)
.padding(16.dp)
.verticalScroll(rememberScrollState())
) {
Text("Penyiraman Air", style = MaterialTheme.typography.titleMedium)
Spacer(modifier = Modifier.height(8.dp))
ControlGrid(
iconRes = R.drawable.ic_valve,
items = List(4) { index -> "Bet ${index + 1}" }
)
Spacer(modifier = Modifier.height(16.dp))
Text("Penyiraman Pupuk", style = MaterialTheme.typography.titleMedium)
Spacer(modifier = Modifier.height(8.dp))
ControlGrid(
iconRes = R.drawable.ic_leaf,
items = List(4) { index -> "Bet ${index + 1}" }
)
}
}
}
@Composable
fun ControlGrid(iconRes: Int, items: List<String>) {
val states = remember { items.map { mutableStateOf(false) } }
val gridHeight = (items.size / 2.0).coerceAtLeast(1.0) * 170
LazyVerticalGrid(
columns = GridCells.Fixed(2),
modifier = Modifier.fillMaxWidth().height(gridHeight.dp),
verticalArrangement = Arrangement.spacedBy(12.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
items(items.size) { index ->
ControlCard(
iconRes = iconRes,
label = items[index],
isOn = states[index].value,
onToggle = { states[index].value = it }
)
}
}
}
@Preview
@Composable
fun ControlActuatorScreenPreview() {
ControlActuatorScreen()
}

View File

@ -1,75 +1,75 @@
package com.syaroful.agrilinkvocpro.control_feature.page
import android.content.res.Configuration.UI_MODE_NIGHT_YES
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.syaroful.agrilinkvocpro.control_feature.viewModel.ControlViewModel
@Composable
fun ControlScreen(
modifier: Modifier = Modifier,
relayState: Boolean,
onRelayStateChange: (Boolean) -> Unit
) {
Scaffold { innerPadding ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(innerPadding)
.padding(24.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(text = "Kontrol Relay", style = MaterialTheme.typography.headlineMedium)
Spacer(modifier = Modifier.height(24.dp))
Switch(
checked = relayState,
onCheckedChange = { isChecked ->
onRelayStateChange(isChecked)
}
)
}
}
}
@Composable
fun ControlScreenRoute(
modifier: Modifier = Modifier,
viewModel: ControlViewModel = viewModel()
) {
val relayState by viewModel.relayState.collectAsState()
ControlScreen(
modifier = modifier,
relayState = relayState,
onRelayStateChange = { viewModel.setRelayState(it) }
)
}
@Preview(showBackground = true, name = "Light Mode")
@Preview(showBackground = true, name = "Dark Mode", uiMode = UI_MODE_NIGHT_YES)
@Composable
fun ControlScreenPreview() {
ControlScreen(
relayState = false,
onRelayStateChange = {}
)
}
//package com.syaroful.agrilinkvocpro.control_feature.page
//
//import android.content.res.Configuration.UI_MODE_NIGHT_YES
//import androidx.compose.foundation.layout.Arrangement
//import androidx.compose.foundation.layout.Column
//import androidx.compose.foundation.layout.Spacer
//import androidx.compose.foundation.layout.fillMaxSize
//import androidx.compose.foundation.layout.height
//import androidx.compose.foundation.layout.padding
//import androidx.compose.material3.MaterialTheme
//import androidx.compose.material3.Scaffold
//import androidx.compose.material3.Switch
//import androidx.compose.material3.Text
//import androidx.compose.runtime.Composable
//import androidx.compose.runtime.collectAsState
//import androidx.compose.runtime.getValue
//import androidx.compose.ui.Alignment
//import androidx.compose.ui.Modifier
//import androidx.compose.ui.tooling.preview.Preview
//import androidx.compose.ui.unit.dp
//import androidx.lifecycle.viewmodel.compose.viewModel
//import com.syaroful.agrilinkvocpro.control_feature.presentation.control.ControlViewModel
//
//@Composable
//fun ControlScreen(
// modifier: Modifier = Modifier,
// relayState: Boolean,
// onRelayStateChange: (Boolean) -> Unit
//) {
// Scaffold { innerPadding ->
// Column(
// modifier = Modifier
// .fillMaxSize()
// .padding(innerPadding)
// .padding(24.dp),
// verticalArrangement = Arrangement.Center,
// horizontalAlignment = Alignment.CenterHorizontally
// ) {
// Text(text = "Kontrol Relay", style = MaterialTheme.typography.headlineMedium)
//
// Spacer(modifier = Modifier.height(24.dp))
//
// Switch(
// checked = relayState,
// onCheckedChange = { isChecked ->
// onRelayStateChange(isChecked)
// }
// )
// }
// }
//}
//
//@Composable
//fun ControlScreenRoute(
// modifier: Modifier = Modifier,
// viewModel: ControlViewModel = viewModel()
//) {
// val relayState by viewModel.relayState.collectAsState()
//
// ControlScreen(
// modifier = modifier,
// relayState = relayState,
// onRelayStateChange = { viewModel.setRelayState(it) }
// )
//}
//
//@Preview(showBackground = true, name = "Light Mode")
//@Preview(showBackground = true, name = "Dark Mode", uiMode = UI_MODE_NIGHT_YES)
//@Composable
//fun ControlScreenPreview() {
// ControlScreen(
// relayState = false,
// onRelayStateChange = {}
// )
//}

View File

@ -0,0 +1,155 @@
package com.syaroful.agrilinkvocpro.control_feature.presentation.control
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Info
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.syaroful.agrilinkvocpro.control_feature.R
import com.syaroful.agrilinkvocpro.control_feature.core.components.ControlCard
import com.syaroful.agrilinkvocpro.control_feature.core.components.DisableControlCard
import com.syaroful.agrilinkvocpro.control_feature.core.state.ControlState
import com.syaroful.agrilinkvocpro.control_feature.data.model.ActuatorType
import com.syaroful.agrilinkvocpro.core.placeholder.shimmerEffect
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ControlActuatorScreen(
viewModel: ControlViewModel
) {
val allActuators by viewModel.allActuatorState.collectAsState()
val waterValveStatus by viewModel.waterValveStatus.collectAsState()
val nutritionValveStatus by viewModel.nutritionValveStatus.collectAsState()
val pumpStatus by viewModel.pumpStatus.collectAsState()
val isLoading by viewModel.isLoading.collectAsState()
val isRefreshing = remember { mutableStateOf(false) }
LaunchedEffect(Unit) {
viewModel.getActuatorStatus()
}
Scaffold(
topBar = {
TopAppBar(
title = {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxWidth()
) {
Text("Control Actuator", style = MaterialTheme.typography.titleMedium)
}
},
navigationIcon = {
IconButton(onClick = { }) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back")
}
},
actions = {
IconButton(onClick = { }) {
Icon(Icons.Filled.Info, contentDescription = "History")
}
}
)
}
) { innerPadding ->
PullToRefreshBox(
isRefreshing = isRefreshing.value,
onRefresh = {
isRefreshing.value = true
viewModel.getActuatorStatus()
}
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(innerPadding)
.padding(16.dp)
.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Text("Daftar Actuator", style = MaterialTheme.typography.titleMedium)
// ControlGrid(
// iconRes = R.drawable.ic_valve,
// items = List(4) { index -> "Bet ${index + 1}" }
// )
when (allActuators) {
is ControlState.Loading -> {
isRefreshing.value = false
Box(
modifier = Modifier
.fillMaxWidth()
.height(96.dp)
.shimmerEffect()
)
}
is ControlState.Error -> {
isRefreshing.value = false
DisableControlCard(
label = "Control Failed!",
isOn = false,
)
}
ControlState.Idle -> {
isRefreshing.value = false
}
is ControlState.Success<*> -> {
isRefreshing.value = false
ControlCard(
iconRes = R.drawable.ic_valve,
label = "Water Valve",
isOn = waterValveStatus,
isLoading = isLoading,
onToggle = { viewModel.controlActuator(ActuatorType.WATER, !waterValveStatus) }
)
ControlCard(
iconRes = R.drawable.ic_leaf,
label = "Nutrient Valve",
isOn = nutritionValveStatus,
isLoading = isLoading,
onToggle = { viewModel.controlActuator(ActuatorType.NUTRIENT, !nutritionValveStatus) }
)
ControlCard(
iconRes = R.drawable.ic_valve,
label = "Pump",
isOn = pumpStatus,
isLoading = isLoading,
onToggle = { viewModel.controlActuator(ActuatorType.PUMP, !pumpStatus) }
)
}
}
}
}
}
}

View File

@ -0,0 +1,109 @@
package com.syaroful.agrilinkvocpro.control_feature.presentation.control
import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.syaroful.agrilinkvocpro.control_feature.core.state.ControlState
import com.syaroful.agrilinkvocpro.control_feature.data.model.ActuatorStatusResponse
import com.syaroful.agrilinkvocpro.control_feature.data.model.ActuatorType
import com.syaroful.agrilinkvocpro.control_feature.data.repository.ControlRepository
import com.syaroful.agrilinkvocpro.data.UserPreferences
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
private const val TAG = "ControlViewModel"
class ControlViewModel(
private val userPreferences: UserPreferences,
private val repository: ControlRepository
) : ViewModel() {
private val _allActuatorState =
MutableStateFlow<ControlState<ActuatorStatusResponse>>(ControlState.Idle)
val allActuatorState: StateFlow<ControlState<ActuatorStatusResponse>> = _allActuatorState
private val _waterValveStatus = MutableStateFlow<Boolean>(false)
val waterValveStatus: StateFlow<Boolean> = _waterValveStatus
private val _nutritionValveStatus = MutableStateFlow<Boolean>(false)
val nutritionValveStatus: StateFlow<Boolean> = _nutritionValveStatus
private val _pumpStatus = MutableStateFlow<Boolean>(false)
val pumpStatus: StateFlow<Boolean> = _pumpStatus
private val _isLoading = MutableStateFlow<Boolean>(false)
val isLoading: StateFlow<Boolean> = _isLoading
fun getActuatorStatus() {
_allActuatorState.value = ControlState.Loading
viewModelScope.launch {
val token = userPreferences.tokenFlow.first()
val authHeader = "Bearer $token"
try {
delay(500L)
val response = repository.getActuatorStatus(authHeader)
if (response.isSuccessful) {
val data = response.body()
_allActuatorState.value = ControlState.Success(data)
data?.data?.forEach { actuator ->
val isOn = actuator?.currentStatus == "ON"
when (actuator?.name) {
ActuatorType.WATER.displayName -> _waterValveStatus.value = isOn
ActuatorType.NUTRIENT.displayName -> _nutritionValveStatus.value = isOn
ActuatorType.PUMP.displayName -> _pumpStatus.value = isOn
}
}
} else {
val errorBody = response.errorBody()?.string()
val errorMessage = errorBody ?: "Error ${response.code()}"
_allActuatorState.value = ControlState.Error(errorMessage)
}
} catch (e: Exception) {
_allActuatorState.value = ControlState.Error(e.message ?: "Unknown error")
}
}
}
fun controlActuator(actuator: ActuatorType, state: Boolean) {
_isLoading.value = true
viewModelScope.launch {
val token = userPreferences.tokenFlow.first()
val authHeader = "Bearer $token"
val stringState = if (state) "ON" else "OFF"
try {
delay(500L)
val response = when (actuator) {
ActuatorType.WATER -> repository.controlWaterValve(authHeader, stringState)
ActuatorType.NUTRIENT -> repository.controlNutrientValve(authHeader, stringState)
ActuatorType.PUMP -> repository.controlPump(authHeader, stringState)
}
if (response.isSuccessful) {
val isOn = response.body()?.log?.action == "ON"
updateActuatorStatus(actuator, isOn)
Log.d(TAG, "Successfully controlled ${actuator.displayName}")
} else {
val errorBody = response.errorBody()?.string()
Log.e(TAG, "Error controlling ${actuator.displayName}: $errorBody")
}
} catch (e: Exception) {
Log.e(TAG, "Exception while controlling ${actuator.displayName}: ${e.message}")
} finally {
_isLoading.value = false
}
}
}
private fun updateActuatorStatus(actuator: ActuatorType, isOn: Boolean) {
when (actuator) {
ActuatorType.WATER -> _waterValveStatus.value = isOn
ActuatorType.NUTRIENT -> _nutritionValveStatus.value = isOn
ActuatorType.PUMP -> _pumpStatus.value = isOn
}
}
}

View File

@ -1,57 +0,0 @@
package com.syaroful.agrilinkvocpro.control_feature.viewModel
import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.google.firebase.database.DataSnapshot
import com.google.firebase.database.DatabaseError
import com.google.firebase.database.ValueEventListener
import com.google.firebase.database.ktx.database
import com.google.firebase.ktx.Firebase
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
private const val TAG = "ControlViewModel"
class ControlViewModel : ViewModel() {
private val _relayState = MutableStateFlow(false)
val relayState: StateFlow<Boolean> = _relayState
private val database = Firebase.database
private val relayRef = database.getReference("relay1/state")
init {
observeRelayState()
}
private fun observeRelayState() {
relayRef.get()
.addOnSuccessListener { snapshot ->
val value = snapshot.getValue(Int::class.java) ?: 0
_relayState.value = (value == 1)
}.addOnFailureListener{
Log.e(TAG, "Failed to fetch initial relay state", it)
}
relayRef.addValueEventListener(object : ValueEventListener {
override fun onDataChange(snapshot: DataSnapshot) {
val value = snapshot.getValue(Int::class.java) ?: 0
_relayState.value = (value == 1)
}
override fun onCancelled(error: DatabaseError) {
Log.e(TAG, "Failed to observe relay state", error.toException())
}
})
}
fun setRelayState(isOn: Boolean) {
viewModelScope.launch {
relayRef.setValue(if (isOn) 1 else 0)
.addOnFailureListener {
Log.e(TAG, "Failed to set relay state", it)
}
}
}
}