diff --git a/app/src/main/java/io/homeassistant/companion/android/settings/server/ServerSettingsFragment.kt b/app/src/main/java/io/homeassistant/companion/android/settings/server/ServerSettingsFragment.kt index 26326aef48e..f56b267308b 100644 --- a/app/src/main/java/io/homeassistant/companion/android/settings/server/ServerSettingsFragment.kt +++ b/app/src/main/java/io/homeassistant/companion/android/settings/server/ServerSettingsFragment.kt @@ -1,8 +1,6 @@ package io.homeassistant.companion.android.settings.server -import android.Manifest import android.content.Intent -import android.content.pm.PackageManager import android.graphics.Color import android.os.Build import android.os.Bundle @@ -10,7 +8,6 @@ import android.os.Handler import android.os.Looper import android.text.InputType import android.util.Log -import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AlertDialog import androidx.appcompat.content.res.AppCompatResources import androidx.core.content.ContextCompat @@ -26,8 +23,6 @@ import dagger.hilt.android.AndroidEntryPoint import io.homeassistant.companion.android.R import io.homeassistant.companion.android.authenticator.Authenticator import io.homeassistant.companion.android.common.R as commonR -import io.homeassistant.companion.android.common.util.DisabledLocationHandler -import io.homeassistant.companion.android.common.util.LocationPermissionInfoHandler import io.homeassistant.companion.android.launch.LaunchActivity import io.homeassistant.companion.android.settings.SettingsActivity import io.homeassistant.companion.android.settings.ssid.SsidFragment @@ -50,10 +45,6 @@ class ServerSettingsFragment : ServerSettingsView, PreferenceFragmentCompat() { @Inject lateinit var presenter: ServerSettingsPresenter - private val permissionsRequest = registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { - onPermissionsResult(it) - } - private var serverId = -1 private var serverDeleteDialog: AlertDialog? = null @@ -155,7 +146,14 @@ class ServerSettingsFragment : ServerSettingsView, PreferenceFragmentCompat() { findPreference("connection_internal_ssids")?.let { it.setOnPreferenceClickListener { - onDisplaySsidScreen() + parentFragmentManager.commit { + replace( + R.id.content, + SsidFragment::class.java, + Bundle().apply { putInt(SsidFragment.EXTRA_SERVER, serverId) } + ) + addToBackStack(getString(commonR.string.pref_connection_homenetwork)) + } return@setOnPreferenceClickListener true } it.isVisible = presenter.hasWifi() @@ -208,10 +206,9 @@ class ServerSettingsFragment : ServerSettingsView, PreferenceFragmentCompat() { override fun enableInternalConnection(isEnabled: Boolean) { val iconTint = if (isEnabled) ContextCompat.getColor(requireContext(), commonR.color.colorAccent) else Color.DKGRAY - val doEnable = isEnabled && hasLocationPermission() findPreference("connection_internal")?.let { - it.isEnabled = doEnable + it.isEnabled = isEnabled try { val unwrappedDrawable = AppCompatResources.getDrawable(requireContext(), R.drawable.ic_computer) @@ -223,7 +220,7 @@ class ServerSettingsFragment : ServerSettingsView, PreferenceFragmentCompat() { } findPreference("app_lock_home_bypass")?.let { - it.isEnabled = doEnable + it.isEnabled = isEnabled try { val unwrappedDrawable = AppCompatResources.getDrawable(requireContext(), R.drawable.ic_wifi) @@ -246,68 +243,18 @@ class ServerSettingsFragment : ServerSettingsView, PreferenceFragmentCompat() { } } - override fun updateSsids(ssids: List) { + override fun updateHomeNetwork(ssids: List, ethernet: Boolean?, vpn: Boolean?) { findPreference("connection_internal_ssids")?.let { it.summary = - if (ssids.isEmpty()) { - getString(commonR.string.pref_connection_ssids_empty) + if (ssids.isEmpty() && ethernet != true && vpn != true) { + getString(commonR.string.not_set) } else { - ssids.joinToString() - } - } - } + val options = ssids.toMutableList() + if (ethernet == true) options += getString(commonR.string.manage_ssids_ethernet) + if (vpn == true) options += getString(commonR.string.manage_ssids_vpn) - private fun onDisplaySsidScreen() { - val permissionsToCheck: Array = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - arrayOf(Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_BACKGROUND_LOCATION) - } else { - arrayOf(Manifest.permission.ACCESS_COARSE_LOCATION) - } - - if (DisabledLocationHandler.isLocationEnabled(requireContext())) { - if (!checkPermission(permissionsToCheck)) { - LocationPermissionInfoHandler.showLocationPermInfoDialogIfNeeded( - requireContext(), - permissionsToCheck, - continueYesCallback = { - requestLocationPermission() - // showSsidSettings() will be called if permission is granted - } - ) - } else { - showSsidSettings() - } - } else { - if (presenter.isSsidUsed()) { - DisabledLocationHandler.showLocationDisabledWarnDialog( - requireActivity(), - arrayOf( - getString(commonR.string.pref_connection_wifi) - ), - showAsNotification = false, - withDisableOption = true - ) { - presenter.clearSsids() + options.joinToString() } - } else { - DisabledLocationHandler.showLocationDisabledWarnDialog( - requireActivity(), - arrayOf( - getString(commonR.string.pref_connection_wifi) - ) - ) - } - } - } - - private fun showSsidSettings() { - parentFragmentManager.commit { - replace( - R.id.content, - SsidFragment::class.java, - Bundle().apply { putInt(SsidFragment.EXTRA_SERVER, serverId) } - ) - addToBackStack(getString(commonR.string.manage_ssids)) } } @@ -324,37 +271,6 @@ class ServerSettingsFragment : ServerSettingsView, PreferenceFragmentCompat() { return (result == Authenticator.SUCCESS || result == Authenticator.CANCELED) } - private fun hasLocationPermission(): Boolean { - val permissionsToCheck: Array = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - arrayOf(Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_BACKGROUND_LOCATION) - } else { - arrayOf(Manifest.permission.ACCESS_COARSE_LOCATION) - } - return checkPermission(permissionsToCheck) - } - - private fun requestLocationPermission() { - val permissions = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - arrayOf(Manifest.permission.ACCESS_FINE_LOCATION) // Background location will be requested later - } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - arrayOf(Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_BACKGROUND_LOCATION) - } else { - arrayOf(Manifest.permission.ACCESS_COARSE_LOCATION) - } - permissionsRequest.launch(permissions) - } - - private fun checkPermission(permissions: Array?): Boolean { - if (!permissions.isNullOrEmpty()) { - for (permission in permissions) { - if (ContextCompat.checkSelfPermission(requireContext(), permission) == PackageManager.PERMISSION_DENIED) { - return false - } - } - } - return true - } - override fun onRemovedServer(success: Boolean, hasAnyRemaining: Boolean) { serverDeleteHandler.removeCallbacksAndMessages(null) serverDeleteDialog?.cancel() @@ -368,24 +284,6 @@ class ServerSettingsFragment : ServerSettingsView, PreferenceFragmentCompat() { } } - private fun onPermissionsResult(results: Map) { - if (results.keys.contains(Manifest.permission.ACCESS_FINE_LOCATION) && - results[Manifest.permission.ACCESS_FINE_LOCATION] == true && - Build.VERSION.SDK_INT >= Build.VERSION_CODES.R - ) { - // For Android 11+ we MUST NOT request Background Location permission with fine or coarse - // permissions as for Android 11 the background location request needs to be done separately - // See here: https://developer.android.com/about/versions/11/privacy/location#request-background-location-separately - // The separate request of background location is done here - permissionsRequest.launch(arrayOf(Manifest.permission.ACCESS_BACKGROUND_LOCATION)) - return - } - - if (results.entries.all { it.value }) { - showSsidSettings() - } - } - override fun onResume() { super.onResume() diff --git a/app/src/main/java/io/homeassistant/companion/android/settings/server/ServerSettingsPresenterImpl.kt b/app/src/main/java/io/homeassistant/companion/android/settings/server/ServerSettingsPresenterImpl.kt index 459d9930624..cf65c200e69 100644 --- a/app/src/main/java/io/homeassistant/companion/android/settings/server/ServerSettingsPresenterImpl.kt +++ b/app/src/main/java/io/homeassistant/companion/android/settings/server/ServerSettingsPresenterImpl.kt @@ -147,21 +147,10 @@ class ServerSettingsPresenterImpl @Inject constructor( } } mainScope.launch { - val ssids = serverManager.getServer(serverId)?.connection?.internalSsids.orEmpty() - if (ssids.isEmpty()) { - serverManager.getServer(serverId)?.let { - serverManager.updateServer( - it.copy( - connection = it.connection.copy( - internalUrl = null - ) - ) - ) - } - } - - view.enableInternalConnection(ssids.isNotEmpty()) - view.updateSsids(ssids) + val connection = serverManager.getServer(serverId)?.connection + val ssids = connection?.internalSsids.orEmpty() + view.enableInternalConnection(ssids.isNotEmpty() || connection?.internalEthernet == true || connection?.internalVpn == true) + view.updateHomeNetwork(ssids, connection?.internalEthernet, connection?.internalVpn) } } diff --git a/app/src/main/java/io/homeassistant/companion/android/settings/server/ServerSettingsView.kt b/app/src/main/java/io/homeassistant/companion/android/settings/server/ServerSettingsView.kt index d95407f5ebe..73847b8ae37 100644 --- a/app/src/main/java/io/homeassistant/companion/android/settings/server/ServerSettingsView.kt +++ b/app/src/main/java/io/homeassistant/companion/android/settings/server/ServerSettingsView.kt @@ -4,6 +4,6 @@ interface ServerSettingsView { fun updateServerName(name: String) fun enableInternalConnection(isEnabled: Boolean) fun updateExternalUrl(url: String, useCloud: Boolean) - fun updateSsids(ssids: List) + fun updateHomeNetwork(ssids: List, ethernet: Boolean?, vpn: Boolean?) fun onRemovedServer(success: Boolean, hasAnyRemaining: Boolean) } diff --git a/app/src/main/java/io/homeassistant/companion/android/settings/ssid/SsidFragment.kt b/app/src/main/java/io/homeassistant/companion/android/settings/ssid/SsidFragment.kt index 98d03a94017..08a12071c37 100644 --- a/app/src/main/java/io/homeassistant/companion/android/settings/ssid/SsidFragment.kt +++ b/app/src/main/java/io/homeassistant/companion/android/settings/ssid/SsidFragment.kt @@ -1,14 +1,24 @@ package io.homeassistant.companion.android.settings.ssid +import android.Manifest +import android.content.pm.PackageManager +import android.os.Build import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue import androidx.compose.ui.platform.ComposeView +import androidx.core.content.ContextCompat import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import dagger.hilt.android.AndroidEntryPoint import io.homeassistant.companion.android.common.R as commonR +import io.homeassistant.companion.android.common.util.DisabledLocationHandler +import io.homeassistant.companion.android.common.util.LocationPermissionInfoHandler import io.homeassistant.companion.android.settings.addHelpMenuProvider import io.homeassistant.companion.android.settings.ssid.views.SsidView import io.homeassistant.companion.android.util.compose.HomeAssistantAppTheme @@ -22,6 +32,12 @@ class SsidFragment : Fragment() { val viewModel: SsidViewModel by viewModels() + private val permissionsRequest = registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { + onPermissionsResult(it) + } + + private var canReadWifi by mutableStateOf(false) + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -32,12 +48,18 @@ class SsidFragment : Fragment() { HomeAssistantAppTheme { SsidView( wifiSsids = viewModel.wifiSsids, + canReadWifi = canReadWifi, + ethernet = viewModel.ethernet, + vpn = viewModel.vpn, prioritizeInternal = viewModel.prioritizeInternal, usingWifi = viewModel.usingWifi, activeSsid = viewModel.activeSsid, activeBssid = viewModel.activeBssid, onAddWifiSsid = viewModel::addHomeWifiSsid, onRemoveWifiSsid = viewModel::removeHomeWifiSsid, + onRequestPermission = { onRequestLocationPermission() }, + onSetEthernet = viewModel::setInternalWithEthernet, + onSetVpn = viewModel::setInternalWithVpn, onSetPrioritize = viewModel::setPrioritize ) } @@ -51,6 +73,86 @@ class SsidFragment : Fragment() { override fun onResume() { super.onResume() - activity?.title = getString(commonR.string.pref_connection_wifi) + activity?.title = getString(commonR.string.pref_connection_homenetwork) + updateLocationStatus() + } + + private fun updateLocationStatus() { + val locationEnabled = DisabledLocationHandler.isLocationEnabled(requireContext()) + if (!locationEnabled) { + canReadWifi = false + return + } + + val permissionsToCheck = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + arrayOf(Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_BACKGROUND_LOCATION) + } else { + arrayOf(Manifest.permission.ACCESS_COARSE_LOCATION) + } + canReadWifi = checkPermission(permissionsToCheck) + } + + private fun onRequestLocationPermission() { + val permissionsToCheck: Array = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + arrayOf(Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_BACKGROUND_LOCATION) + } else { + arrayOf(Manifest.permission.ACCESS_COARSE_LOCATION) + } + + if (DisabledLocationHandler.isLocationEnabled(requireContext())) { + LocationPermissionInfoHandler.showLocationPermInfoDialogIfNeeded( + requireContext(), + permissionsToCheck, + continueYesCallback = { + requestLocationPermission() + } + ) + } else { + DisabledLocationHandler.showLocationDisabledWarnDialog( + requireActivity(), + arrayOf( + getString(commonR.string.manage_ssids_wifi) + ), + showAsNotification = false + ) + } + } + + private fun checkPermission(permissions: Array?): Boolean { + if (!permissions.isNullOrEmpty()) { + for (permission in permissions) { + if (ContextCompat.checkSelfPermission(requireContext(), permission) == PackageManager.PERMISSION_DENIED) { + return false + } + } + } + return true + } + + private fun requestLocationPermission() { + val permissions = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + arrayOf(Manifest.permission.ACCESS_FINE_LOCATION) // Background location will be requested later + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + arrayOf(Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_BACKGROUND_LOCATION) + } else { + arrayOf(Manifest.permission.ACCESS_COARSE_LOCATION) + } + permissionsRequest.launch(permissions) + } + + private fun onPermissionsResult(results: Map) { + if (results.keys.contains(Manifest.permission.ACCESS_FINE_LOCATION) && + results[Manifest.permission.ACCESS_FINE_LOCATION] == true && + Build.VERSION.SDK_INT >= Build.VERSION_CODES.R + ) { + // For Android 11+ we MUST NOT request Background Location permission with fine or coarse + // permissions as for Android 11 the background location request needs to be done separately + // See here: https://developer.android.com/about/versions/11/privacy/location#request-background-location-separately + // The separate request of background location is done here + permissionsRequest.launch(arrayOf(Manifest.permission.ACCESS_BACKGROUND_LOCATION)) + return + } + updateLocationStatus() + viewModel.updateWifiState() } } diff --git a/app/src/main/java/io/homeassistant/companion/android/settings/ssid/SsidViewModel.kt b/app/src/main/java/io/homeassistant/companion/android/settings/ssid/SsidViewModel.kt index 51ec6eef9e4..48a9cb4945a 100644 --- a/app/src/main/java/io/homeassistant/companion/android/settings/ssid/SsidViewModel.kt +++ b/app/src/main/java/io/homeassistant/companion/android/settings/ssid/SsidViewModel.kt @@ -1,6 +1,7 @@ package io.homeassistant.companion.android.settings.ssid import android.app.Application +import android.util.Log import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf @@ -22,9 +23,19 @@ class SsidViewModel @Inject constructor( application: Application ) : AndroidViewModel(application) { + companion object { + private const val TAG = "SsidViewModel" + } + var wifiSsids = mutableStateListOf() private set + var ethernet by mutableStateOf(null) + private set + + var vpn by mutableStateOf(null) + private set + var prioritizeInternal by mutableStateOf(false) private set @@ -45,11 +56,11 @@ class SsidViewModel @Inject constructor( val server = serverManager.getServer(serverId) wifiSsids.clear() wifiSsids.addAll(server?.connection?.internalSsids.orEmpty()) + ethernet = server?.connection?.internalEthernet + vpn = server?.connection?.internalVpn server?.connection?.prioritizeInternal?.let { prioritizeInternal = it } - usingWifi = wifiHelper.isUsingWifi() - activeSsid = wifiHelper.getWifiSsid()?.removeSurrounding("\"") - activeBssid = wifiHelper.getWifiBssid() + updateWifiState() } } @@ -82,6 +93,46 @@ class SsidViewModel @Inject constructor( } } + fun updateWifiState() { + try { + usingWifi = wifiHelper.isUsingWifi() + activeSsid = wifiHelper.getWifiSsid()?.removeSurrounding("\"") + activeBssid = wifiHelper.getWifiBssid() + } catch (e: Exception) { + Log.w(TAG, "Unable to update Wi-Fi state", e) + } + } + + fun setInternalWithEthernet(eth: Boolean) { + viewModelScope.launch { + serverManager.getServer(serverId)?.let { + serverManager.updateServer( + it.copy( + connection = it.connection.copy( + internalEthernet = eth + ) + ) + ) + ethernet = eth + } + } + } + + fun setInternalWithVpn(privateNetwork: Boolean) { + viewModelScope.launch { + serverManager.getServer(serverId)?.let { + serverManager.updateServer( + it.copy( + connection = it.connection.copy( + internalVpn = privateNetwork + ) + ) + ) + vpn = privateNetwork + } + } + } + fun setPrioritize(prioritize: Boolean) { viewModelScope.launch { serverManager.getServer(serverId)?.let { diff --git a/app/src/main/java/io/homeassistant/companion/android/settings/ssid/views/SsidView.kt b/app/src/main/java/io/homeassistant/companion/android/settings/ssid/views/SsidView.kt index 8b65df89a41..757bc7cea31 100644 --- a/app/src/main/java/io/homeassistant/companion/android/settings/ssid/views/SsidView.kt +++ b/app/src/main/java/io/homeassistant/companion/android/settings/ssid/views/SsidView.kt @@ -2,7 +2,6 @@ package io.homeassistant.companion.android.settings.ssid.views import android.net.wifi.WifiManager import android.os.Build -import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -14,6 +13,7 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.text.KeyboardActions @@ -27,13 +27,16 @@ import androidx.compose.material.DropdownMenuItem import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.Icon import androidx.compose.material.LocalContentAlpha -import androidx.compose.material.LocalContentColor import androidx.compose.material.MaterialTheme +import androidx.compose.material.Switch +import androidx.compose.material.SwitchDefaults import androidx.compose.material.Text import androidx.compose.material.TextField import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Clear import androidx.compose.material.icons.filled.Error +import androidx.compose.material.icons.filled.SettingsEthernet +import androidx.compose.material.icons.filled.VpnKey import androidx.compose.material.icons.filled.Wifi import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider @@ -42,8 +45,9 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment -import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.stringResource @@ -51,80 +55,55 @@ import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import com.mikepenz.iconics.compose.Image +import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial import io.homeassistant.companion.android.common.R as commonR import io.homeassistant.companion.android.common.data.wifi.WifiHelper +import io.homeassistant.companion.android.util.compose.HaAlertInfo +import io.homeassistant.companion.android.util.compose.HaAlertWarning -@OptIn( - ExperimentalComposeUiApi::class, - ExperimentalMaterialApi::class, - ExperimentalFoundationApi::class -) +@OptIn(ExperimentalMaterialApi::class) @Composable fun SsidView( wifiSsids: List, + canReadWifi: Boolean, + ethernet: Boolean?, + vpn: Boolean?, prioritizeInternal: Boolean, usingWifi: Boolean, activeSsid: String?, activeBssid: String?, onAddWifiSsid: (String) -> Boolean, onRemoveWifiSsid: (String) -> Unit, + onRequestPermission: () -> Unit, + onSetEthernet: (Boolean) -> Unit, + onSetVpn: (Boolean) -> Unit, onSetPrioritize: (Boolean) -> Unit ) { - val keyboardController = LocalSoftwareKeyboardController.current - LazyColumn(contentPadding = PaddingValues(vertical = 16.dp)) { - item("ssid.intro") { + item("intro") { Column { Text( text = stringResource(commonR.string.manage_ssids_introduction), modifier = Modifier .padding(horizontal = 16.dp) - .padding(bottom = 8.dp) + .padding(bottom = 16.dp) ) - Row(modifier = Modifier.padding(horizontal = 16.dp)) { - var ssidInput by remember { mutableStateOf("") } - var ssidError by remember { mutableStateOf(false) } - - TextField( - value = ssidInput, - singleLine = true, - onValueChange = { - ssidInput = it - ssidError = false - }, - label = { Text(stringResource(commonR.string.manage_ssids_input)) }, - keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), - keyboardActions = KeyboardActions( - onDone = { - keyboardController?.hide() - ssidError = !onAddWifiSsid(ssidInput) - if (!ssidError) ssidInput = "" - } - ), - isError = ssidError, - trailingIcon = if (ssidError) { - { - Icon( - imageVector = Icons.Default.Error, - contentDescription = stringResource(commonR.string.manage_ssids_input_exists) - ) - } - } else { - null - }, - modifier = Modifier.weight(1f) - ) - Button( - modifier = Modifier - .height(56.dp) // align with TextField: 56 - .padding(start = 8.dp, top = 0.dp), - onClick = { - keyboardController?.hide() - ssidError = !onAddWifiSsid(ssidInput) - if (!ssidError) ssidInput = "" - } - ) { - Text(stringResource(commonR.string.add_ssid)) + SsidSubheader( + title = stringResource(commonR.string.manage_ssids_wifi), + icon = Icons.Default.Wifi, + checked = null, + onClicked = null + ) + if (canReadWifi) { + SsidInput(onAddWifiSsid) + } else { + Box(Modifier.padding(horizontal = 16.dp)) { + HaAlertWarning( + message = stringResource(commonR.string.manage_ssids_permission), + action = stringResource(commonR.string.allow), + onActionClicked = onRequestPermission + ) } } } @@ -162,21 +141,18 @@ fun SsidView( } Row( modifier = Modifier - .heightIn(min = 56.dp) - .padding(horizontal = 16.dp, vertical = 8.dp) - .animateItemPlacement(), + .heightIn(min = 48.dp) + .padding(horizontal = 16.dp) + .animateItem(), verticalAlignment = Alignment.CenterVertically ) { - Icon( - imageVector = Icons.Default.Wifi, - contentDescription = null, - tint = - if (connected) { - colorResource(commonR.color.colorAccent) - } else { - LocalContentColor.current.copy(alpha = LocalContentAlpha.current) - } - ) + if (connected) { + Image( + asset = CommunityMaterial.Icon3.cmd_wifi_check, + colorFilter = ColorFilter.tint(colorResource(commonR.color.colorAccent)) + ) + Spacer(Modifier.width(16.dp)) + } Text( text = if (it.startsWith(WifiHelper.BSSID_PREFIX)) { @@ -191,7 +167,7 @@ fun SsidView( null }, modifier = Modifier - .padding(horizontal = 16.dp) + .padding(end = 16.dp) .weight(1f) ) Icon( @@ -206,56 +182,202 @@ fun SsidView( } } - item("prioritize") { - var prioritizeDropdown by remember { mutableStateOf(false) } + item("ethernet") { + Column { + Spacer(Modifier.height(16.dp)) + SsidSubheader( + title = stringResource(commonR.string.manage_ssids_ethernet), + icon = Icons.Default.SettingsEthernet, + checked = ethernet, + onClicked = { onSetEthernet(it) } + ) + } + } + + item("vpn") { + SsidSubheader( + title = stringResource(commonR.string.manage_ssids_vpn), + icon = Icons.Default.VpnKey, + checked = vpn, + onClicked = { onSetVpn(it) } + ) + } + + if (wifiSsids.isNotEmpty() || ethernet == true || vpn == true) { + item("warn") { + Box( + Modifier + .padding(horizontal = 16.dp) + .padding(top = 32.dp) + .animateItem() + ) { + HaAlertInfo( + message = stringResource(commonR.string.manage_ssids_warning), + action = null, + onActionClicked = null + ) + } + } + } + item("prioritize") { Column { Spacer(modifier = Modifier.height(48.dp)) Divider(modifier = Modifier.padding(horizontal = 16.dp)) - Box { - Column( - modifier = Modifier - .clickable { prioritizeDropdown = true } - .fillMaxWidth() - .padding(all = 16.dp) - ) { - Text( - text = stringResource(commonR.string.prioritize_internal_title), - style = MaterialTheme.typography.body1 - ) - CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) { - Text( - text = stringResource( - if (prioritizeInternal) { - commonR.string.prioritize_internal_on - } else { - commonR.string.prioritize_internal_off - } - ), - style = MaterialTheme.typography.body2 - ) - } - } - if (prioritizeDropdown) { - DropdownMenu( - expanded = true, - onDismissRequest = { prioritizeDropdown = false }, - modifier = Modifier.fillMaxWidth() - ) { - DropdownMenuItem(onClick = { - onSetPrioritize(false) - prioritizeDropdown = false - }) { - Text(stringResource(commonR.string.prioritize_internal_off)) - } - DropdownMenuItem(onClick = { - onSetPrioritize(true) - prioritizeDropdown = false - }) { - Text(stringResource(commonR.string.prioritize_internal_on_expanded)) - } + SsidPrioritizeInternal( + prioritize = prioritizeInternal, + onChanged = onSetPrioritize + ) + } + } + } +} + +@Composable +fun SsidSubheader( + title: String, + icon: ImageVector, + checked: Boolean?, + onClicked: ((Boolean) -> Unit)?, + modifier: Modifier = Modifier +) { + val modifier = if (onClicked != null) { + modifier.then( + Modifier + .clickable { checked?.let { onClicked(!it) } ?: onClicked(true) } + .heightIn(min = 56.dp) + .padding(horizontal = 16.dp) + ) + } else { + modifier.then( + Modifier + .heightIn(min = 56.dp) + .padding(horizontal = 16.dp) + ) + } + Row( + modifier = modifier, + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = icon, + contentDescription = null + ) + Text( + text = title, + modifier = Modifier.padding(start = 16.dp).weight(1f), + style = MaterialTheme.typography.subtitle1 + ) + if (onClicked != null) { + Switch( + checked = checked == true, + onCheckedChange = null, + colors = SwitchDefaults.colors(uncheckedThumbColor = colorResource(commonR.color.colorSwitchUncheckedThumb)) + ) + } + } +} + +@Composable +fun SsidInput( + onSubmit: (String) -> Boolean +) { + val keyboardController = LocalSoftwareKeyboardController.current + Row(modifier = Modifier.padding(horizontal = 16.dp)) { + var ssidInput by remember { mutableStateOf("") } + var ssidError by remember { mutableStateOf(false) } + + TextField( + value = ssidInput, + singleLine = true, + onValueChange = { + ssidInput = it + ssidError = false + }, + label = { Text(stringResource(commonR.string.manage_ssids_input)) }, + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), + keyboardActions = KeyboardActions( + onDone = { + keyboardController?.hide() + ssidError = !onSubmit(ssidInput) + if (!ssidError) ssidInput = "" + } + ), + isError = ssidError, + trailingIcon = if (ssidError) { + { + Icon( + imageVector = Icons.Default.Error, + contentDescription = stringResource(commonR.string.manage_ssids_input_exists) + ) + } + } else { + null + }, + modifier = Modifier.weight(1f) + ) + Button( + modifier = Modifier + .height(56.dp) // align with TextField: 56 + .padding(start = 8.dp, top = 0.dp), + onClick = { + keyboardController?.hide() + ssidError = !onSubmit(ssidInput) + if (!ssidError) ssidInput = "" + } + ) { + Text(stringResource(commonR.string.add_ssid)) + } + } +} + +@Composable +fun SsidPrioritizeInternal( + prioritize: Boolean, + onChanged: (Boolean) -> Unit +) { + var prioritizeDropdown by remember { mutableStateOf(false) } + Box { + Column( + modifier = Modifier + .clickable { prioritizeDropdown = true } + .fillMaxWidth() + .padding(all = 16.dp) + ) { + Text( + text = stringResource(commonR.string.prioritize_internal_title), + style = MaterialTheme.typography.body1 + ) + CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) { + Text( + text = stringResource( + if (prioritize) { + commonR.string.prioritize_internal_on + } else { + commonR.string.prioritize_internal_off } - } + ), + style = MaterialTheme.typography.body2 + ) + } + } + if (prioritizeDropdown) { + DropdownMenu( + expanded = true, + onDismissRequest = { prioritizeDropdown = false }, + modifier = Modifier.fillMaxWidth() + ) { + DropdownMenuItem(onClick = { + onChanged(false) + prioritizeDropdown = false + }) { + Text(stringResource(commonR.string.prioritize_internal_off)) + } + DropdownMenuItem(onClick = { + onChanged(true) + prioritizeDropdown = false + }) { + Text(stringResource(commonR.string.prioritize_internal_on_expanded)) } } } @@ -267,12 +389,18 @@ fun SsidView( private fun PreviewSsidViewEmpty() { SsidView( wifiSsids = emptyList(), + canReadWifi = true, + ethernet = null, + vpn = null, prioritizeInternal = false, activeSsid = "home-assistant-wifi", activeBssid = "02:00:00:00:00:00", usingWifi = true, onAddWifiSsid = { true }, onRemoveWifiSsid = {}, + onRequestPermission = {}, + onSetEthernet = {}, + onSetVpn = {}, onSetPrioritize = {} ) } @@ -282,12 +410,18 @@ private fun PreviewSsidViewEmpty() { private fun PreviewSsidViewItems() { SsidView( wifiSsids = listOf("home-assistant-wifi", "wifi-one", "BSSID:1A:2B:3C:4D:5E:6F"), + canReadWifi = true, + ethernet = false, + vpn = true, prioritizeInternal = false, activeSsid = "home-assistant-wifi", activeBssid = "02:00:00:00:00:00", usingWifi = true, onAddWifiSsid = { true }, onRemoveWifiSsid = {}, + onRequestPermission = {}, + onSetEthernet = {}, + onSetVpn = {}, onSetPrioritize = {} ) } diff --git a/app/src/main/java/io/homeassistant/companion/android/util/compose/HaAlert.kt b/app/src/main/java/io/homeassistant/companion/android/util/compose/HaAlert.kt index 8dd745c5874..686bf21e5f8 100644 --- a/app/src/main/java/io/homeassistant/companion/android/util/compose/HaAlert.kt +++ b/app/src/main/java/io/homeassistant/companion/android/util/compose/HaAlert.kt @@ -11,20 +11,53 @@ import androidx.compose.material.TextButton import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.colorResource import androidx.compose.ui.unit.dp import io.homeassistant.companion.android.common.R as commonR +@Composable +fun HaAlertInfo( + message: String, + action: String?, + onActionClicked: (() -> Unit)? +) { + HaAlert( + message, + action, + onActionClicked, + colorResource(commonR.color.colorAlertInfo), + colorResource(commonR.color.colorOnAlertInfo) + ) +} + @Composable fun HaAlertWarning( message: String, action: String?, - onActionClicked: () -> Unit + onActionClicked: (() -> Unit)? +) { + HaAlert( + message, + action, + onActionClicked, + colorResource(commonR.color.colorAlertWarning), + colorResource(commonR.color.colorOnAlertWarning) + ) +} + +@Composable +fun HaAlert( + message: String, + action: String?, + onActionClicked: (() -> Unit)?, + backgroundColor: Color, + onBackgroundColor: Color ) { Row( modifier = Modifier .fillMaxWidth() - .background(colorResource(commonR.color.colorAlertWarning), MaterialTheme.shapes.medium) + .background(backgroundColor, MaterialTheme.shapes.medium) .padding(all = 8.dp), verticalAlignment = Alignment.CenterVertically ) { @@ -34,9 +67,9 @@ fun HaAlertWarning( .weight(1f) .padding(end = if (action != null) 8.dp else 0.dp) ) - if (action != null) { + if (action != null && onActionClicked != null) { TextButton( - colors = ButtonDefaults.textButtonColors(contentColor = colorResource(commonR.color.colorOnAlertWarning)), + colors = ButtonDefaults.textButtonColors(contentColor = onBackgroundColor), onClick = onActionClicked ) { Text(action) diff --git a/app/src/main/java/io/homeassistant/companion/android/webview/WebViewActivity.kt b/app/src/main/java/io/homeassistant/companion/android/webview/WebViewActivity.kt index 922c902de94..804c78a7df5 100644 --- a/app/src/main/java/io/homeassistant/companion/android/webview/WebViewActivity.kt +++ b/app/src/main/java/io/homeassistant/companion/android/webview/WebViewActivity.kt @@ -934,7 +934,7 @@ class WebViewActivity : BaseActivity(), io.homeassistant.companion.android.webvi val settingsWithLocationPermissions = mutableListOf() if (!DisabledLocationHandler.isLocationEnabled(this) && presenter.isSsidUsed()) { showLocationDisabledWarning = true - settingsWithLocationPermissions.add(getString(commonR.string.pref_connection_wifi)) + settingsWithLocationPermissions.add(getString(commonR.string.pref_connection_homenetwork)) } for (manager in SensorReceiver.MANAGERS) { for (basicSensor in manager.getAvailableSensors(this)) { diff --git a/app/src/main/res/xml/preferences_server.xml b/app/src/main/res/xml/preferences_server.xml index 15e85dcb7db..8d7715cfebe 100644 --- a/app/src/main/res/xml/preferences_server.xml +++ b/app/src/main/res/xml/preferences_server.xml @@ -31,8 +31,7 @@ + android:title="@string/pref_connection_homenetwork"/> ): Boolean fun getWifiSsid(): String? fun getWifiBssid(): String? + + /** Returns if the active data connection is using ethernet */ + fun isUsingEthernet(): Boolean + + /** Returns if the active data connection is a VPN connection */ + fun isUsingVpn(): Boolean } diff --git a/common/src/main/java/io/homeassistant/companion/android/common/data/wifi/WifiHelperImpl.kt b/common/src/main/java/io/homeassistant/companion/android/common/data/wifi/WifiHelperImpl.kt index 98a273dc013..1605f2844be 100644 --- a/common/src/main/java/io/homeassistant/companion/android/common/data/wifi/WifiHelperImpl.kt +++ b/common/src/main/java/io/homeassistant/companion/android/common/data/wifi/WifiHelperImpl.kt @@ -49,4 +49,28 @@ class WifiHelperImpl @Inject constructor( override fun getWifiBssid(): String? = wifiManager?.connectionInfo?.bssid // Deprecated but callback doesn't provide BSSID info instantly + + override fun isUsingEthernet(): Boolean = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + connectivityManager.activeNetwork?.let { + connectivityManager + .getNetworkCapabilities(it) + ?.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) == true + } == true + } else { + connectivityManager.activeNetworkInfo?.isConnected == true && + connectivityManager.activeNetworkInfo?.type == ConnectivityManager.TYPE_ETHERNET + } + + override fun isUsingVpn(): Boolean = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + connectivityManager.activeNetwork?.let { + connectivityManager + .getNetworkCapabilities(it) + ?.hasTransport(NetworkCapabilities.TRANSPORT_VPN) == true + } == true + } else { + connectivityManager.activeNetworkInfo?.isConnected == true && + connectivityManager.activeNetworkInfo?.type == ConnectivityManager.TYPE_VPN + } } diff --git a/common/src/main/java/io/homeassistant/companion/android/common/sensors/BluetoothSensorManager.kt b/common/src/main/java/io/homeassistant/companion/android/common/sensors/BluetoothSensorManager.kt index feb00f45fc5..bd59a351d27 100644 --- a/common/src/main/java/io/homeassistant/companion/android/common/sensors/BluetoothSensorManager.kt +++ b/common/src/main/java/io/homeassistant/companion/android/common/sensors/BluetoothSensorManager.kt @@ -250,7 +250,7 @@ class BluetoothSensorManager : SensorManager { ) } - private fun isPermittedOnThisWifiNetwork(context: Context) = serverManager(context).defaultServers.any { it.connection.isHomeWifiSsid() } + private fun isPermittedOnThisNetwork(context: Context) = serverManager(context).defaultServers.any { it.connection.isInternal() } private suspend fun updateBLEDevice(context: Context) { val transmitActive = getToggleSetting(context, bleTransmitter, SETTING_BLE_TRANSMIT_ENABLED, default = true) @@ -353,14 +353,14 @@ class BluetoothSensorManager : SensorManager { // transmit when BT is on, if we are not already transmitting, or details have changed, and we're permitted on this wifi network if (isBtOn(context)) { if (bleTransmitterDevice.transmitRequested && (!bleTransmitterDevice.transmitting || bleTransmitterDevice.restartRequired) && - (!bleTransmitterDevice.onlyTransmitOnHomeWifiSetting || isPermittedOnThisWifiNetwork(context)) + (!bleTransmitterDevice.onlyTransmitOnHomeWifiSetting || isPermittedOnThisNetwork(context)) ) { TransmitterManager.startTransmitting(context, bleTransmitterDevice) } } // BT off, or TransmitToggled off, or not permitted on this network - stop transmitting if we have been - if (!isBtOn(context) || !bleTransmitterDevice.transmitRequested || (bleTransmitterDevice.onlyTransmitOnHomeWifiSetting && !isPermittedOnThisWifiNetwork(context))) { + if (!isBtOn(context) || !bleTransmitterDevice.transmitRequested || (bleTransmitterDevice.onlyTransmitOnHomeWifiSetting && !isPermittedOnThisNetwork(context))) { TransmitterManager.stopTransmitting(bleTransmitterDevice) } diff --git a/common/src/main/java/io/homeassistant/companion/android/database/AppDatabase.kt b/common/src/main/java/io/homeassistant/companion/android/database/AppDatabase.kt index 3f3b64f5b60..df81f5951c6 100644 --- a/common/src/main/java/io/homeassistant/companion/android/database/AppDatabase.kt +++ b/common/src/main/java/io/homeassistant/companion/android/database/AppDatabase.kt @@ -97,7 +97,7 @@ import kotlinx.coroutines.runBlocking Server::class, Setting::class ], - version = 47, + version = 48, autoMigrations = [ AutoMigration(from = 24, to = 25), AutoMigration(from = 25, to = 26), @@ -120,7 +120,8 @@ import kotlinx.coroutines.runBlocking AutoMigration(from = 43, to = 44), AutoMigration(from = 44, to = 45), AutoMigration(from = 45, to = 46), - AutoMigration(from = 46, to = 47) + AutoMigration(from = 46, to = 47), + AutoMigration(from = 47, to = 48) ] ) @TypeConverters( diff --git a/common/src/main/java/io/homeassistant/companion/android/database/server/ServerConnectionInfo.kt b/common/src/main/java/io/homeassistant/companion/android/database/server/ServerConnectionInfo.kt index 83e90ca39fa..156536afb4c 100644 --- a/common/src/main/java/io/homeassistant/companion/android/database/server/ServerConnectionInfo.kt +++ b/common/src/main/java/io/homeassistant/companion/android/database/server/ServerConnectionInfo.kt @@ -28,6 +28,10 @@ data class ServerConnectionInfo( val useCloud: Boolean = false, @ColumnInfo(name = "internal_ssids") val internalSsids: List = emptyList(), + @ColumnInfo(name = "internal_ethernet") + val internalEthernet: Boolean? = null, + @ColumnInfo(name = "internal_vpn") + val internalVpn: Boolean? = null, @ColumnInfo(name = "prioritize_internal") val prioritizeInternal: Boolean = false ) { @@ -91,14 +95,29 @@ data class ServerConnectionInfo( fun canUseCloud(): Boolean = !this.cloudUrl.isNullOrBlank() - fun isHomeWifiSsid(): Boolean = wifiHelper.isUsingSpecificWifi(internalSsids) - fun isInternal(): Boolean { - val usesInternalSsid = wifiHelper.isUsingSpecificWifi(internalSsids) - val usesWifi = wifiHelper.isUsingWifi() - val localUrl = internalUrl - Log.d(this::class.simpleName, "localUrl is: ${!localUrl.isNullOrBlank()}, usesInternalSsid is: $usesInternalSsid, usesWifi is: $usesWifi") - return !localUrl.isNullOrBlank() && usesInternalSsid && usesWifi + if (internalUrl.isNullOrBlank()) return false + + if (internalEthernet == true) { + val usesEthernet = wifiHelper.isUsingEthernet() + Log.d(this::class.simpleName, "usesEthernet is: $usesEthernet") + if (usesEthernet) return true + } + + if (internalVpn == true) { + val usesVpn = wifiHelper.isUsingVpn() + Log.d(this::class.simpleName, "usesVpn is: $usesVpn") + if (usesVpn) return true + } + + return if (internalSsids.isNotEmpty()) { + val usesInternalSsid = wifiHelper.isUsingSpecificWifi(internalSsids) + val usesWifi = wifiHelper.isUsingWifi() + Log.d(this::class.simpleName, "usesInternalSsid is: $usesInternalSsid, usesWifi is: $usesWifi") + usesInternalSsid && usesWifi + } else { + false + } } } diff --git a/common/src/main/res/values-night/colors.xml b/common/src/main/res/values-night/colors.xml index aef60841e58..1d8b363d97d 100644 --- a/common/src/main/res/values-night/colors.xml +++ b/common/src/main/res/values-night/colors.xml @@ -20,6 +20,8 @@ #2B2B2B #111111 #282828 + #182b35 + #382d18 #CAC4D0 #66CAC4D0 \ No newline at end of file diff --git a/common/src/main/res/values/colors.xml b/common/src/main/res/values/colors.xml index c8cec6e34d5..1845be3cf6e 100644 --- a/common/src/main/res/values/colors.xml +++ b/common/src/main/res/values/colors.xml @@ -26,7 +26,9 @@ @android:color/white #fafafa #f5f5f5 + #dbeef7 #1fffa600 + #039be5 #ffa600 #43a047 #FDD663 diff --git a/common/src/main/res/values/strings.xml b/common/src/main/res/values/strings.xml index 3b0260437df..e57ac6ed1d0 100644 --- a/common/src/main/res/values/strings.xml +++ b/common/src/main/res/values/strings.xml @@ -394,7 +394,7 @@ Old location Very old location Use location history - Android set up restrictions for apps which want to use your WiFi, because WiFi can be theoretically used to determine your location.\n\nAlso to ensure the app can access WiFi in background (URL decision making, sensors) you need to allow location access all the time.\n\nTherefore, to use this option location access all the time is needed.\n\nContinue granting location access? + Android set up restrictions for apps which want to use your Wi-Fi, because Wi-Fi can be theoretically used to determine your location.\n\nTo ensure the app can access Wi-Fi in background (URL decision making, sensors), you must allow location access all the time.\n\nTherefore, to use this option location access all the time is needed.\n\nContinue granting location access? Location access Location tracking View a history of location tracking updates to troubleshoot the device tracker @@ -402,8 +402,8 @@ Location Use biometric or screen lock credentials to unlock app Lock app - Disable app lock when connected to the home WiFi network - Unlock on home WiFi + Disable app lock when connected to the home network + Unlock on home network Locks App locked! Log file could not be created. @@ -425,10 +425,14 @@ Manage all sensors Manage launcher shortcuts Manage shortcuts - This SSID already exists. + Ethernet connected + This SSID already exists New Wi-Fi SSID - The internal connection URL will be used when connected to one of the listed SSIDs. - Manage SSID(s) + Indicate when you\'re connected to your home network, based on Wi-Fi, ethernet or VPN. This can be used to, for example, use the internal connection URL or disable the app lock when at home. + Home Assistant needs additional access to read Wi-Fi information. + Wi-Fi network connected + VPN connected + Make sure to set up your home network correctly. Adding public Wi-Fi networks or using multiple ethernet/VPN connections may unintentionally expose information about or access to your app or server. Manage this notification Set up and manage quick settings tiles here. They will not function until you set them up here. Manage tiles @@ -498,6 +502,7 @@ None None selected Your connection to this site is not private. + Not set Full notification data Failed to send event on notification dismissed History of notifications @@ -524,11 +529,10 @@ Allow application to detect call occurrence and notify server about it. Calls tracking Internal connection URL - Configure your WiFi SSID(s) Connection information Home Assistant URL - Home network WiFi SSID - When loading dashboard and unknown if connected to home network WiFi + Home network + When loading dashboard and unknown if connected to home network Use internal connection URL Use internal connection URL (use this if you typically turn location access off) Use Home Assistant URL (recommended) @@ -592,7 +596,7 @@ The current battery level of the device The current charging state of the battery The current battery temperature - Send BLE iBeacon with configured interval, used to track presence around house, e.g. together with roomassistant, esp32-mqtt-room or espresence projects.\n\nWarning: this can affect battery life, particularly if the \"Transmitter power\" setting is set to High or \"Advertise Mode\" is set to Low latency.\n\nSettings allow for specifying:\n- \"UUID\" (standard UUID format), \"Major\" and \"Minor\" (should be 0 - 65535), to tailor identifiers and groups. The default Minor value 40004 has the special meaning of forcing the beacon to be recognized by the iBeacon Tracker integration\n- \"Transmitter Power\" and \"Advertise Mode\" to help to preserve battery life (use lowest values if possible)\n - \"Measured Power\" to specify power measured at 1m (initial default -59)\n\nIt is also possible to set beacons to only be transmitted when connected to a Home Network WiFi SSID, which may be desirable for privacy and battery life.\n\nNote:\nAdditionally a separate setting exists (\"Enable Transmitter\") to stop or start transmitting. + Send BLE iBeacon with configured interval, used to track presence around house, e.g. together with roomassistant, esp32-mqtt-room or espresence projects.\n\nWarning: this can affect battery life, particularly if the \"Transmitter power\" setting is set to High or \"Advertise Mode\" is set to Low latency.\n\nSettings allow for specifying:\n- \"UUID\" (standard UUID format), \"Major\" and \"Minor\" (should be 0 - 65535), to tailor identifiers and groups. The default Minor value 40004 has the special meaning of forcing the beacon to be recognized by the iBeacon Tracker integration\n- \"Transmitter Power\" and \"Advertise Mode\" to help to preserve battery life (use lowest values if possible)\n - \"Measured Power\" to specify power measured at 1m (initial default -59)\n\nIt is also possible to set beacons to only be transmitted when connected to a Home network, which may be desirable for privacy and battery life.\n\nNote:\nAdditionally a separate setting exists (\"Enable Transmitter\") to stop or start transmitting. Scans for iBeacons and shows the IDs of nearby beacons and their distance in meters. A notification will be shown on the device when scanning is actively running.\n\nWarning: this can affect battery life, especially with a short \"Scan Interval\".\n\nSettings allow for specifying:\n- \"Filter Iterations\" (should be 1 - 100, default: 10), higher values will result in more stable measurements but also less responsiveness.\n- \"Filter RSSI Multiplier\" (should be 1.0 - 2.0, default: 1.05), can be used to achieve more stable measurements when beacons are farther away. This will also affect responsiveness.\n- \"Scan Interval\" (default: 500) milliseconds between scans. Shorter intervals will drain the battery more quickly.\n- \"Scan Period\" (default: 1100) milliseconds to scan for beacons. Most beacons will send a signal every second so this value should be at least 1100ms.\n- \"UUID Filter\" allows to restrict the reported beacons by including (or excluding) those with the selected UUIDs.\n- \"Exclude selected UUIDs\", if false (default) only the beacons with the selected UUIDs are reported. If true all beacons except the selected ones are reported. Not available when \"UUID Filter\" is empty.\n\nNote:\nAdditionally a separate setting exists (\"Enable Beacon Monitor\") to stop or start scanning. Information about currently connected Bluetooth devices Whether Bluetooth is enabled on the device @@ -717,7 +721,7 @@ Measured power Minor Enable transmitter - Transmit on Home Network WiFi SSID only + Transmit on Home network only High Low Medium @@ -1081,8 +1085,8 @@ Pinned shortcuts WebView remote debugging Allow remote debugging of WebView to troubleshoot Home Assistant frontend issues - On home WiFi only\n\nNotifications will be delivered by Google only when not on home WiFi. - On home WiFi only\n\nNotifications will only be received when on home WiFi. + On home network only\n\nNotifications will be delivered by Google only when not on home network. + On home network only\n\nNotifications will only be received when on home network. Sensor update frequency Sensors will update either instantly or on a defined interval. If the sensor supports instant updates then it will always receive instant updates. View the sensor details to learn which sensors update instantly. Location sensors are not part of the update frequency setting as they have their own interval, consider using High Accuracy mode if you need to adjust the update interval.\n\nIf the sensor does not support instant updates then it will update based on one of the below selected options.\n\nYou must restart the application when you make any changes to this setting. Normal\n\nSensors will update on a 15 minute interval.