Add daily floors sensor for Wear OS3 devices (#3005)

* Add daily floors sensor for Wear OS3 devices

* Do a compatibility check for sensor availability in health services

* Print more logs and attempt to send latest data, update more getAvailableSensor methods

* Process review comments

* Process more review comments, use actual time in millis

* Clear out available sensors when updating with new list

* Log the capabilities in case we need to troubleshoot later

* Review comment

* Missed one additional review comment

* Return immediately instead of storing a list
This commit is contained in:
Daniel Shokouhi 2022-11-09 13:12:13 -08:00 committed by GitHub
parent e01246ae28
commit affc60178e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
51 changed files with 326 additions and 112 deletions

View file

@ -196,7 +196,7 @@ class ActivitySensorManager : BroadcastReceiver(), SensorManager {
override val name: Int
get() = commonR.string.sensor_name_activity
override fun getAvailableSensors(context: Context): List<SensorManager.BasicSensor> {
override suspend fun getAvailableSensors(context: Context): List<SensorManager.BasicSensor> {
return listOf(activity, sleepConfidence, sleepSegment)
}

View file

@ -44,7 +44,7 @@ class GeocodeSensorManager : SensorManager {
get() = false
override val name: Int
get() = commonR.string.sensor_name_geolocation
override fun getAvailableSensors(context: Context): List<SensorManager.BasicSensor> {
override suspend fun getAvailableSensors(context: Context): List<SensorManager.BasicSensor> {
return listOf(geocodedLocation)
}

View file

@ -654,7 +654,7 @@ class LocationSensorManager : LocationSensorManagerBase() {
}
}
override fun getAvailableSensors(context: Context): List<SensorManager.BasicSensor> {
override suspend fun getAvailableSensors(context: Context): List<SensorManager.BasicSensor> {
return if (DisabledLocationHandler.hasGPS(context)) {
listOf(singleAccurateLocation, backgroundLocation, zoneLocation, highAccuracyMode, highAccuracyUpdateInterval)
} else {

View file

@ -134,7 +134,7 @@ class BluetoothSensorManager : SensorManager {
get() = false
override val name: Int
get() = commonR.string.sensor_name_bluetooth
override fun getAvailableSensors(context: Context): List<SensorManager.BasicSensor> {
override suspend fun getAvailableSensors(context: Context): List<SensorManager.BasicSensor> {
return listOf(bluetoothConnection, bluetoothState, bleTransmitter, beaconMonitor)
}

View file

@ -30,7 +30,7 @@ class DevicePolicyManager : SensorManager {
override val name: Int
get() = R.string.sensor_name_device_policy
override fun getAvailableSensors(context: Context): List<SensorManager.BasicSensor> {
override suspend fun getAvailableSensors(context: Context): List<SensorManager.BasicSensor> {
return listOf(isWorkProfile)
}

View file

@ -34,7 +34,7 @@ class DisplaySensorManager : SensorManager {
override val name: Int
get() = commonR.string.sensor_name_display_sensors
override fun getAvailableSensors(context: Context): List<SensorManager.BasicSensor> {
override suspend fun getAvailableSensors(context: Context): List<SensorManager.BasicSensor> {
return listOf(screenBrightness, screenOffTimeout)
}

View file

@ -30,7 +30,7 @@ class DynamicColorSensorManager : SensorManager {
override val name: Int
get() = commonR.string.sensor_name_dynamic_color
override fun getAvailableSensors(context: Context): List<SensorManager.BasicSensor> {
override suspend fun getAvailableSensors(context: Context): List<SensorManager.BasicSensor> {
return listOf(accentColorSensor)
}

View file

@ -54,7 +54,7 @@ class KeyguardSensorManager : SensorManager {
override val name: Int
get() = commonR.string.sensor_name_keyguard
override fun getAvailableSensors(context: Context): List<SensorManager.BasicSensor> {
override suspend fun getAvailableSensors(context: Context): List<SensorManager.BasicSensor> {
return when {
(Build.VERSION.SDK_INT >= 23) -> listOf(deviceLocked, deviceSecure, keyguardLocked, keyguardSecure)
(Build.VERSION.SDK_INT >= 22) -> listOf(deviceLocked, keyguardLocked, keyguardSecure)

View file

@ -32,7 +32,7 @@ class LastAppSensorManager : SensorManager {
override val name: Int
get() = commonR.string.sensor_name_last_app
override fun getAvailableSensors(context: Context): List<SensorManager.BasicSensor> {
override suspend fun getAvailableSensors(context: Context): List<SensorManager.BasicSensor> {
return listOf(last_used)
}

View file

@ -42,7 +42,7 @@ class LastRebootSensorManager : SensorManager {
override val name: Int
get() = commonR.string.sensor_name_last_reboot
override fun getAvailableSensors(context: Context): List<SensorManager.BasicSensor> {
override suspend fun getAvailableSensors(context: Context): List<SensorManager.BasicSensor> {
return listOf(lastRebootSensor)
}

View file

@ -38,7 +38,7 @@ class LightSensorManager : SensorManager, SensorEventListener {
override val name: Int
get() = commonR.string.sensor_name_light
override fun getAvailableSensors(context: Context): List<SensorManager.BasicSensor> {
override suspend fun getAvailableSensors(context: Context): List<SensorManager.BasicSensor> {
return listOf(lightSensor)
}

View file

@ -40,7 +40,7 @@ class MobileDataManager : SensorManager {
override val name: Int
get() = commonR.string.sensor_name_mobile_data
override fun getAvailableSensors(context: Context): List<SensorManager.BasicSensor> {
override suspend fun getAvailableSensors(context: Context): List<SensorManager.BasicSensor> {
return listOf(mobileDataState, mobileDataRoaming)
}

View file

@ -74,7 +74,7 @@ class NotificationSensorManager : NotificationListenerService(), SensorManager {
}
override val name: Int
get() = commonR.string.sensor_name_last_notification
override fun getAvailableSensors(context: Context): List<SensorManager.BasicSensor> {
override suspend fun getAvailableSensors(context: Context): List<SensorManager.BasicSensor> {
return listOf(lastNotification, lastRemovedNotification, activeNotificationCount, mediaSession)
}
override val enabledByDefault: Boolean

View file

@ -57,7 +57,7 @@ class PhoneStateSensorManager : SensorManager {
override fun hasSensor(context: Context): Boolean {
return context.packageManager.hasSystemFeature(PackageManager.FEATURE_TELEPHONY)
}
override fun getAvailableSensors(context: Context): List<SensorManager.BasicSensor> {
override suspend fun getAvailableSensors(context: Context): List<SensorManager.BasicSensor> {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1)
listOf(phoneState, sim_1, sim_2)
else listOf(phoneState)

View file

@ -41,7 +41,7 @@ class PressureSensorManager : SensorManager, SensorEventListener {
override val name: Int
get() = commonR.string.sensor_name_pressure
override fun getAvailableSensors(context: Context): List<SensorManager.BasicSensor> {
override suspend fun getAvailableSensors(context: Context): List<SensorManager.BasicSensor> {
return listOf(pressureSensor)
}

View file

@ -40,7 +40,7 @@ class ProximitySensorManager : SensorManager, SensorEventListener {
override val name: Int
get() = commonR.string.sensor_name_proximity
override fun getAvailableSensors(context: Context): List<SensorManager.BasicSensor> {
override suspend fun getAvailableSensors(context: Context): List<SensorManager.BasicSensor> {
return listOf(proximitySensor)
}

View file

@ -31,7 +31,7 @@ class QuestSensorManager : SensorManager {
override val name: Int
get() = commonR.string.sensor_name_quest
override fun getAvailableSensors(context: Context): List<SensorManager.BasicSensor> {
override suspend fun getAvailableSensors(context: Context): List<SensorManager.BasicSensor> {
return listOf(
headsetMounted
)

View file

@ -66,7 +66,7 @@ class StorageSensorManager : SensorManager {
get() = false
override val name: Int
get() = commonR.string.sensor_name_storage
override fun getAvailableSensors(context: Context): List<SensorManager.BasicSensor> {
override suspend fun getAvailableSensors(context: Context): List<SensorManager.BasicSensor> {
return listOf(storageSensor, externalStorage)
}

View file

@ -29,7 +29,7 @@ class TimeZoneManager : SensorManager {
override val name: Int
get() = commonR.string.sensor_name_time_zone
override fun getAvailableSensors(context: Context): List<SensorManager.BasicSensor> {
override suspend fun getAvailableSensors(context: Context): List<SensorManager.BasicSensor> {
return listOf(currentTimeZone)
}

View file

@ -67,7 +67,7 @@ class TrafficStatsManager : SensorManager {
override val name: Int
get() = commonR.string.sensor_name_traffic_stats
override fun getAvailableSensors(context: Context): List<SensorManager.BasicSensor> {
override suspend fun getAvailableSensors(context: Context): List<SensorManager.BasicSensor> {
return if (hasCellular) {
listOf(rxBytesMobile, txBytesMobile, rxBytesTotal, txBytesTotal)
} else listOf(rxBytesTotal, txBytesTotal)

View file

@ -17,6 +17,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel
import io.homeassistant.companion.android.common.bluetooth.BluetoothUtils
import io.homeassistant.companion.android.common.data.integration.IntegrationRepository
import io.homeassistant.companion.android.common.sensors.NetworkSensorManager
import io.homeassistant.companion.android.common.sensors.SensorManager
import io.homeassistant.companion.android.common.util.DisabledLocationHandler
import io.homeassistant.companion.android.database.sensor.SensorDao
import io.homeassistant.companion.android.database.sensor.SensorSetting
@ -75,10 +76,17 @@ class SensorDetailViewModel @Inject constructor(
private val _permissionSnackbar = MutableSharedFlow<PermissionSnackbar>()
var permissionSnackbar = _permissionSnackbar.asSharedFlow()
val sensorManager = SensorReceiver.MANAGERS
.find { it.getAvailableSensors(getApplication()).any { sensor -> sensor.id == sensorId } }
val basicSensor = sensorManager?.getAvailableSensors(getApplication())
?.find { it.id == sensorId }
val sensorManager: SensorManager? = runBlocking {
SensorReceiver.MANAGERS
.find {
it.getAvailableSensors(getApplication()).any { sensor -> sensor.id == sensorId }
}
}
val basicSensor: SensorManager.BasicSensor? = runBlocking {
sensorManager?.getAvailableSensors(getApplication())
?.find { it.id == sensorId }
}
var sensor by mutableStateOf<SensorWithAttributes?>(null)
private set
@ -133,7 +141,11 @@ class SensorDetailViewModel @Inject constructor(
if ((fineLocation || coarseLocation) &&
!DisabledLocationHandler.isLocationEnabled(getApplication())
) {
val sensorName = basicSensor?.let { getApplication<Application>().getString(basicSensor.name) }.orEmpty()
val sensorName = basicSensor?.let {
getApplication<Application>().getString(
basicSensor.name
)
}.orEmpty()
locationPermissionRequests.value = LocationPermissionsDialog(block = true, sensors = arrayOf(sensorName))
return
} else {

View file

@ -15,7 +15,6 @@ import androidx.fragment.app.viewModels
import com.google.android.material.composethemeadapter.MdcTheme
import dagger.hilt.android.AndroidEntryPoint
import io.homeassistant.companion.android.R
import io.homeassistant.companion.android.sensors.SensorReceiver
import io.homeassistant.companion.android.settings.sensor.views.SensorListView
import io.homeassistant.companion.android.common.R as commonR
@ -93,7 +92,6 @@ class SensorSettingsFragment : Fragment() {
MdcTheme {
SensorListView(
viewModel = viewModel,
managers = SensorReceiver.MANAGERS.sortedBy { getString(it.name) },
onSensorClicked = { sensor ->
parentFragmentManager
.beginTransaction()

View file

@ -7,6 +7,7 @@ import androidx.compose.runtime.setValue
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import io.homeassistant.companion.android.common.sensors.SensorManager
import io.homeassistant.companion.android.database.sensor.Sensor
import io.homeassistant.companion.android.database.sensor.SensorDao
import io.homeassistant.companion.android.sensors.SensorReceiver
@ -30,6 +31,9 @@ class SensorSettingsViewModel @Inject constructor(
var sensors by mutableStateOf<Map<String, Sensor>>(emptyMap())
private set
var allSensors by mutableStateOf<Map<SensorManager, List<SensorManager.BasicSensor>>>(emptyMap())
private set
var searchQuery: String? = null
var sensorFilter by mutableStateOf(SensorFilter.ALL)
private set
@ -44,17 +48,22 @@ class SensorSettingsViewModel @Inject constructor(
}
fun setSensorsSearchQuery(query: String? = "") {
searchQuery = query
filterSensorsList()
viewModelScope.launch {
searchQuery = query
filterSensorsList()
}
}
fun setSensorFilterChoice(filter: SensorFilter) {
sensorFilter = filter
filterSensorsList()
viewModelScope.launch {
sensorFilter = filter
filterSensorsList()
}
}
private fun filterSensorsList() {
private suspend fun filterSensorsList() {
val app = getApplication<Application>()
val managers = SensorReceiver.MANAGERS.sortedBy { app.getString(it.name) }
sensors = SensorReceiver.MANAGERS
.filter { it.hasSensor(app.applicationContext) }
.flatMap { manager ->
@ -76,5 +85,13 @@ class SensorSettingsViewModel @Inject constructor(
.mapNotNull { sensor -> sensorsList.firstOrNull { it.id == sensor.id } }
}
.associateBy { it.id }
allSensors = managers.associateWith { manager ->
manager.getAvailableSensors(app)
.filter { basicSensor ->
sensors.containsKey(basicSensor.id)
}
.sortedBy { app.getString(it.name) }.distinct()
}
}
}

View file

@ -31,20 +31,11 @@ import io.homeassistant.companion.android.common.R as commonR
@Composable
fun SensorListView(
viewModel: SensorSettingsViewModel,
managers: List<SensorManager>,
onSensorClicked: (String) -> Unit
) {
val context = LocalContext.current
val listEntries = managers.associateWith { manager ->
manager.getAvailableSensors(context)
.filter { basicSensor ->
basicSensor.id in viewModel.sensors
}
.sortedBy { context.getString(it.name) }
}
LazyColumn {
listEntries.forEach { (manager, currentSensors) ->
viewModel.allSensors.forEach { (manager, currentSensors) ->
stickyHeader(
key = manager.id()
) {
@ -80,7 +71,7 @@ fun SensorListView(
onSensorClicked = onSensorClicked
)
}
if (currentSensors.any() && manager.id() != listEntries.keys.last().id()) {
if (currentSensors.any() && manager.id() != viewModel.allSensors.keys.last().id()) {
item {
Divider()
}

View file

@ -44,6 +44,7 @@ import android.widget.ImageView
import android.widget.Toast
import androidx.activity.OnBackPressedCallback
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AlertDialog
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
@ -711,7 +712,9 @@ class WebViewActivity : BaseActivity(), io.homeassistant.companion.android.webvi
SensorWorker.start(this)
WebsocketManager.start(this)
checkAndWarnForDisabledLocation()
lifecycleScope.launch {
checkAndWarnForDisabledLocation()
}
changeLog.showChangeLog(this, false)
}
@ -721,7 +724,7 @@ class WebViewActivity : BaseActivity(), io.homeassistant.companion.android.webvi
presenter.setAppActive(false)
}
private fun checkAndWarnForDisabledLocation() {
private suspend fun checkAndWarnForDisabledLocation() {
var showLocationDisabledWarning = false
var settingsWithLocationPermissions = mutableListOf<String>()
if (!DisabledLocationHandler.isLocationEnabled(this) && presenter.isSsidUsed()) {
@ -980,10 +983,12 @@ class WebViewActivity : BaseActivity(), io.homeassistant.companion.android.webvi
windowInsetsController.show(WindowInsetsCompat.Type.systemBars())
}
@RequiresApi(Build.VERSION_CODES.O)
override fun onPictureInPictureModeChanged(
isInPictureInPictureMode: Boolean,
newConfig: Configuration
) {
super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig)
if (exoPlayerView.visibility != View.VISIBLE) {
if (isInPictureInPictureMode) {
(decor.getChildAt(3) as FrameLayout).layoutParams.height =

View file

@ -17,7 +17,7 @@ class ActivitySensorManager : BroadcastReceiver(), SensorManager {
override val name: Int
get() = commonR.string.sensor_name_activity
override fun getAvailableSensors(context: Context): List<SensorManager.BasicSensor> {
override suspend fun getAvailableSensors(context: Context): List<SensorManager.BasicSensor> {
return listOf()
}

View file

@ -22,7 +22,7 @@ class GeocodeSensorManager : SensorManager {
override val name: Int
get() = commonR.string.sensor_name_geolocation
override fun getAvailableSensors(context: Context): List<SensorManager.BasicSensor> {
override suspend fun getAvailableSensors(context: Context): List<SensorManager.BasicSensor> {
return listOf()
}

View file

@ -61,7 +61,7 @@ class LocationSensorManager : LocationSensorManagerBase(), SensorManager {
override val name: Int
get() = commonR.string.sensor_name_location
override fun getAvailableSensors(context: Context): List<SensorManager.BasicSensor> {
override suspend fun getAvailableSensors(context: Context): List<SensorManager.BasicSensor> {
return listOf()
}

View file

@ -100,7 +100,7 @@ class AppSensorManager : SensorManager {
override val name: Int
get() = commonR.string.sensor_name_app_sensor
override fun getAvailableSensors(context: Context): List<SensorManager.BasicSensor> {
override suspend fun getAvailableSensors(context: Context): List<SensorManager.BasicSensor> {
return when {
(Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) ->
listOf(

View file

@ -138,7 +138,7 @@ class AudioSensorManager : SensorManager {
override val name: Int
get() = commonR.string.sensor_name_audio
override fun getAvailableSensors(context: Context): List<SensorManager.BasicSensor> {
override suspend fun getAvailableSensors(context: Context): List<SensorManager.BasicSensor> {
val allSupportedSensors = listOf(
audioSensor, audioState, headphoneState, micMuted, speakerphoneState,
musicActive, volAlarm, volCall, volMusic, volRing, volNotification, volSystem,

View file

@ -103,7 +103,7 @@ class BatterySensorManager : SensorManager {
override val name: Int
get() = commonR.string.sensor_name_battery
override fun getAvailableSensors(context: Context): List<SensorManager.BasicSensor> {
override suspend fun getAvailableSensors(context: Context): List<SensorManager.BasicSensor> {
return listOf(
batteryLevel,
batteryState,

View file

@ -32,7 +32,7 @@ class DNDSensorManager : SensorManager {
override val name: Int
get() = commonR.string.sensor_name_dnd
override fun getAvailableSensors(context: Context): List<SensorManager.BasicSensor> {
override suspend fun getAvailableSensors(context: Context): List<SensorManager.BasicSensor> {
return listOf(dndSensor)
}

View file

@ -32,7 +32,7 @@ class LastUpdateManager : SensorManager {
override val name: Int
get() = commonR.string.sensor_name_last_update
override fun getAvailableSensors(context: Context): List<SensorManager.BasicSensor> {
override suspend fun getAvailableSensors(context: Context): List<SensorManager.BasicSensor> {
return listOf(lastUpdate)
}

View file

@ -126,7 +126,7 @@ class NetworkSensorManager : SensorManager {
get() = false
override val name: Int
get() = commonR.string.sensor_name_network
override fun getAvailableSensors(context: Context): List<SensorManager.BasicSensor> {
override suspend fun getAvailableSensors(context: Context): List<SensorManager.BasicSensor> {
val list = listOf(
wifiConnection,
bssidState,

View file

@ -38,7 +38,7 @@ class NextAlarmManager : SensorManager {
override val name: Int
get() = commonR.string.sensor_name_alarm
override fun getAvailableSensors(context: Context): List<SensorManager.BasicSensor> {
override suspend fun getAvailableSensors(context: Context): List<SensorManager.BasicSensor> {
return listOf(nextAlarm)
}

View file

@ -49,7 +49,7 @@ class PowerSensorManager : SensorManager {
override val name: Int
get() = commonR.string.sensor_name_power
override fun getAvailableSensors(context: Context): List<SensorManager.BasicSensor> {
override suspend fun getAvailableSensors(context: Context): List<SensorManager.BasicSensor> {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
listOf(interactiveDevice, doze, powerSave)
} else {

View file

@ -99,7 +99,7 @@ interface SensorManager {
*/
fun requestSensorUpdate(context: Context)
fun getAvailableSensors(context: Context): List<BasicSensor>
suspend fun getAvailableSensors(context: Context): List<BasicSensor>
/**
* Check if the user's device supports this type of sensor

View file

@ -39,7 +39,7 @@ class StepsSensorManager : SensorManager, SensorEventListener {
override val name: Int
get() = commonR.string.sensor_name_steps
override fun getAvailableSensors(context: Context): List<SensorManager.BasicSensor> {
override suspend fun getAvailableSensors(context: Context): List<SensorManager.BasicSensor> {
return listOf(stepsSensor)
}

View file

@ -947,4 +947,6 @@
<string name="sensor_name_health_services">Health Services</string>
<string name="sensor_name_heart_rate">Heart Rate</string>
<string name="sensor_description_heart_rate">Current heart rate in beats per minute, an attribute also exists for the reported accuracy from the sensor</string>
<string name="sensor_name_daily_floors">Daily Floors</string>
<string name="sensor_description_daily_floors">The total number floors climbed over a day, where the previous day ends and a new day begins at 12:00 AM local time</string>
</resources>

View file

@ -10,7 +10,6 @@ import dagger.hilt.android.AndroidEntryPoint
import io.homeassistant.companion.android.home.views.LoadHomePage
import io.homeassistant.companion.android.onboarding.OnboardingActivity
import io.homeassistant.companion.android.onboarding.integration.MobileAppIntegrationActivity
import io.homeassistant.companion.android.sensors.SensorReceiver
import io.homeassistant.companion.android.sensors.SensorWorker
import javax.inject.Inject
@ -47,15 +46,7 @@ class HomeActivity : ComponentActivity(), HomeView {
super.onResume()
SensorWorker.start(this)
initAllSensors()
}
private fun initAllSensors() {
for (manager in SensorReceiver.MANAGERS) {
for (basicSensor in manager.getAvailableSensors(this)) {
manager.isEnabled(this, basicSensor.id)
}
}
mainViewModel.initAllSensors()
}
override fun onPause() {

View file

@ -10,6 +10,7 @@ import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import io.homeassistant.companion.android.HomeAssistantApplication
import io.homeassistant.companion.android.common.data.integration.Entity
import io.homeassistant.companion.android.common.data.integration.domain
import io.homeassistant.companion.android.common.data.websocket.WebSocketState
@ -23,6 +24,7 @@ import io.homeassistant.companion.android.database.wear.FavoriteCaches
import io.homeassistant.companion.android.database.wear.FavoriteCachesDao
import io.homeassistant.companion.android.database.wear.FavoritesDao
import io.homeassistant.companion.android.database.wear.getAllFlow
import io.homeassistant.companion.android.sensors.SensorReceiver
import io.homeassistant.companion.android.sensors.SensorWorker
import io.homeassistant.companion.android.util.RegistriesDataHandler
import kotlinx.coroutines.async
@ -108,6 +110,8 @@ class MainViewModel @Inject constructor(
val sensors = sensorsDao.getAllFlow().collectAsState()
var availableSensors = emptyList<SensorManager.BasicSensor>()
private fun loadSettings() {
viewModelScope.launch {
if (!homePresenter.isConnected()) {
@ -291,6 +295,26 @@ class MainViewModel @Inject constructor(
SensorWorker.start(getApplication())
}
fun updateAllSensors(sensorManager: SensorManager) {
availableSensors = emptyList()
viewModelScope.launch {
val context = getApplication<HomeAssistantApplication>().applicationContext
availableSensors = sensorManager
.getAvailableSensors(context)
.sortedBy { context.getString(it.name) }.distinct()
}
}
fun initAllSensors() {
viewModelScope.launch {
for (manager in SensorReceiver.MANAGERS) {
for (basicSensor in manager.getAvailableSensors(getApplication())) {
manager.isEnabled(getApplication(), basicSensor.id)
}
}
}
}
fun getAreaForEntity(entityId: String): AreaRegistryResponse? =
RegistriesDataHandler.getAreaForEntity(entityId, areaRegistry, deviceRegistry, entityRegistry)

View file

@ -222,8 +222,10 @@ fun LoadHomePage(
val sensorManager = getSensorManagers().first { sensorManager ->
sensorManager.id() == sensorManagerId
}
mainViewModel.updateAllSensors(sensorManager)
SensorManagerUi(
allSensors = mainViewModel.sensors.value,
allAvailSensors = mainViewModel.availableSensors,
sensorManager = sensorManager,
) { sensorId, isEnabled ->
mainViewModel.enableDisableSensor(sensorManager, sensorId, isEnabled)

View file

@ -1,39 +1,38 @@
package io.homeassistant.companion.android.home.views
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Devices
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.wear.compose.material.CircularProgressIndicator
import androidx.wear.compose.material.PositionIndicator
import androidx.wear.compose.material.Scaffold
import androidx.wear.compose.material.ScalingLazyListState
import androidx.wear.compose.material.rememberScalingLazyListState
import io.homeassistant.companion.android.common.R
import io.homeassistant.companion.android.common.sensors.SensorManager
import io.homeassistant.companion.android.database.sensor.Sensor
import io.homeassistant.companion.android.theme.WearAppTheme
import io.homeassistant.companion.android.util.batterySensorManager
import io.homeassistant.companion.android.util.sensorList
import io.homeassistant.companion.android.views.ListHeader
import io.homeassistant.companion.android.views.ThemeLazyColumn
@Composable
fun SensorManagerUi(
allSensors: List<Sensor>?,
allAvailSensors: List<SensorManager.BasicSensor>?,
sensorManager: SensorManager,
onSensorClicked: (String, Boolean) -> Unit,
) {
val scalingLazyListState: ScalingLazyListState = rememberScalingLazyListState()
val context = LocalContext.current
val availableSensors by remember {
mutableStateOf(
sensorManager
.getAvailableSensors(context)
.sortedBy { context.getString(it.name) }
)
}
WearAppTheme {
Scaffold(
positionIndicator = {
@ -49,21 +48,38 @@ fun SensorManagerUi(
ListHeader(id = sensorManager.name)
}
val currentSensors = allSensors?.filter { sensor ->
availableSensors.firstOrNull { availableSensor ->
allAvailSensors?.firstOrNull { availableSensor ->
sensor.id == availableSensor.id
} != null
}
items(availableSensors.size, { availableSensors[it].id }) { index ->
val basicSensor = availableSensors[index]
val sensor = currentSensors?.firstOrNull { sensor ->
sensor.id == basicSensor.id
if (allAvailSensors?.isEmpty() == true) {
item {
Column(
modifier = Modifier
.fillMaxSize()
.padding(vertical = 32.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
ListHeader(id = R.string.loading)
CircularProgressIndicator()
}
}
} else {
allAvailSensors?.size?.let { int ->
items(int, { allAvailSensors[it].id }) { index ->
val basicSensor = allAvailSensors[index]
val sensor = currentSensors?.firstOrNull { sensor ->
sensor.id == basicSensor.id
}
SensorUi(
sensor = sensor,
manager = sensorManager,
basicSensor = basicSensor,
) { sensorId, enabled -> onSensorClicked(sensorId, enabled) }
}
}
SensorUi(
sensor = sensor,
manager = sensorManager,
basicSensor = basicSensor,
) { sensorId, enabled -> onSensorClicked(sensorId, enabled) }
}
}
}
@ -76,6 +92,7 @@ private fun PreviewSensorManagerUI() {
CompositionLocalProvider {
SensorManagerUi(
allSensors = listOf(),
allAvailSensors = sensorList,
sensorManager = batterySensorManager
) { _, _ -> }
}

View file

@ -21,6 +21,7 @@ import io.homeassistant.companion.android.common.R
import io.homeassistant.companion.android.common.sensors.SensorManager
import io.homeassistant.companion.android.database.sensor.Sensor
import io.homeassistant.companion.android.util.batterySensorManager
import kotlinx.coroutines.runBlocking
@Composable
fun SensorUi(
@ -96,11 +97,12 @@ fun SensorUi(
@Preview(device = Devices.WEAR_OS_LARGE_ROUND)
@Composable
private fun PreviewSensorUI() {
val context = LocalContext.current
CompositionLocalProvider {
SensorUi(
sensor = null,
manager = batterySensorManager,
basicSensor = batterySensorManager.getAvailableSensors(LocalContext.current).first()
basicSensor = runBlocking { batterySensorManager.getAvailableSensors(context).first() }
) { _, _ -> }
}
}

View file

@ -30,7 +30,7 @@ class BedtimeModeSensorManager : SensorManager {
override val name: Int
get() = commonR.string.sensor_name_bedtime_mode
override fun getAvailableSensors(context: Context): List<SensorManager.BasicSensor> {
override suspend fun getAvailableSensors(context: Context): List<SensorManager.BasicSensor> {
return listOf(bedtimeMode)
}

View file

@ -3,18 +3,27 @@ package io.homeassistant.companion.android.sensors
import android.Manifest
import android.content.Context
import android.os.Build
import android.os.SystemClock
import android.util.Log
import androidx.annotation.RequiresApi
import androidx.health.services.client.HealthServices
import androidx.health.services.client.HealthServicesClient
import androidx.health.services.client.PassiveListenerCallback
import androidx.health.services.client.PassiveMonitoringClient
import androidx.health.services.client.data.DataPointContainer
import androidx.health.services.client.data.DataType
import androidx.health.services.client.data.DeltaDataType
import androidx.health.services.client.data.ExerciseType
import androidx.health.services.client.data.IntervalDataPoint
import androidx.health.services.client.data.PassiveListenerConfig
import androidx.health.services.client.data.PassiveMonitoringCapabilities
import androidx.health.services.client.data.UserActivityInfo
import androidx.health.services.client.data.UserActivityState
import io.homeassistant.companion.android.common.sensors.SensorManager
import io.homeassistant.companion.android.database.AppDatabase
import kotlinx.coroutines.guava.await
import kotlinx.coroutines.runBlocking
import java.time.Instant
import io.homeassistant.companion.android.common.R as commonR
@RequiresApi(Build.VERSION_CODES.R)
@ -31,16 +40,29 @@ class HealthServicesSensorManager : SensorManager {
entityCategory = SensorManager.ENTITY_CATEGORY_DIAGNOSTIC,
updateType = SensorManager.BasicSensor.UpdateType.INTENT
)
private val dailyFloors = SensorManager.BasicSensor(
"daily_floors",
"sensor",
commonR.string.sensor_name_daily_floors,
commonR.string.sensor_description_daily_floors,
"mdi:stairs",
unitOfMeasurement = "floors",
entityCategory = SensorManager.ENTITY_CATEGORY_DIAGNOSTIC,
updateType = SensorManager.BasicSensor.UpdateType.INTENT
)
}
private lateinit var latestContext: Context
private var healthClient: HealthServicesClient? = null
private var passiveMonitoringClient: PassiveMonitoringClient? = null
private var passiveMonitoringCapabilities: PassiveMonitoringCapabilities? = null
private var passiveListenerConfig: PassiveListenerConfig? = null
private var callBackRegistered = false
private var dataTypesRegistered = emptySet<DataType<*, *>>()
private var activityStateRegistered = false
override fun docsLink(): String {
return "https://companion.home-assistant.io/docs/wear-os/#sensors"
return "https://companion.home-assistant.io/docs/wear-os/sensors#health-services"
}
override val enabledByDefault: Boolean
get() = false
@ -48,8 +70,22 @@ class HealthServicesSensorManager : SensorManager {
override val name: Int
get() = commonR.string.sensor_name_health_services
override fun getAvailableSensors(context: Context): List<SensorManager.BasicSensor> {
return listOf(userActivityState)
override suspend fun getAvailableSensors(context: Context): List<SensorManager.BasicSensor> {
latestContext = context
if (healthClient == null)
healthClient = HealthServices.getClient(latestContext)
if (passiveMonitoringClient == null)
passiveMonitoringClient = healthClient?.passiveMonitoringClient
if (passiveMonitoringCapabilities == null) {
passiveMonitoringCapabilities = passiveMonitoringClient?.getCapabilitiesAsync()?.await()
Log.d(TAG, "Supported capabilities: $passiveMonitoringCapabilities")
}
val supportedSensors = mutableListOf(userActivityState)
if (passiveMonitoringCapabilities?.supportedDataTypesPassiveMonitoring?.contains(DataType.FLOORS_DAILY) == true)
supportedSensors += dailyFloors
return supportedSensors
}
override fun requiredPermissions(sensorId: String): Array<String> {
@ -62,22 +98,37 @@ class HealthServicesSensorManager : SensorManager {
override fun requestSensorUpdate(context: Context) {
latestContext = context
updateUserActivityState()
updateHealthServices()
}
private fun updateUserActivityState() {
if (!isEnabled(latestContext, userActivityState.id)) {
passiveMonitoringClient?.clearPassiveListenerCallbackAsync()
callBackRegistered = false
private fun updateHealthServices() {
val activityStateEnabled = isEnabled(latestContext, userActivityState.id)
val dailyFloorEnabled = isEnabled(latestContext, dailyFloors.id)
if (!activityStateEnabled && !dailyFloorEnabled) {
clearHealthServicesCallBack()
return
}
if (healthClient == null) healthClient = HealthServices.getClient(latestContext)
if (passiveMonitoringClient == null) passiveMonitoringClient = healthClient?.passiveMonitoringClient
val dataTypes = mutableSetOf<DataType<*, *>>()
if (dailyFloorEnabled)
dataTypes += DataType.FLOORS_DAILY
passiveListenerConfig = PassiveListenerConfig.builder()
.setShouldUserActivityInfoBeRequested(isEnabled(latestContext, userActivityState.id))
.setShouldUserActivityInfoBeRequested(activityStateEnabled)
.setDataTypes(dataTypes)
.build()
if (dataTypesRegistered != dataTypes || activityStateRegistered != activityStateEnabled) {
clearHealthServicesCallBack()
}
activityStateRegistered = activityStateEnabled
dataTypesRegistered = dataTypes
val passiveListenerCallback: PassiveListenerCallback = object : PassiveListenerCallback {
override fun onUserActivityInfoReceived(info: UserActivityInfo) {
Log.d(TAG, "User activity state: ${info.userActivityState.name}")
@ -90,21 +141,61 @@ class HealthServicesSensorManager : SensorManager {
UserActivityState.USER_ACTIVITY_EXERCISE -> "exercise"
else -> "unknown"
},
when (info.userActivityState) {
UserActivityState.USER_ACTIVITY_EXERCISE -> "mdi:run"
UserActivityState.USER_ACTIVITY_ASLEEP -> "mdi:sleep"
UserActivityState.USER_ACTIVITY_PASSIVE -> "mdi:human-handsdown"
else -> userActivityState.statelessIcon
},
getActivityIcon(info),
mapOf(
"time" to info.stateChangeTime,
"exercise_type" to info.exerciseInfo?.exerciseType?.name
)
),
forceUpdate = info.userActivityState == UserActivityState.USER_ACTIVITY_EXERCISE
)
SensorWorker.start(latestContext)
}
override fun onNewDataPointsReceived(dataPoints: DataPointContainer) {
Log.d(TAG, "New data point received: ${dataPoints.dataTypes}")
val floorsDaily = dataPoints.getData(DataType.FLOORS_DAILY)
val bootInstant =
Instant.ofEpochMilli(System.currentTimeMillis() - SystemClock.elapsedRealtime())
var latest = 0
var lastIndex = 0
dataPoints.dataTypes.forEachIndexed { _, dataType ->
if (dataType is DeltaDataType<*, *>) {
val data = dataPoints.getData(dataType)
data.forEachIndexed { indexPoint, dataPoint ->
if (dataPoint is IntervalDataPoint) {
val endTime = dataPoint.getEndInstant(bootInstant)
Log.d(
TAG,
"Data for ${dataType.name} index: $indexPoint with value: ${dataPoint.value} end time: ${endTime.toEpochMilli()}"
)
}
}
}
}
if (floorsDaily.isNotEmpty()) {
floorsDaily.forEachIndexed { index, intervalDataPoint ->
val endTime = intervalDataPoint.getEndInstant(bootInstant)
Log.d(TAG, "Daily Floors data index: $index with value: ${intervalDataPoint.value} end time: ${endTime.toEpochMilli()}")
if (endTime.toEpochMilli() > latest) {
latest = endTime.toEpochMilli().toInt()
lastIndex = index
}
}
onSensorUpdated(
latestContext,
dailyFloors,
floorsDaily[lastIndex].value,
dailyFloors.statelessIcon,
mapOf()
)
SensorWorker.start(latestContext)
}
}
override fun onPermissionLost() {
val sensorDao = AppDatabase.getInstance(latestContext).sensorDao()
runBlocking {
@ -116,6 +207,11 @@ class HealthServicesSensorManager : SensorManager {
Log.e(TAG, "onRegistrationFailed: ", throwable)
callBackRegistered = false
}
override fun onRegistered() {
Log.d(TAG, "Health services callback successfully registered for the following data types: ${passiveListenerConfig!!.dataTypes} User Activity Info: ${passiveListenerConfig!!.shouldUserActivityInfoBeRequested} Health Events: ${passiveListenerConfig!!.healthEventTypes}")
callBackRegistered = true
}
}
if (!callBackRegistered) {
@ -124,6 +220,61 @@ class HealthServicesSensorManager : SensorManager {
passiveListenerCallback
)
}
// Assume the callback is registered to avoid making multiple requests
callBackRegistered = true
}
private fun clearHealthServicesCallBack() {
passiveMonitoringClient?.clearPassiveListenerCallbackAsync()
callBackRegistered = false
}
private fun getActivityIcon(info: UserActivityInfo): String {
return when (info.userActivityState) {
UserActivityState.USER_ACTIVITY_EXERCISE -> {
when (info.exerciseInfo?.exerciseType) {
ExerciseType.ALPINE_SKIING, ExerciseType.SKIING -> "mdi:skiing"
ExerciseType.WEIGHTLIFTING, ExerciseType.BARBELL_SHOULDER_PRESS, ExerciseType.BENCH_PRESS -> "mdi:weight-lifter"
ExerciseType.BIKING, ExerciseType.BIKING_STATIONARY, ExerciseType.MOUNTAIN_BIKING -> "mdi:bike"
ExerciseType.SWIMMING_POOL, ExerciseType.SWIMMING_OPEN_WATER -> "mdi:swim"
ExerciseType.BASEBALL -> "mdi:baseball"
ExerciseType.BASKETBALL -> "mdi:basketball"
ExerciseType.FOOTBALL_AMERICAN -> "mdi:football"
ExerciseType.FOOTBALL_AUSTRALIAN -> "mdi:football-australian"
ExerciseType.SOCCER -> "mdi:soccer"
ExerciseType.SKATING, ExerciseType.INLINE_SKATING -> "mdi:skate"
ExerciseType.ROLLER_SKATING -> "mdi:roller-skate"
ExerciseType.SCUBA_DIVING -> "mdi:diving-scuba"
ExerciseType.SAILING -> "mdi:sail-boat"
ExerciseType.RUGBY -> "mdi:rugby"
ExerciseType.ROWING, ExerciseType.ROWING_MACHINE -> "mdi:rowing"
ExerciseType.RACQUETBALL -> "mdi:racquetball"
ExerciseType.HORSE_RIDING -> "mdi:horse-human"
ExerciseType.ICE_HOCKEY, ExerciseType.ROLLER_HOCKEY -> "mdi:hockey-sticks"
ExerciseType.GYMNASTICS -> "mdi:gymnastics"
ExerciseType.DANCING -> "mdi:dance-ballroom"
ExerciseType.CRICKET -> "mdi:cricket"
ExerciseType.JUMP_ROPE -> "mdi:jump-rope"
ExerciseType.JUMPING_JACK -> "mdi:human-handsdown"
ExerciseType.SNOWBOARDING -> "mdi:snowboard"
ExerciseType.MEDITATION -> "mdi:meditation"
ExerciseType.SURFING -> "mdi:surfing"
ExerciseType.TENNIS -> "mdi:tennis"
ExerciseType.TABLE_TENNIS -> "mdi:table-tennis"
ExerciseType.VOLLEYBALL -> "mdi:volleyball"
ExerciseType.HANDBALL -> "mdi:handball"
ExerciseType.YOGA -> "mdi:yoga"
ExerciseType.WATER_POLO -> "mdi:water-polo"
ExerciseType.STAIR_CLIMBING, ExerciseType.STAIR_CLIMBING_MACHINE -> "mdi:stairs"
ExerciseType.PARA_GLIDING -> "mdi:paragliding"
ExerciseType.GOLF -> "mdi:golf"
else -> "mdi:run"
}
}
UserActivityState.USER_ACTIVITY_PASSIVE -> "mdi:human-handsdown"
UserActivityState.USER_ACTIVITY_ASLEEP -> "mdi:sleep"
else -> userActivityState.statelessIcon
}
}
}

View file

@ -47,7 +47,7 @@ class HeartRateSensorManager : SensorManager, SensorEventListener {
override val name: Int
get() = commonR.string.sensor_name_heart_rate
override fun getAvailableSensors(context: Context): List<SensorManager.BasicSensor> {
override suspend fun getAvailableSensors(context: Context): List<SensorManager.BasicSensor> {
return listOf(heartRate)
}

View file

@ -38,7 +38,7 @@ class OnBodySensorManager : SensorManager, SensorEventListener {
override val name: Int
get() = commonR.string.sensor_name_on_body
override fun getAvailableSensors(context: Context): List<SensorManager.BasicSensor> {
override suspend fun getAvailableSensors(context: Context): List<SensorManager.BasicSensor> {
return listOf(onBodySensor)
}

View file

@ -30,7 +30,7 @@ class TheaterModeSensorManager : SensorManager {
override val name: Int
get() = commonR.string.sensor_name_theater_mode
override fun getAvailableSensors(context: Context): List<SensorManager.BasicSensor> {
override suspend fun getAvailableSensors(context: Context): List<SensorManager.BasicSensor> {
return listOf(theaterMode)
}

View file

@ -32,7 +32,7 @@ class WetModeSensorManager : SensorManager {
override val name: Int
get() = commonR.string.sensor_name_wet_mode
override fun getAvailableSensors(context: Context): List<SensorManager.BasicSensor> {
override suspend fun getAvailableSensors(context: Context): List<SensorManager.BasicSensor> {
return listOf(
wetModeSensor
)

View file

@ -31,3 +31,5 @@ val playPreviewEntityScene2 = Entity("scene.second", "on", mapOf("friendly_name"
val playPreviewEntityScene3 = Entity("scene.third", "on", mapOf("friendly_name" to "Goodbye"), calendar, calendar, mapOf())
val batterySensorManager = BatterySensorManager()
val sensorList = listOf(BatterySensorManager.isChargingState)