feat: adacam migration — update IP, pairing, bearer auth, wifi/ssh config, remove JSch and cmd
- Default API URL: 192.168.0.10 → 10.77.0.1 - BeeApiClient: apiToken field, bearer auth on POST endpoints, pair() method, getSshStatus(), setSshEnabled() — JSch/SSH-from-app removed entirely - Models: PairResponse, SshStatus, WifiStatus data classes - SettingsDataStore: deviceSerial, apiToken, isPaired persistence + deriveApiToken() - SettingsViewModel: pairDevice(), connectWifi(), toggleSsh(), refreshSshStatus(), refreshWifiStatus(), isPaired/deviceSerial/sshStatus/wifiStatus StateFlows - SettingsScreen: Pairing section, Home WiFi config section, SSH toggle section renamed BEE DEVICE → ADACAM DEVICE, hint URL updated - build.gradle: removed JSch dependency (no longer SSHing from app)
This commit is contained in:
parent
60d2f693d1
commit
d11f6b62d1
5 changed files with 430 additions and 8 deletions
|
|
@ -77,7 +77,7 @@ dependencies {
|
|||
// WorkManager (background uploads)
|
||||
implementation(libs.work.runtime.ktx)
|
||||
// SSH connectivity for device_id fallback
|
||||
implementation("com.jcraft:jsch:0.1.55")
|
||||
|
||||
// QR Code scanning
|
||||
implementation("com.google.zxing:core:3.5.2")
|
||||
implementation("com.journeyapps:zxing-android-embedded:4.3.0")
|
||||
|
|
|
|||
|
|
@ -122,6 +122,21 @@ data class BeePlugin(
|
|||
@SerializedName("running") val running: Boolean? = null
|
||||
)
|
||||
|
||||
// ── Pairing API ───────────────────────────────────────────────────────────────
|
||||
|
||||
data class PairResponse(
|
||||
@SerializedName("serial") val serial: String,
|
||||
@SerializedName("version") val version: String,
|
||||
@SerializedName("ap_ip") val apIp: String,
|
||||
@SerializedName("api_port") val apiPort: Int
|
||||
)
|
||||
|
||||
// ── SSH API ───────────────────────────────────────────────────────────────────
|
||||
|
||||
data class SshStatus(
|
||||
@SerializedName("active") val active: Boolean
|
||||
)
|
||||
|
||||
// ── Bee Settings API models ───────────────────────────────────────────────────
|
||||
|
||||
data class WifiClientSettings(
|
||||
|
|
|
|||
|
|
@ -10,11 +10,12 @@ import androidx.datastore.preferences.core.stringPreferencesKey
|
|||
import androidx.datastore.preferences.preferencesDataStore
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import java.security.MessageDigest
|
||||
|
||||
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "varroa_settings")
|
||||
|
||||
data class VarroaSettings(
|
||||
val beeApiUrl: String = "http://192.168.0.10:5000",
|
||||
val beeApiUrl: String = "http://10.77.0.1:5000",
|
||||
val adamapsApiUrl: String = "https://api.adamaps.org",
|
||||
val adamapsApiKey: String = "adamaps-ingest-2026",
|
||||
val pollIntervalSeconds: Int = 30,
|
||||
|
|
@ -22,9 +23,23 @@ data class VarroaSettings(
|
|||
val cameraRefreshSeconds: Int = 30,
|
||||
val forwardingEnabled: Boolean = true,
|
||||
val cachedDeviceId: String = "unknown",
|
||||
val walletAddress: String = ""
|
||||
val walletAddress: String = "",
|
||||
// AdaCam pairing
|
||||
val deviceSerial: String = "",
|
||||
val apiToken: String = "",
|
||||
val isPaired: Boolean = false
|
||||
)
|
||||
|
||||
/**
|
||||
* Derive the API token from the device serial.
|
||||
* Token = first 32 chars of SHA-256("adacam-api-{serial}-token")
|
||||
*/
|
||||
fun deriveApiToken(serial: String): String {
|
||||
val input = "adacam-api-$serial-token"
|
||||
val bytes = MessageDigest.getInstance("SHA-256").digest(input.toByteArray())
|
||||
return bytes.joinToString("") { "%02x".format(it) }.substring(0, 32)
|
||||
}
|
||||
|
||||
class SettingsDataStore(private val context: Context) {
|
||||
|
||||
companion object {
|
||||
|
|
@ -37,11 +52,15 @@ class SettingsDataStore(private val context: Context) {
|
|||
private val KEY_FORWARDING_ENABLED = booleanPreferencesKey("forwarding_enabled")
|
||||
private val KEY_CACHED_DEVICE_ID = stringPreferencesKey("cached_device_id")
|
||||
private val KEY_WALLET_ADDRESS = stringPreferencesKey("wallet_address")
|
||||
// AdaCam pairing keys
|
||||
private val KEY_DEVICE_SERIAL = stringPreferencesKey("device_serial")
|
||||
private val KEY_API_TOKEN = stringPreferencesKey("api_token")
|
||||
private val KEY_IS_PAIRED = booleanPreferencesKey("is_paired")
|
||||
}
|
||||
|
||||
val settings: Flow<VarroaSettings> = context.dataStore.data.map { prefs ->
|
||||
VarroaSettings(
|
||||
beeApiUrl = prefs[KEY_BEE_URL] ?: "http://192.168.0.10:5000",
|
||||
beeApiUrl = prefs[KEY_BEE_URL] ?: "http://10.77.0.1:5000",
|
||||
adamapsApiUrl = prefs[KEY_ADAMAPS_URL] ?: "https://api.adamaps.org",
|
||||
adamapsApiKey = prefs[KEY_ADAMAPS_KEY] ?: "adamaps-ingest-2026",
|
||||
pollIntervalSeconds = prefs[KEY_POLL_INTERVAL] ?: 30,
|
||||
|
|
@ -49,7 +68,10 @@ class SettingsDataStore(private val context: Context) {
|
|||
cameraRefreshSeconds = prefs[KEY_CAMERA_REFRESH] ?: 30,
|
||||
forwardingEnabled = prefs[KEY_FORWARDING_ENABLED] ?: true,
|
||||
cachedDeviceId = prefs[KEY_CACHED_DEVICE_ID] ?: "unknown",
|
||||
walletAddress = prefs[KEY_WALLET_ADDRESS] ?: ""
|
||||
walletAddress = prefs[KEY_WALLET_ADDRESS] ?: "",
|
||||
deviceSerial = prefs[KEY_DEVICE_SERIAL] ?: "",
|
||||
apiToken = prefs[KEY_API_TOKEN] ?: "",
|
||||
isPaired = prefs[KEY_IS_PAIRED] ?: false
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -64,6 +86,9 @@ class SettingsDataStore(private val context: Context) {
|
|||
prefs[KEY_FORWARDING_ENABLED] = s.forwardingEnabled
|
||||
prefs[KEY_CACHED_DEVICE_ID] = s.cachedDeviceId
|
||||
prefs[KEY_WALLET_ADDRESS] = s.walletAddress
|
||||
prefs[KEY_DEVICE_SERIAL] = s.deviceSerial
|
||||
prefs[KEY_API_TOKEN] = s.apiToken
|
||||
prefs[KEY_IS_PAIRED] = s.isPaired
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -72,4 +97,28 @@ class SettingsDataStore(private val context: Context) {
|
|||
prefs[KEY_CACHED_DEVICE_ID] = deviceId
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Store pairing data after successful pairing with AdaCam.
|
||||
* Derives and stores the API token from the serial.
|
||||
*/
|
||||
suspend fun savePairing(serial: String) {
|
||||
val token = deriveApiToken(serial)
|
||||
context.dataStore.edit { prefs ->
|
||||
prefs[KEY_DEVICE_SERIAL] = serial
|
||||
prefs[KEY_API_TOKEN] = token
|
||||
prefs[KEY_IS_PAIRED] = true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear pairing data (for re-pairing or reset).
|
||||
*/
|
||||
suspend fun clearPairing() {
|
||||
context.dataStore.edit { prefs ->
|
||||
prefs[KEY_DEVICE_SERIAL] = ""
|
||||
prefs[KEY_API_TOKEN] = ""
|
||||
prefs[KEY_IS_PAIRED] = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,6 +27,9 @@ import androidx.compose.ui.text.input.KeyboardType
|
|||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.material.icons.filled.Link
|
||||
import androidx.compose.material.icons.filled.Wifi
|
||||
import androidx.compose.material.icons.filled.Terminal
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.adamaps.varroa.data.VarroaSettings
|
||||
import com.adamaps.varroa.ui.theme.*
|
||||
|
|
@ -54,6 +57,19 @@ fun SettingsScreen(
|
|||
var cameraEndpoint by remember(currentSettings) { mutableStateOf(currentSettings.cameraEndpoint) }
|
||||
var walletAddress by remember(currentSettings) { mutableStateOf(currentSettings.walletAddress) }
|
||||
|
||||
// Pairing & device state
|
||||
val isPaired by vm.isPaired.collectAsState()
|
||||
val deviceSerial by vm.deviceSerial.collectAsState()
|
||||
val pairingInProgress by vm.pairingInProgress.collectAsState()
|
||||
val pairingResult by vm.pairingResult.collectAsState()
|
||||
val sshStatus by vm.sshStatus.collectAsState()
|
||||
val wifiStatus by vm.wifiStatus.collectAsState()
|
||||
val wifiConnectResult by vm.wifiConnectResult.collectAsState()
|
||||
|
||||
// WiFi config input state
|
||||
var homeWifiSsid by remember { mutableStateOf("") }
|
||||
var homeWifiPassword by remember { mutableStateOf("") }
|
||||
|
||||
// Show snackbar on save
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
LaunchedEffect(saved) {
|
||||
|
|
@ -157,15 +173,129 @@ fun SettingsScreen(
|
|||
}
|
||||
}
|
||||
|
||||
SettingsSection("BEE DEVICE") {
|
||||
SettingsSection("ADACAM DEVICE") {
|
||||
SettingsField(
|
||||
label = "Bee API URL",
|
||||
label = "AdaCam API URL",
|
||||
value = beeApiUrl,
|
||||
onValueChange = { beeApiUrl = it },
|
||||
hint = "http://192.168.0.10:5000"
|
||||
hint = "http://10.77.0.1:5000"
|
||||
)
|
||||
}
|
||||
|
||||
// ── Pairing ───────────────────────────────────────────────────────
|
||||
SettingsSection("PAIRING") {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Link,
|
||||
contentDescription = null,
|
||||
tint = if (isPaired) Amber else Color.Gray,
|
||||
modifier = Modifier.size(16.dp)
|
||||
)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text(
|
||||
if (isPaired) "Paired — serial: $deviceSerial"
|
||||
else "Not paired",
|
||||
color = if (isPaired) Amber else Color.Gray,
|
||||
fontFamily = FontFamily.Monospace,
|
||||
fontSize = 12.sp
|
||||
)
|
||||
}
|
||||
Spacer(Modifier.height(8.dp))
|
||||
pairingResult?.let {
|
||||
Text(it, color = if (it.startsWith("Error")) Color.Red else Amber,
|
||||
fontFamily = FontFamily.Monospace, fontSize = 11.sp)
|
||||
Spacer(Modifier.height(4.dp))
|
||||
}
|
||||
Button(
|
||||
onClick = { vm.pairDevice() },
|
||||
enabled = !pairingInProgress,
|
||||
colors = ButtonDefaults.buttonColors(containerColor = Amber, contentColor = Background),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text(
|
||||
if (pairingInProgress) "Pairing…" else if (isPaired) "Re-pair AdaCam" else "Pair with AdaCam",
|
||||
fontFamily = FontFamily.Monospace,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ── Home WiFi config ──────────────────────────────────────────────
|
||||
SettingsSection("HOME WIFI") {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Icon(Icons.Default.Wifi, contentDescription = null,
|
||||
tint = if (wifiStatus?.connected == true) Amber else Color.Gray,
|
||||
modifier = Modifier.size(16.dp))
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text(
|
||||
wifiStatus?.let {
|
||||
if (it.connected) "Connected: ${it.ssid} (${it.ip})"
|
||||
else "Disconnected"
|
||||
} ?: "Unknown",
|
||||
color = if (wifiStatus?.connected == true) Amber else Color.Gray,
|
||||
fontFamily = FontFamily.Monospace, fontSize = 12.sp
|
||||
)
|
||||
}
|
||||
Spacer(Modifier.height(8.dp))
|
||||
SettingsField(label = "SSID", value = homeWifiSsid,
|
||||
onValueChange = { homeWifiSsid = it }, hint = "Your home network")
|
||||
SettingsField(label = "Password", value = homeWifiPassword,
|
||||
onValueChange = { homeWifiPassword = it }, hint = "WiFi password",
|
||||
keyboardType = KeyboardType.Password)
|
||||
wifiConnectResult?.let {
|
||||
Text(it, color = if (it.startsWith("Error")) Color.Red else Amber,
|
||||
fontFamily = FontFamily.Monospace, fontSize = 11.sp)
|
||||
Spacer(Modifier.height(4.dp))
|
||||
}
|
||||
Button(
|
||||
onClick = { vm.connectWifi(homeWifiSsid, homeWifiPassword) },
|
||||
enabled = isPaired && homeWifiSsid.isNotBlank() && homeWifiPassword.isNotBlank(),
|
||||
colors = ButtonDefaults.buttonColors(containerColor = Amber, contentColor = Background),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text("Connect AdaCam to WiFi", fontFamily = FontFamily.Monospace, fontWeight = FontWeight.Bold)
|
||||
}
|
||||
if (!isPaired) {
|
||||
Text("Pair device first to configure WiFi",
|
||||
color = Color.Gray, fontFamily = FontFamily.Monospace, fontSize = 10.sp)
|
||||
}
|
||||
}
|
||||
|
||||
// ── SSH access ────────────────────────────────────────────────────
|
||||
SettingsSection("SSH ACCESS") {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Icon(Icons.Default.Terminal, contentDescription = null,
|
||||
tint = if (sshStatus?.active == true) Amber else Color.Gray,
|
||||
modifier = Modifier.size(16.dp))
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Column {
|
||||
Text("SSH over home WiFi",
|
||||
color = Color.White, fontFamily = FontFamily.Monospace, fontSize = 12.sp)
|
||||
Text(if (sshStatus?.active == true) "Active — ssh root@<device-ip>" else "Inactive",
|
||||
color = Color.Gray, fontFamily = FontFamily.Monospace, fontSize = 10.sp)
|
||||
}
|
||||
}
|
||||
Switch(
|
||||
checked = sshStatus?.active == true,
|
||||
onCheckedChange = { vm.toggleSsh(it) },
|
||||
enabled = isPaired,
|
||||
colors = SwitchDefaults.colors(checkedThumbColor = Background, checkedTrackColor = Amber)
|
||||
)
|
||||
}
|
||||
if (!isPaired) {
|
||||
Text("Pair device first to toggle SSH",
|
||||
color = Color.Gray, fontFamily = FontFamily.Monospace, fontSize = 10.sp)
|
||||
}
|
||||
}
|
||||
|
||||
SettingsSection("ADAMAPS") {
|
||||
SettingsField(
|
||||
label = "ADAMaps API URL",
|
||||
|
|
|
|||
|
|
@ -1,30 +1,97 @@
|
|||
package com.adamaps.varroa.viewmodel
|
||||
|
||||
import android.app.Application
|
||||
import android.util.Log
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.adamaps.varroa.api.BeeApiClient
|
||||
import com.adamaps.varroa.data.ApiResult
|
||||
import com.adamaps.varroa.data.SettingsDataStore
|
||||
import com.adamaps.varroa.data.SshStatus
|
||||
import com.adamaps.varroa.data.VarroaSettings
|
||||
import com.adamaps.varroa.data.WifiStatus
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class SettingsViewModel(app: Application) : AndroidViewModel(app) {
|
||||
|
||||
companion object {
|
||||
private const val TAG = "SettingsVM"
|
||||
}
|
||||
|
||||
private val store = SettingsDataStore(app)
|
||||
private var beeClient: BeeApiClient? = null
|
||||
|
||||
val settings: StateFlow<VarroaSettings> = store.settings
|
||||
.stateIn(viewModelScope, SharingStarted.Eagerly, VarroaSettings())
|
||||
|
||||
// Derived observables from settings
|
||||
val isPaired: StateFlow<Boolean> = store.settings
|
||||
.map { it.isPaired }
|
||||
.stateIn(viewModelScope, SharingStarted.Eagerly, false)
|
||||
|
||||
val deviceSerial: StateFlow<String> = store.settings
|
||||
.map { it.deviceSerial }
|
||||
.stateIn(viewModelScope, SharingStarted.Eagerly, "")
|
||||
|
||||
private val _saved = MutableStateFlow(false)
|
||||
val saved: StateFlow<Boolean> = _saved.asStateFlow()
|
||||
|
||||
// Pairing state
|
||||
private val _pairingInProgress = MutableStateFlow(false)
|
||||
val pairingInProgress: StateFlow<Boolean> = _pairingInProgress.asStateFlow()
|
||||
|
||||
private val _pairingResult = MutableStateFlow<String?>(null)
|
||||
val pairingResult: StateFlow<String?> = _pairingResult.asStateFlow()
|
||||
|
||||
// SSH state
|
||||
private val _sshStatus = MutableStateFlow<SshStatus?>(null)
|
||||
val sshStatus: StateFlow<SshStatus?> = _sshStatus.asStateFlow()
|
||||
|
||||
private val _sshToggleResult = MutableStateFlow<String?>(null)
|
||||
val sshToggleResult: StateFlow<String?> = _sshToggleResult.asStateFlow()
|
||||
|
||||
// WiFi state
|
||||
private val _wifiStatus = MutableStateFlow<WifiStatus?>(null)
|
||||
val wifiStatus: StateFlow<WifiStatus?> = _wifiStatus.asStateFlow()
|
||||
|
||||
private val _wifiConnectResult = MutableStateFlow<String?>(null)
|
||||
val wifiConnectResult: StateFlow<String?> = _wifiConnectResult.asStateFlow()
|
||||
|
||||
init {
|
||||
// Initialize BeeApiClient with stored settings and token
|
||||
viewModelScope.launch {
|
||||
val s = store.settings.first()
|
||||
val client = BeeApiClient(s.beeApiUrl)
|
||||
if (s.apiToken.isNotBlank()) {
|
||||
client.apiToken = s.apiToken
|
||||
}
|
||||
beeClient = client
|
||||
|
||||
// Fetch initial statuses if paired
|
||||
if (s.isPaired) {
|
||||
refreshSshStatus()
|
||||
refreshWifiStatus()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun save(s: VarroaSettings) {
|
||||
viewModelScope.launch {
|
||||
store.save(s)
|
||||
|
||||
// Update client URL and token if changed
|
||||
beeClient?.updateUrl(s.beeApiUrl)
|
||||
if (s.apiToken.isNotBlank()) {
|
||||
beeClient?.apiToken = s.apiToken
|
||||
}
|
||||
|
||||
_saved.value = true
|
||||
}
|
||||
}
|
||||
|
|
@ -32,4 +99,165 @@ class SettingsViewModel(app: Application) : AndroidViewModel(app) {
|
|||
fun clearSaved() {
|
||||
_saved.value = false
|
||||
}
|
||||
|
||||
// ── Pairing ───────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Pair with the AdaCam device.
|
||||
* Fetches serial from /pair endpoint, derives token, stores both.
|
||||
*/
|
||||
fun pairDevice() {
|
||||
viewModelScope.launch {
|
||||
_pairingInProgress.value = true
|
||||
_pairingResult.value = null
|
||||
|
||||
try {
|
||||
val s = store.settings.first()
|
||||
val client = BeeApiClient(s.beeApiUrl)
|
||||
beeClient = client
|
||||
|
||||
when (val result = client.pair()) {
|
||||
is ApiResult.Success -> {
|
||||
val serial = result.data.serial
|
||||
Log.i(TAG, "Pairing successful: serial=$serial")
|
||||
|
||||
// Store pairing data (derives token automatically)
|
||||
store.savePairing(serial)
|
||||
|
||||
// Update client with new token
|
||||
val newSettings = store.settings.first()
|
||||
client.apiToken = newSettings.apiToken
|
||||
|
||||
_pairingResult.value = "Paired successfully! Serial: $serial"
|
||||
|
||||
// Fetch statuses now that we're paired
|
||||
refreshSshStatus()
|
||||
refreshWifiStatus()
|
||||
}
|
||||
is ApiResult.Error -> {
|
||||
Log.e(TAG, "Pairing failed: ${result.message}")
|
||||
_pairingResult.value = "Pairing failed: ${result.message}"
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Pairing exception", e)
|
||||
_pairingResult.value = "Pairing error: ${e.message}"
|
||||
} finally {
|
||||
_pairingInProgress.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear pairing data (for re-pairing).
|
||||
*/
|
||||
fun clearPairing() {
|
||||
viewModelScope.launch {
|
||||
store.clearPairing()
|
||||
beeClient?.apiToken = ""
|
||||
_sshStatus.value = null
|
||||
_wifiStatus.value = null
|
||||
_pairingResult.value = "Pairing cleared"
|
||||
}
|
||||
}
|
||||
|
||||
fun clearPairingResult() {
|
||||
_pairingResult.value = null
|
||||
}
|
||||
|
||||
// ── WiFi ──────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Refresh WiFi status from device.
|
||||
*/
|
||||
fun refreshWifiStatus() {
|
||||
viewModelScope.launch {
|
||||
val client = beeClient ?: return@launch
|
||||
when (val result = client.getWifiStatus()) {
|
||||
is ApiResult.Success -> {
|
||||
_wifiStatus.value = result.data
|
||||
}
|
||||
is ApiResult.Error -> {
|
||||
Log.w(TAG, "Failed to get WiFi status: ${result.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect device to a home WiFi network.
|
||||
*/
|
||||
fun connectWifi(ssid: String, password: String) {
|
||||
viewModelScope.launch {
|
||||
val client = beeClient ?: run {
|
||||
_wifiConnectResult.value = "Not connected to device"
|
||||
return@launch
|
||||
}
|
||||
_wifiConnectResult.value = null
|
||||
|
||||
when (val result = client.setWifiConfig(ssid, password)) {
|
||||
is ApiResult.Success -> {
|
||||
_wifiConnectResult.value = "WiFi config sent. Connecting to $ssid..."
|
||||
// Refresh status after a short delay
|
||||
kotlinx.coroutines.delay(3000)
|
||||
refreshWifiStatus()
|
||||
}
|
||||
is ApiResult.Error -> {
|
||||
_wifiConnectResult.value = "Failed: ${result.message}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun clearWifiResult() {
|
||||
_wifiConnectResult.value = null
|
||||
}
|
||||
|
||||
// ── SSH ───────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Refresh SSH status from device.
|
||||
*/
|
||||
fun refreshSshStatus() {
|
||||
viewModelScope.launch {
|
||||
val client = beeClient ?: return@launch
|
||||
when (val result = client.getSshStatus()) {
|
||||
is ApiResult.Success -> {
|
||||
_sshStatus.value = result.data
|
||||
}
|
||||
is ApiResult.Error -> {
|
||||
Log.w(TAG, "Failed to get SSH status: ${result.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle SSH on/off on the device.
|
||||
*/
|
||||
fun toggleSsh(enabled: Boolean) {
|
||||
viewModelScope.launch {
|
||||
val client = beeClient ?: run {
|
||||
_sshToggleResult.value = "Not connected to device"
|
||||
return@launch
|
||||
}
|
||||
_sshToggleResult.value = null
|
||||
|
||||
when (val result = client.setSshEnabled(enabled)) {
|
||||
is ApiResult.Success -> {
|
||||
_sshToggleResult.value = if (enabled) "SSH enabled" else "SSH disabled"
|
||||
_sshStatus.value = SshStatus(active = enabled)
|
||||
}
|
||||
is ApiResult.Error -> {
|
||||
_sshToggleResult.value = "Failed: ${result.message}"
|
||||
// Refresh to get actual state
|
||||
refreshSshStatus()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun clearSshResult() {
|
||||
_sshToggleResult.value = null
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue