Add Wear OS battery sensors (#1890)

* Move database to common module

* Fix lint

* Better kapt error reporting

* Alias common module's resources into app

* Move base sensor receiver & worker to common module

* Add sensor receiver & worker to wear module

* Schedule sensors Worker on app start up

* Export the SensorReceiver in the wear manifest

* Upgrade base and wear sensors Receivers and Workers to Hilt + hoist updateSensors to sensors Receiver base + introduce base location manager + fix minimal sensor stubs

* Re-add important battery sensor info

* Remove unused imports

* Listen for battery updates in wear app

* Listen for screen updates in wear app

* Fix ktlint

* Add sensors to wear OS on home screen

* Stop listening to screens and power save mode

The sensor isn't added yet

* Remove commented code due to absent sensors... for now. Mwahahaha!

* Remove unused import
This commit is contained in:
Timothy Kist 2021-11-27 18:08:26 +00:00 committed by GitHub
parent 196c30f8e8
commit a42cb8ab05
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
82 changed files with 676 additions and 389 deletions

View file

@ -27,12 +27,6 @@ android {
versionCode = System.getenv("VERSION_CODE")?.toIntOrNull() ?: 1
manifestPlaceholders["sentryRelease"] = "$applicationId@$versionName"
javaCompileOptions {
annotationProcessorOptions {
arguments(mapOf("room.incremental" to "true"))
}
}
}
buildFeatures {
@ -119,6 +113,10 @@ android {
isAbortOnError = false
disable("MissingTranslation")
}
kapt {
correctErrorTypes = true
}
}
play {
@ -160,10 +158,6 @@ dependencies {
implementation("androidx.wear:wear-remote-interactions:1.0.0")
implementation("com.google.android.gms:play-services-wearable:17.1.0")
implementation("androidx.room:room-runtime:2.3.0")
implementation("androidx.room:room-ktx:2.3.0")
kapt("androidx.room:room-compiler:2.3.0")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.13.0")
implementation("com.squareup.okhttp3:okhttp:4.9.3")
implementation("com.squareup.picasso:picasso:2.8")
@ -175,7 +169,6 @@ dependencies {
"fullImplementation"("io.sentry:sentry-android:5.4.1")
"fullImplementation"("org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.5.2")
implementation("androidx.work:work-runtime-ktx:2.7.0")
implementation("androidx.biometric:biometric:1.1.0")
implementation("androidx.webkit:webkit:1.4.0")

View file

@ -15,6 +15,7 @@ import com.google.android.gms.location.SleepSegmentEvent
import com.google.android.gms.location.SleepSegmentRequest
import dagger.hilt.android.AndroidEntryPoint
import io.homeassistant.companion.android.R
import io.homeassistant.companion.android.common.sensors.SensorManager
@AndroidEntryPoint
class ActivitySensorManager : BroadcastReceiver(), SensorManager {

View file

@ -8,6 +8,7 @@ import android.os.Build
import android.util.Log
import com.google.android.gms.location.LocationServices
import io.homeassistant.companion.android.R
import io.homeassistant.companion.android.common.sensors.SensorManager
import io.homeassistant.companion.android.database.AppDatabase
import io.homeassistant.companion.android.database.sensor.Setting
import io.homeassistant.companion.android.location.HighAccuracyLocationService

View file

@ -2,7 +2,6 @@ package io.homeassistant.companion.android.sensors
import android.Manifest
import android.app.PendingIntent
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.location.Location
@ -24,9 +23,10 @@ import dagger.hilt.android.AndroidEntryPoint
import io.homeassistant.companion.android.R
import io.homeassistant.companion.android.bluetooth.BluetoothUtils
import io.homeassistant.companion.android.common.data.integration.Entity
import io.homeassistant.companion.android.common.data.integration.IntegrationRepository
import io.homeassistant.companion.android.common.data.integration.UpdateLocation
import io.homeassistant.companion.android.common.data.integration.ZoneAttributes
import io.homeassistant.companion.android.common.sensors.LocationSensorManagerBase
import io.homeassistant.companion.android.common.sensors.SensorManager
import io.homeassistant.companion.android.database.AppDatabase
import io.homeassistant.companion.android.database.sensor.Attribute
import io.homeassistant.companion.android.database.sensor.Setting
@ -36,10 +36,9 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import javax.inject.Inject
@AndroidEntryPoint
class LocationSensorManager : BroadcastReceiver(), SensorManager {
class LocationSensorManager : LocationSensorManagerBase() {
companion object {
private const val SETTING_ACCURACY = "location_minimum_accuracy"
@ -126,9 +125,6 @@ class LocationSensorManager : BroadcastReceiver(), SensorManager {
}
}
@Inject
lateinit var integrationUseCase: IntegrationRepository
private val ioScope: CoroutineScope = CoroutineScope(Dispatchers.IO)
lateinit var latestContext: Context

View file

@ -12,8 +12,8 @@ import android.os.PowerManager
import android.telephony.TelephonyManager
import dagger.hilt.android.HiltAndroidApp
import io.homeassistant.companion.android.common.data.prefs.PrefsRepository
import io.homeassistant.companion.android.common.sensors.LastUpdateManager
import io.homeassistant.companion.android.database.AppDatabase
import io.homeassistant.companion.android.sensors.LastUpdateManager
import io.homeassistant.companion.android.sensors.SensorReceiver
import io.homeassistant.companion.android.widgets.button.ButtonWidget
import io.homeassistant.companion.android.widgets.entity.EntityWidget

View file

@ -75,8 +75,9 @@ abstract class TileExtensions : TileService() {
tile.state = if (state.state == "on" || state.state == "open") Tile.STATE_ACTIVE else Tile.STATE_INACTIVE
} else
tile.state = Tile.STATE_INACTIVE
if (tileData.iconId != null) {
val icon = getTileIcon(tileData.iconId, context)
val iconId = tileData.iconId
if (iconId != null) {
val icon = getTileIcon(iconId, context)
tile.icon = Icon.createWithBitmap(icon)
}
tile.updateTile()

View file

@ -10,6 +10,7 @@ import android.util.Log
import androidx.annotation.RequiresApi
import io.homeassistant.companion.android.BuildConfig
import io.homeassistant.companion.android.R
import io.homeassistant.companion.android.common.sensors.SensorManager
import java.math.RoundingMode
class AppSensorManager : SensorManager {

View file

@ -5,6 +5,7 @@ import android.media.AudioDeviceInfo
import android.media.AudioManager
import android.os.Build
import io.homeassistant.companion.android.R
import io.homeassistant.companion.android.common.sensors.SensorManager
class AudioSensorManager : SensorManager {
companion object {

View file

@ -7,6 +7,7 @@ import io.homeassistant.companion.android.R
import io.homeassistant.companion.android.bluetooth.BluetoothUtils
import io.homeassistant.companion.android.bluetooth.ble.IBeaconTransmitter
import io.homeassistant.companion.android.bluetooth.ble.TransmitterManager
import io.homeassistant.companion.android.common.sensors.SensorManager
import io.homeassistant.companion.android.database.AppDatabase
import io.homeassistant.companion.android.database.sensor.Setting
import java.util.UUID

View file

@ -4,6 +4,7 @@ import android.content.Context
import android.provider.Settings.Global
import android.util.Log
import io.homeassistant.companion.android.R
import io.homeassistant.companion.android.common.sensors.SensorManager
class DNDSensorManager : SensorManager {
companion object {

View file

@ -5,6 +5,7 @@ import android.content.Context
import android.os.Build
import androidx.annotation.RequiresApi
import io.homeassistant.companion.android.R
import io.homeassistant.companion.android.common.sensors.SensorManager
class KeyguardSensorManager : SensorManager {
companion object {

View file

@ -5,6 +5,7 @@ import android.content.Context
import android.os.SystemClock
import android.util.Log
import io.homeassistant.companion.android.R
import io.homeassistant.companion.android.common.sensors.SensorManager
import io.homeassistant.companion.android.database.AppDatabase
import io.homeassistant.companion.android.database.sensor.Setting
import java.text.SimpleDateFormat

View file

@ -9,6 +9,7 @@ import android.hardware.SensorEventListener
import android.hardware.SensorManager.SENSOR_DELAY_NORMAL
import android.util.Log
import io.homeassistant.companion.android.R
import io.homeassistant.companion.android.common.sensors.SensorManager
import kotlin.math.roundToInt
class LightSensorManager : SensorManager, SensorEventListener {

View file

@ -6,6 +6,7 @@ import android.provider.Settings
import android.provider.Settings.Global.getInt
import android.telephony.TelephonyManager
import io.homeassistant.companion.android.R
import io.homeassistant.companion.android.common.sensors.SensorManager
class MobileDataManager : SensorManager {

View file

@ -7,6 +7,7 @@ import android.net.wifi.WifiManager
import android.os.Build
import android.util.Log
import io.homeassistant.companion.android.R
import io.homeassistant.companion.android.common.sensors.SensorManager
import io.homeassistant.companion.android.database.AppDatabase
import io.homeassistant.companion.android.database.sensor.Setting
import okhttp3.Call

View file

@ -4,6 +4,7 @@ import android.app.AlarmManager
import android.content.Context
import android.util.Log
import io.homeassistant.companion.android.R
import io.homeassistant.companion.android.common.sensors.SensorManager
import io.homeassistant.companion.android.database.AppDatabase
import io.homeassistant.companion.android.database.sensor.Setting
import java.text.SimpleDateFormat

View file

@ -11,6 +11,7 @@ import android.service.notification.StatusBarNotification
import android.util.Log
import androidx.core.app.NotificationManagerCompat
import io.homeassistant.companion.android.R
import io.homeassistant.companion.android.common.sensors.SensorManager
class NotificationSensorManager : NotificationListenerService(), SensorManager {
companion object {

View file

@ -7,6 +7,7 @@ import android.telephony.SubscriptionInfo
import android.telephony.SubscriptionManager
import android.telephony.TelephonyManager
import io.homeassistant.companion.android.R
import io.homeassistant.companion.android.common.sensors.SensorManager
class PhoneStateSensorManager : SensorManager {

View file

@ -6,6 +6,7 @@ import android.os.Build
import android.os.PowerManager
import androidx.annotation.RequiresApi
import io.homeassistant.companion.android.R
import io.homeassistant.companion.android.common.sensors.SensorManager
class PowerSensorManager : SensorManager {
companion object {

View file

@ -9,6 +9,7 @@ import android.hardware.SensorEventListener
import android.hardware.SensorManager.SENSOR_DELAY_NORMAL
import android.util.Log
import io.homeassistant.companion.android.R
import io.homeassistant.companion.android.common.sensors.SensorManager
import java.math.RoundingMode
class PressureSensorManager : SensorManager, SensorEventListener {

View file

@ -9,6 +9,7 @@ import android.hardware.SensorEventListener
import android.hardware.SensorManager.SENSOR_DELAY_NORMAL
import android.util.Log
import io.homeassistant.companion.android.R
import io.homeassistant.companion.android.common.sensors.SensorManager
import kotlin.math.roundToInt
class ProximitySensorManager : SensorManager, SensorEventListener {

View file

@ -23,6 +23,7 @@ import dagger.hilt.android.AndroidEntryPoint
import io.homeassistant.companion.android.R
import io.homeassistant.companion.android.bluetooth.BluetoothUtils
import io.homeassistant.companion.android.common.data.integration.IntegrationRepository
import io.homeassistant.companion.android.common.sensors.SensorManager
import io.homeassistant.companion.android.database.AppDatabase
import io.homeassistant.companion.android.database.sensor.Sensor
import io.homeassistant.companion.android.database.sensor.SensorDao

View file

@ -3,28 +3,27 @@ package io.homeassistant.companion.android.sensors
import android.annotation.SuppressLint
import android.app.NotificationManager
import android.bluetooth.BluetoothAdapter
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.res.Configuration
import android.media.AudioManager
import android.os.PowerManager
import android.util.Log
import dagger.hilt.android.AndroidEntryPoint
import io.homeassistant.companion.android.BuildConfig
import io.homeassistant.companion.android.common.data.integration.IntegrationRepository
import io.homeassistant.companion.android.common.data.integration.SensorRegistration
import io.homeassistant.companion.android.database.AppDatabase
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import java.util.Locale
import javax.inject.Inject
import io.homeassistant.companion.android.common.sensors.BatterySensorManager
import io.homeassistant.companion.android.common.sensors.LastUpdateManager
import io.homeassistant.companion.android.common.sensors.SensorManager
import io.homeassistant.companion.android.common.sensors.SensorReceiverBase
@AndroidEntryPoint
class SensorReceiver : BroadcastReceiver() {
class SensorReceiver : SensorReceiverBase() {
override val tag: String
get() = TAG
override val currentAppVersion: String
get() = BuildConfig.VERSION_NAME
override val managers: List<SensorManager>
get() = MANAGERS
companion object {
const val TAG = "SensorReceiver"
@ -59,21 +58,9 @@ class SensorReceiver : BroadcastReceiver() {
"io.homeassistant.companion.android.background.REQUEST_SENSORS_UPDATE"
}
private val ioScope: CoroutineScope = CoroutineScope(Dispatchers.IO + Job())
@Inject
lateinit var integrationUseCase: IntegrationRepository
private val chargingActions = listOf(
Intent.ACTION_BATTERY_LOW,
Intent.ACTION_BATTERY_OKAY,
Intent.ACTION_POWER_CONNECTED,
Intent.ACTION_POWER_DISCONNECTED
)
// Suppress Lint because we only register for the receiver if the android version matches the intent
@SuppressLint("InlinedApi")
private val skippableActions = mapOf(
override val skippableActions = mapOf(
"android.app.action.NEXT_ALARM_CLOCK_CHANGED" to NextAlarmManager.nextAlarm.id,
"android.bluetooth.device.action.ACL_CONNECTED" to BluetoothSensorManager.bluetoothConnection.id,
"android.bluetooth.device.action.ACL_DISCONNECTED" to BluetoothSensorManager.bluetoothConnection.id,
@ -87,142 +74,4 @@ class SensorReceiver : BroadcastReceiver() {
AudioManager.ACTION_SPEAKERPHONE_STATE_CHANGED to AudioSensorManager.speakerphoneState.id,
AudioManager.RINGER_MODE_CHANGED_ACTION to AudioSensorManager.audioSensor.id
)
override fun onReceive(context: Context, intent: Intent) {
if (skippableActions.containsKey(intent.action)) {
val sensor = skippableActions[intent.action]
if (!isSensorEnabled(context, sensor!!)) {
Log.d(
TAG,
String.format
("Sensor %s corresponding to received event %s is disabled, skipping sensors update", sensor, intent.action)
)
return
}
}
if (isSensorEnabled(context, LastUpdateManager.lastUpdate.id)) {
LastUpdateManager().sendLastUpdate(context, intent.action)
val sensorDao = AppDatabase.getInstance(context).sensorDao()
val allSettings = sensorDao.getSettings(LastUpdateManager.lastUpdate.id)
for (setting in allSettings) {
if (setting.value != "" && intent.action == setting.value) {
val eventData = intent.extras?.keySet()?.map { it.toString() to intent.extras?.get(it).toString() }?.toMap()?.plus("intent" to intent.action.toString())
?: mapOf("intent" to intent.action.toString())
Log.d(TAG, "Event data: $eventData")
ioScope.launch {
try {
integrationUseCase.fireEvent(
"android.intent_received",
eventData as Map<String, Any>
)
Log.d(TAG, "Event successfully sent to Home Assistant")
} catch (e: Exception) {
Log.e(TAG, "Unable to send event data to Home Assistant", e)
}
}
}
}
}
ioScope.launch {
updateSensors(context, integrationUseCase)
if (chargingActions.contains(intent.action)) {
// Add a 5 second delay to perform another update so charging state updates completely.
// This is necessary as the system needs a few seconds to verify the charger.
delay(5000L)
updateSensors(context, integrationUseCase)
}
}
}
private fun isSensorEnabled(context: Context, id: String): Boolean {
return AppDatabase.getInstance(context).sensorDao().get(id)?.enabled == true
}
suspend fun updateSensors(
context: Context,
integrationUseCase: IntegrationRepository
) {
val sensorDao = AppDatabase.getInstance(context).sensorDao()
val enabledRegistrations = mutableListOf<SensorRegistration<Any>>()
val checkDeviceRegistration = integrationUseCase.getRegistration()
if (checkDeviceRegistration.appVersion == null) {
Log.w(TAG, "Device not registered, skipping sensor update/registration")
return
}
val currentAppVersion = BuildConfig.VERSION_NAME
val currentHaVersion = integrationUseCase.getHomeAssistantVersion()
MANAGERS.forEach { manager ->
// Since we don't have this manager injected it doesn't fulfil it's injects, manually
// inject for now I guess?
if (manager is LocationSensorManager)
manager.integrationUseCase = integrationUseCase
try {
manager.requestSensorUpdate(context)
} catch (e: Exception) {
Log.e(TAG, "Issue requesting updates for ${context.getString(manager.name)}", e)
}
manager.getAvailableSensors(context).forEach { basicSensor ->
val fullSensor = sensorDao.getFull(basicSensor.id)
val sensor = fullSensor?.sensor
val sensorCoreRegistration = sensor?.coreRegistration
val sensorAppRegistration = sensor?.appRegistration
// Always register enabled sensors in case of available entity updates
// when app or core version change is detected every 4 hours
if (sensor?.enabled == true && sensor.type.isNotBlank() && sensor.icon.isNotBlank() &&
(currentAppVersion != sensorAppRegistration || currentHaVersion != sensorCoreRegistration || !sensor.registered)
) {
val reg = fullSensor.toSensorRegistration()
val config = Configuration(context.resources.configuration)
config.setLocale(Locale("en"))
reg.name = context.createConfigurationContext(config).resources.getString(basicSensor.name)
try {
integrationUseCase.registerSensor(reg)
sensor.registered = true
sensor.coreRegistration = currentHaVersion
sensor.appRegistration = currentAppVersion
sensorDao.update(sensor)
} catch (e: Exception) {
Log.e(TAG, "Issue registering sensor: ${reg.uniqueId}", e)
}
}
if (sensor?.enabled == true && sensor.registered && sensor.state != sensor.lastSentState) {
enabledRegistrations.add(fullSensor.toSensorRegistration())
}
}
}
if (enabledRegistrations.isNotEmpty()) {
var success = false
try {
success = integrationUseCase.updateSensors(enabledRegistrations.toTypedArray())
enabledRegistrations.forEach {
sensorDao.updateLastSendState(it.uniqueId, it.state.toString())
}
} catch (e: Exception) {
Log.e(TAG, "Exception while updating sensors.", e)
}
// We failed to update a sensor, we should re register next time
if (!success) {
enabledRegistrations.forEach {
val sensor = sensorDao.get(it.uniqueId)
if (sensor != null) {
sensor.registered = false
sensor.lastSentState = ""
sensorDao.update(sensor)
}
}
}
} else Log.d(TAG, "Nothing to update")
}
}

View file

@ -1,16 +1,8 @@
package io.homeassistant.companion.android.sensors
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Context
import android.content.Context.NOTIFICATION_SERVICE
import android.os.Build
import android.util.Log
import androidx.core.app.NotificationCompat
import androidx.work.Constraints
import androidx.work.CoroutineWorker
import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.ForegroundInfo
import androidx.work.NetworkType
import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkManager
@ -19,22 +11,17 @@ import dagger.hilt.EntryPoint
import dagger.hilt.InstallIn
import dagger.hilt.android.EntryPointAccessors
import dagger.hilt.components.SingletonComponent
import io.homeassistant.companion.android.R
import io.homeassistant.companion.android.common.data.integration.IntegrationRepository
import io.homeassistant.companion.android.database.AppDatabase
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import io.homeassistant.companion.android.common.sensors.SensorReceiverBase
import io.homeassistant.companion.android.common.sensors.SensorWorkerBase
import java.util.concurrent.TimeUnit
class SensorWorker(
private val appContext: Context,
appContext: Context,
workerParams: WorkerParameters
) : CoroutineWorker(appContext, workerParams) {
companion object {
private const val TAG = "SensorWorker"
const val channelId = "Sensor Worker"
const val NOTIFICATION_ID = 42
) : SensorWorkerBase(appContext, workerParams) {
companion object {
fun start(context: Context) {
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED).build()
@ -44,51 +31,28 @@ class SensorWorker(
.setConstraints(constraints)
.build()
WorkManager.getInstance(context).enqueueUniquePeriodicWork(TAG, ExistingPeriodicWorkPolicy.REPLACE, sensorWorker)
WorkManager.getInstance(context)
.enqueueUniquePeriodicWork(TAG, ExistingPeriodicWorkPolicy.REPLACE, sensorWorker)
}
}
@EntryPoint
@InstallIn(SingletonComponent::class)
interface SensorWorkerEntryPoint {
fun integrationRepository(): IntegrationRepository
}
private val notificationManager = appContext.getSystemService(NOTIFICATION_SERVICE) as NotificationManager
override suspend fun doWork(): Result = withContext(Dispatchers.IO) {
val sensorDao = AppDatabase.getInstance(applicationContext).sensorDao()
val enabledSensorCount = sensorDao.getEnabledCount() ?: 0
if (enabledSensorCount > 0) {
Log.d(TAG, "Updating all Sensors.")
createNotificationChannel()
val notification = NotificationCompat.Builder(applicationContext, channelId)
.setSmallIcon(R.drawable.ic_stat_ic_notification)
.setContentTitle(appContext.getString(R.string.updating_sensors))
.setPriority(NotificationCompat.PRIORITY_LOW)
.build()
val foregroundInfo = ForegroundInfo(NOTIFICATION_ID, notification)
setForeground(foregroundInfo)
val lastUpdateSensor = sensorDao.get(LastUpdateManager.lastUpdate.id)
if (lastUpdateSensor?.enabled == true) {
LastUpdateManager().sendLastUpdate(appContext, TAG)
}
val integrationUseCase = EntryPointAccessors.fromApplication(appContext, SensorWorkerEntryPoint::class.java).integrationRepository()
SensorReceiver().updateSensors(appContext, integrationUseCase)
override val integrationUseCase: IntegrationRepository
get() {
return EntryPointAccessors.fromApplication(
appContext,
SensorWorkerEntryPoint::class.java
)
.integrationRepository()
}
Result.success()
}
private fun createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
var notificationChannel =
notificationManager.getNotificationChannel(channelId)
if (notificationChannel == null) {
notificationChannel = NotificationChannel(
channelId, TAG, NotificationManager.IMPORTANCE_LOW
)
notificationManager.createNotificationChannel(notificationChannel)
}
override val sensorReceiver: SensorReceiverBase
get() {
return SensorReceiver()
}
}
}

View file

@ -11,6 +11,7 @@ import android.hardware.SensorManager.SENSOR_DELAY_NORMAL
import android.os.Build
import android.util.Log
import io.homeassistant.companion.android.R
import io.homeassistant.companion.android.common.sensors.SensorManager
import kotlin.math.roundToInt
class StepsSensorManager : SensorManager, SensorEventListener {

View file

@ -5,6 +5,7 @@ import android.os.Environment
import android.os.StatFs
import android.util.Log
import io.homeassistant.companion.android.R
import io.homeassistant.companion.android.common.sensors.SensorManager
import java.io.File
import kotlin.math.roundToInt

View file

@ -2,6 +2,7 @@ package io.homeassistant.companion.android.sensors
import android.content.Context
import io.homeassistant.companion.android.R
import io.homeassistant.companion.android.common.sensors.SensorManager
import java.util.Date
import java.util.Locale
import java.util.TimeZone

View file

@ -6,6 +6,7 @@ import android.net.NetworkCapabilities
import android.net.TrafficStats
import android.util.Log
import io.homeassistant.companion.android.R
import io.homeassistant.companion.android.common.sensors.SensorManager
import java.math.RoundingMode
import kotlin.math.absoluteValue

View file

@ -128,9 +128,10 @@ class ManageTilesFragment constructor(
findPreference<ListPreference>("tile_entity")?.value = item.entityId
tileEntity = item.entityId
findPreference<Preference>("tile_icon")?.let {
it.summary = item.iconId.toString()
if (item.iconId != null) {
val iconDrawable = iconPack.getIcon(item.iconId)?.drawable
val iconId = item.iconId
it.summary = iconId.toString()
if (iconId != null) {
val iconDrawable = iconPack.getIcon(iconId)?.drawable
if (iconDrawable != null) {
val icon = DrawableCompat.wrap(iconDrawable)
icon.setColorFilter(

View file

@ -98,12 +98,13 @@ class EntityWidgetConfigureActivity : BaseActivity() {
}
}
if (!staticWidget.attributeIds.isNullOrEmpty()) {
val attributeIds = staticWidget.attributeIds
if (!attributeIds.isNullOrEmpty()) {
binding.appendAttributeValueCheckbox.isChecked = true
appendAttributes = true
for (item in staticWidget.attributeIds.split(','))
for (item in attributeIds.split(','))
selectedAttributeIds.add(item)
binding.widgetTextConfigAttribute.setText(staticWidget.attributeIds.replace(",", ", "))
binding.widgetTextConfigAttribute.setText(attributeIds.replace(",", ", "))
binding.attributeValueLinearLayout.visibility = VISIBLE
binding.attributeSeparator.setText(staticWidget.attributeSeparator)
}

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<item name="sensor" type="string">@string/sensor</item>
<item name="sensors" type="string">@string/sensors</item>
<item name="ic_stat_ic_notification" type="drawable">@drawable/ic_stat_ic_notification</item>
<item name="ic_stat_ic_notification_blue" type="drawable">@drawable/ic_stat_ic_notification_blue</item>
</resources>

View file

@ -26,15 +26,10 @@
<string name="basic_sensor_name_app_rx_gb">App Rx GB</string>
<string name="basic_sensor_name_app_standby">App Standby Bucket</string>
<string name="basic_sensor_name_app_tx_gb">App Tx GB</string>
<string name="basic_sensor_name_battery_health">Battery Health</string>
<string name="basic_sensor_name_battery_level">Battery Level</string>
<string name="basic_sensor_name_battery_state">Battery State</string>
<string name="basic_sensor_name_bluetooth">Bluetooth Connection</string>
<string name="basic_sensor_name_bluetooth_ble_emitter">Ble Transmitter</string>
<string name="basic_sensor_name_bluetooth_state">Bluetooth State</string>
<string name="basic_sensor_name_call_number">Call Number</string>
<string name="basic_sensor_name_charger_type">Charger Type</string>
<string name="basic_sensor_name_charging">Is Charging</string>
<string name="basic_sensor_name_current_time_zone">Current Time Zone</string>
<string name="basic_sensor_name_current_version">Current Version</string>
<string name="basic_sensor_name_device_locked">Device Locked</string>
@ -49,7 +44,6 @@
<string name="basic_sensor_name_last_notification">Last Notification</string>
<string name="basic_sensor_name_last_reboot">Last Reboot</string>
<string name="basic_sensor_name_last_removed_notification">Last Removed Notification</string>
<string name="basic_sensor_name_last_update">Last Update Trigger</string>
<string name="basic_sensor_name_location_accurate">Single Accurate Location</string>
<string name="basic_sensor_name_location_background">Background Location</string>
<string name="basic_sensor_name_location_zone">Location Zone</string>
@ -101,8 +95,6 @@ to your home internet.</string>
<string name="crash_reporting">Crash Reporting</string>
<string name="crash_reporting_summary">Help the developers fix bugs and crashes by leaving this enabled. If the application crashes this will automatically generate and send a report. If you notice a crash also create an issue on Github!</string>
<string name="create_template">Create Template</string>
<string name="database_event_failure">Unable to send migration failure event to Home Assistant</string>
<string name="database_migration_failed">Database migration failed, all sensor and widget data has been reset. Please re-enable your sensors and recreate your widgets.</string>
<string name="delete_all_notifications">Delete all notifications from history</string>
<string name="delete_this_notification">Delete this notification from history</string>
<string name="delete_widget">Delete Widget</string>
@ -131,7 +123,7 @@ to your home internet.</string>
<string name="enable_all_sensors">Enable All Sensors</string>
<string name="enable_all_sensors_summary">All required permissions will be requested upon enabling</string>
<string name="enable_location_tracking">Enable Location Tracking</string>
<string name="enable_location_tracking_description">Enabling this sensor will allow the Home Assistant application to track you location and report it back to your Home Assistant server. Ensure that you enable background access to location, otherwise we cannot enable location tracking. Your location data is sent directly to your Home Assistant server and is never sent to a 3rd party.</string>
<string name="enable_location_tracking_description">Enabling this sensor will allow the Home Assistant application to track you location and report it back to your instance of Home Assistant. Ensure that you enable background access to location, otherwise we cannot enable location tracking. Your location data is sent directly to your Home Assistant instance and is never sent to a 3rd party.</string>
<string name="enable_location_tracking_prompt">This app collects location data to enable Location Tracking, Wifi Connection Status, and URL decision making even when the app is closed or not in use.</string>
<string name="enable_remaining_sensors">Enable %1$d Sensors</string>
<string name="enabled_summary">When enabled values will be sent to Home Assistant</string>
@ -142,7 +134,7 @@ to your home internet.</string>
<string name="error_onboarding_connection_failed">Unable to connect to Home Assistant.</string>
<string name="error_ssl">Unable to communicate with Home Assistant because of a SSL error. Please ensure your certificate is valid.</string>
<string name="error_with_registration">Please check to ensure you have the mobile_app
integration enabled on your home assistant server.</string>
integration enabled on your home assistant instance.</string>
<string name="event_error">Failed to notify HA of picked option.</string>
<string name="exit">Exit</string>
<string name="failed_scan">Scanning for Home Assistant failed.</string>
@ -160,7 +152,7 @@ integration enabled on your home assistant server.</string>
<string name="high_accuracy_mode_notification_title">High accuracy (GPS) mode enabled</string>
<string name="history">History</string>
<string name="home_assistant_not_discover">Unable to find your
Home Assistant server</string>
Home Assistant instance</string>
<string name="icon">Icon</string>
<string name="input_url">Home Assistant URL</string>
<string name="input_url_hint">https://example.duckdns.org:8123</string>
@ -283,9 +275,8 @@ for Home Assistant</string>
<string name="security_vulnerably_understand">I Understand</string>
<string name="security_vulnerably_view">View Bulletin</string>
<string name="select_entity_to_display">Select Entity to display</string>
<string name="select_instance">Select the server you would
<string name="select_instance">Select the instance you would
like to connect to:</string>
<string name="sensor">Sensor</string>
<string name="sensor_description">Description</string>
<string name="sensor_description_active_notification_count">Total count of active notifications that are visible to the user including silent, persistent and the Sensor Worker notifications.</string>
<string name="sensor_description_app_importance">If the app is in the foreground, background or any other state it can be.</string>
@ -296,15 +287,10 @@ like to connect to:</string>
<string name="sensor_description_app_tx_gb">App Tx GB since last device reboot</string>
<string name="sensor_description_audio_mode">The state of the devices audio mode</string>
<string name="sensor_description_audio_sensor">The state of the devices ringer mode</string>
<string name="sensor_description_battery_health">The health of the battery</string>
<string name="sensor_description_battery_level">The current battery level of the device</string>
<string name="sensor_description_battery_state">The current charging state of the battery</string>
<string name="sensor_description_bluetooth_ble_emitter">Send BLE iBeacon with configured interval, used to track presence around house, e.g. together with roomassistant, esp32-mqtt-room or espresence projects.\nBy default this sensor is not turned on with the Enable all toggle, to avoid battery drain, there is a setting to enable this (\"Include when enabling all sensors\").\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.\nSettings allow for specifying:\n- \"UUID\" (standard UUID format), \"Major\" and \"Minor\" (should be 0 - 65535), to tailor identifiers and groups\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\nNote:\nAdditionally a separate setting exists (\"Enable Transmitter\") to stop or start transmitting.</string>
<string name="sensor_description_bluetooth_connection">Information about currently connected bluetooth devices</string>
<string name="sensor_description_bluetooth_state">Whether or not bluetooth is enabled on the device</string>
<string name="sensor_description_call_number">The cell number of the incoming or outgoing call</string>
<string name="sensor_description_charger_type">The type of charger plugged into the device currently</string>
<string name="sensor_description_charging">Whether or not the device is actively charging</string>
<string name="sensor_description_current_time_zone">The current time zone the device is in</string>
<string name="sensor_description_current_version">The current installed application version</string>
<string name="sensor_description_detected_activity">The current activity type as computed by Googles Activity Recognition API</string>
@ -322,7 +308,6 @@ like to connect to:</string>
<string name="sensor_description_last_notification">The details of the last notification.</string>
<string name="sensor_description_last_reboot">The date and time of the devices last reboot. The setting below will allow you to adjust the deadband in milliseconds, if you still find the value to jump incorrectly. The default value is 60000 (1 minute).</string>
<string name="sensor_description_last_removed_notification">The details of the last removed notification. This can be any notification either cleared by the user or removed by an application.</string>
<string name="sensor_description_last_update">The intent for the last update that was sent, periodic updates will show as \"SensorWorker\". Enabling the \"Add New Intent\" toggle will create 1 setting to allow you to register for a broadcast intent. The toggle will switch back to off once a new setting is created so you will need to turn it back on to save more intents. You can also clear out the setting value to remove the setting in the next update. You must restart the application after making changes to these settings to take effect.</string>
<string name="sensor_description_light_sensor">The current level of illuminance</string>
<string name="sensor_description_location_accurate">Allow Home Assistant to send a notification to request an accurate location along with the application periodically requesting an accurate location. The Minimum Accuracy setting will allow you to decide how accurate the device location (in meters) has to be in order to send to Home Assistant. The Minimum time between updates (in milliseconds) keeps the device from sending the accurate location too often. The Include in sensor update setting will make a location request with each sensor update.</string>
<string name="sensor_description_location_background">Update your location behind the scenes, periodically. The Minimum Accuracy setting will allow you to decide how accurate the device location (in meters) has to be in order to send to Home Assistant.</string>
@ -335,7 +320,6 @@ like to connect to:</string>
<string name="sensor_description_mobile_tx_gb">Total Tx GB on cellular data since last device reboot</string>
<string name="sensor_description_music_active">Whether or not music is actively playing on the device</string>
<string name="sensor_description_next_alarm">The date and time of the next scheduled alarm, any app or manufacturer can override the default behavior. The package attribute will tell you which app set the next scheduled alarm. The setting below will create an Allow List so you can specify what packages you want the alarm event from. This will ignore alarm events for packages not selected and the state will not update until the next scheduled alarm matches one of the selected packages.</string>
<string name="sensor_description_none">No description</string>
<string name="sensor_description_phone_state">Whether or not the phone is ringing or in a call, no other caller information is stored</string>
<string name="sensor_description_power_save">Whether or not the device is in Power Save mode</string>
<string name="sensor_description_pressure_sensor">The current ambient air pressure reading</string>
@ -368,7 +352,6 @@ like to connect to:</string>
<string name="sensor_name_app_sensor">App Sensors</string>
<string name="sensor_name_audio">Audio Sensors</string>
<string name="sensor_name_audio_mode">Audio Mode</string>
<string name="sensor_name_battery">Battery Sensors</string>
<string name="sensor_name_bluetooth">Bluetooth Sensors</string>
<string name="sensor_name_dnd">Do Not Disturb Sensor</string>
<string name="sensor_name_geolocation">Geolocation Sensors</string>
@ -376,7 +359,6 @@ like to connect to:</string>
<string name="sensor_name_keyguard">Keyguard Sensors</string>
<string name="sensor_name_last_notification">Notification Sensors</string>
<string name="sensor_name_last_reboot">Last Reboot Sensor</string>
<string name="sensor_name_last_update">Last Update Sensor</string>
<string name="sensor_name_light">Light Sensor</string>
<string name="sensor_name_location">Location Sensors</string>
<string name="sensor_name_mic_muted">Mic Muted</string>
@ -401,7 +383,6 @@ like to connect to:</string>
<string name="sensor_settings">Sensor Settings</string>
<string name="sensor_summary">Use this to manage what sensors are enabled/disabled.</string>
<string name="sensor_title">Manage Sensors</string>
<string name="sensors">Sensors</string>
<string name="sensors_with_settings">The following sensors offer custom settings: %1$s</string>
<string name="session_timeout_title">Session TimeOut (in seconds)</string>
<string name="set_lock_message">No biometric sensor or screenlock credential available</string>
@ -470,7 +451,6 @@ like to connect to:</string>
<string name="unable_to_register">Unable to Register Application</string>
<string name="unique_id">Unique Id</string>
<string name="update_widget">Update Widget</string>
<string name="updating_sensors">Updating Sensors</string>
<string name="unknown_address">Unknown address</string>
<string name="url_invalid">Url Invalid</string>
<string name="url_parse_error">Unable to parse your Home Assistant URL. It should look like https://example.com</string>
@ -517,8 +497,6 @@ like to connect to:</string>
<string name="zone_event_failure">Unable to send zone event to Home Assistant</string>
<string name="what_is_this">What is this?</string>
<string name="what_is_this_crash">Unable to load Home Assistant home page, do you have a browser installed?</string>
<string name="basic_sensor_name_battery_temperature">Battery Temperature</string>
<string name="sensor_description_battery_temperature">The current battery temperature</string>
<string name="shortcut_pinned_desc">Pinned Shortcut Description</string>
<string name="shortcut_pinned_label">Pinned Shortcut Label</string>
<string name="shortcut_pinned_id">Pinned Shortcut ID</string>

View file

@ -4,6 +4,7 @@ import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import io.homeassistant.companion.android.R
import io.homeassistant.companion.android.common.sensors.SensorManager
class ActivitySensorManager : BroadcastReceiver(), SensorManager {

View file

@ -2,6 +2,7 @@ package io.homeassistant.companion.android.sensors
import android.content.Context
import io.homeassistant.companion.android.R
import io.homeassistant.companion.android.common.sensors.SensorManager
class GeocodeSensorManager : SensorManager {

View file

@ -1,12 +1,12 @@
package io.homeassistant.companion.android.sensors
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import io.homeassistant.companion.android.R
import io.homeassistant.companion.android.common.data.integration.IntegrationRepository
import io.homeassistant.companion.android.common.sensors.LocationSensorManagerBase
import io.homeassistant.companion.android.common.sensors.SensorManager
class LocationSensorManager : BroadcastReceiver(), SensorManager {
class LocationSensorManager : LocationSensorManagerBase(), SensorManager {
companion object {
const val MINIMUM_ACCURACY = 200
@ -41,8 +41,6 @@ class LocationSensorManager : BroadcastReceiver(), SensorManager {
internal const val TAG = "LocBroadcastReceiver"
}
lateinit var integrationUseCase: IntegrationRepository
override fun onReceive(context: Context, intent: Intent) {
// Noop
}

View file

@ -20,6 +20,30 @@ android {
buildConfigField("String", "PUSH_URL", "\"$homeAssistantAndroidPushUrl\"")
buildConfigField("String", "RATE_LIMIT_URL", "\"$homeAssistantAndroidRateLimitUrl\"")
buildConfigField("String", "VERSION_NAME", "\"$versionName-$versionCode\"")
javaCompileOptions {
annotationProcessorOptions {
arguments(mapOf("room.incremental" to "true"))
}
}
}
kotlinOptions {
jvmTarget = "1.8"
}
compileOptions {
sourceCompatibility(JavaVersion.VERSION_11)
targetCompatibility(JavaVersion.VERSION_11)
}
lint {
isAbortOnError = false
disable("MissingTranslation")
}
kapt {
correctErrorTypes = true
}
}
@ -31,6 +55,12 @@ dependencies {
implementation("com.google.dagger:hilt-android:2.40.1")
kapt("com.google.dagger:hilt-android-compiler:2.40.1")
api("androidx.room:room-runtime:2.3.0")
api("androidx.room:room-ktx:2.3.0")
kapt("androidx.room:room-compiler:2.3.0")
api("androidx.work:work-runtime-ktx:2.7.0")
implementation("com.squareup.retrofit2:retrofit:2.9.0")
implementation("com.squareup.retrofit2:converter-jackson:2.9.0")
implementation("com.squareup.okhttp3:okhttp:4.9.3")

View file

@ -1,10 +1,10 @@
package io.homeassistant.companion.android.sensors
package io.homeassistant.companion.android.common.sensors
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.BatteryManager
import io.homeassistant.companion.android.R
import io.homeassistant.companion.android.common.R
class BatterySensorManager : SensorManager {
@ -65,13 +65,22 @@ class BatterySensorManager : SensorManager {
override fun docsLink(): String {
return "https://companion.home-assistant.io/docs/core/sensors#battery-sensors"
}
override val enabledByDefault: Boolean
get() = true
override val name: Int
get() = R.string.sensor_name_battery
override fun getAvailableSensors(context: Context): List<SensorManager.BasicSensor> {
return listOf(batteryLevel, batteryState, isChargingState, chargerTypeState, batteryHealthState, batteryTemperature)
return listOf(
batteryLevel,
batteryState,
isChargingState,
chargerTypeState,
batteryHealthState,
batteryTemperature
)
}
override fun requiredPermissions(sensorId: String): Array<String> {

View file

@ -1,83 +1,83 @@
package io.homeassistant.companion.android.sensors
import android.content.Context
import android.util.Log
import io.homeassistant.companion.android.R
import io.homeassistant.companion.android.database.AppDatabase
import io.homeassistant.companion.android.database.sensor.Setting
class LastUpdateManager : SensorManager {
companion object {
private const val TAG = "LastUpdate"
private const val SETTING_ADD_NEW_INTENT = "lastupdate_add_new_intent"
val lastUpdate = SensorManager.BasicSensor(
"last_update",
"sensor",
R.string.basic_sensor_name_last_update,
R.string.sensor_description_last_update,
entityCategory = SensorManager.ENTITY_CATEGORY_DIAGNOSTIC
)
}
override fun docsLink(): String {
return "https://companion.home-assistant.io/docs/core/sensors#last-update-trigger-sensor"
}
override val enabledByDefault: Boolean
get() = false
override val name: Int
get() = R.string.sensor_name_last_update
override fun getAvailableSensors(context: Context): List<SensorManager.BasicSensor> {
return listOf(lastUpdate)
}
override fun requiredPermissions(sensorId: String): Array<String> {
return emptyArray()
}
override fun requestSensorUpdate(
context: Context
) {
// No op
}
fun sendLastUpdate(context: Context, intentAction: String?) {
if (!isEnabled(context, lastUpdate.id))
return
if (intentAction.isNullOrEmpty())
return
val icon = "mdi:update"
Log.d(TAG, "Last update is $intentAction")
onSensorUpdated(
context,
lastUpdate,
intentAction,
icon,
mapOf()
)
val sensorDao = AppDatabase.getInstance(context).sensorDao()
val allSettings = sensorDao.getSettings(lastUpdate.id)
val intentSettingName = "lastupdate_intent_var1:${allSettings.size}:"
val addNewIntent = allSettings.firstOrNull { it.name == SETTING_ADD_NEW_INTENT }?.value ?: "false"
val intentSetting = allSettings.firstOrNull { it.name == intentSettingName }?.value ?: ""
if (addNewIntent == "true") {
if (intentSetting == "") {
sensorDao.add(Setting(lastUpdate.id, SETTING_ADD_NEW_INTENT, "false", "toggle"))
sensorDao.add(Setting(lastUpdate.id, intentSettingName, intentAction, "string"))
}
} else {
sensorDao.add(Setting(lastUpdate.id, SETTING_ADD_NEW_INTENT, "false", "toggle"))
}
for (setting in allSettings) {
if (setting.value == "")
sensorDao.removeSetting(lastUpdate.id, setting.name)
}
}
}
package io.homeassistant.companion.android.common.sensors
import android.content.Context
import android.util.Log
import io.homeassistant.companion.android.common.R
import io.homeassistant.companion.android.database.AppDatabase
import io.homeassistant.companion.android.database.sensor.Setting
class LastUpdateManager : SensorManager {
companion object {
private const val TAG = "LastUpdate"
private const val SETTING_ADD_NEW_INTENT = "lastupdate_add_new_intent"
val lastUpdate = SensorManager.BasicSensor(
"last_update",
"sensor",
R.string.basic_sensor_name_last_update,
R.string.sensor_description_last_update,
entityCategory = SensorManager.ENTITY_CATEGORY_DIAGNOSTIC
)
}
override fun docsLink(): String {
return "https://companion.home-assistant.io/docs/core/sensors#last-update-trigger-sensor"
}
override val enabledByDefault: Boolean
get() = false
override val name: Int
get() = R.string.sensor_name_last_update
override fun getAvailableSensors(context: Context): List<SensorManager.BasicSensor> {
return listOf(lastUpdate)
}
override fun requiredPermissions(sensorId: String): Array<String> {
return emptyArray()
}
override fun requestSensorUpdate(
context: Context
) {
// No op
}
fun sendLastUpdate(context: Context, intentAction: String?) {
if (!isEnabled(context, lastUpdate.id))
return
if (intentAction.isNullOrEmpty())
return
val icon = "mdi:update"
Log.d(TAG, "Last update is $intentAction")
onSensorUpdated(
context,
lastUpdate,
intentAction,
icon,
mapOf()
)
val sensorDao = AppDatabase.getInstance(context).sensorDao()
val allSettings = sensorDao.getSettings(lastUpdate.id)
val intentSettingName = "lastupdate_intent_var1:${allSettings.size}:"
val addNewIntent = allSettings.firstOrNull { it.name == SETTING_ADD_NEW_INTENT }?.value ?: "false"
val intentSetting = allSettings.firstOrNull { it.name == intentSettingName }?.value ?: ""
if (addNewIntent == "true") {
if (intentSetting == "") {
sensorDao.add(Setting(lastUpdate.id, SETTING_ADD_NEW_INTENT, "false", "toggle"))
sensorDao.add(Setting(lastUpdate.id, intentSettingName, intentAction, "string"))
}
} else {
sensorDao.add(Setting(lastUpdate.id, SETTING_ADD_NEW_INTENT, "false", "toggle"))
}
for (setting in allSettings) {
if (setting.value == "")
sensorDao.removeSetting(lastUpdate.id, setting.name)
}
}
}

View file

@ -0,0 +1,10 @@
package io.homeassistant.companion.android.common.sensors
import android.content.BroadcastReceiver
import io.homeassistant.companion.android.common.data.integration.IntegrationRepository
import javax.inject.Inject
abstract class LocationSensorManagerBase : BroadcastReceiver(), SensorManager {
@Inject
lateinit var integrationUseCase: IntegrationRepository
}

View file

@ -1,10 +1,10 @@
package io.homeassistant.companion.android.sensors
package io.homeassistant.companion.android.common.sensors
import android.content.Context
import android.content.pm.PackageManager
import android.os.Process.myPid
import android.os.Process.myUid
import io.homeassistant.companion.android.R
import io.homeassistant.companion.android.common.R
import io.homeassistant.companion.android.database.AppDatabase
import io.homeassistant.companion.android.database.sensor.Attribute
import io.homeassistant.companion.android.database.sensor.Sensor

View file

@ -0,0 +1,181 @@
package io.homeassistant.companion.android.common.sensors
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.res.Configuration
import android.util.Log
import io.homeassistant.companion.android.common.data.integration.IntegrationRepository
import io.homeassistant.companion.android.common.data.integration.SensorRegistration
import io.homeassistant.companion.android.database.AppDatabase
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import java.util.Locale
import javax.inject.Inject
abstract class SensorReceiverBase : BroadcastReceiver() {
private val ioScope: CoroutineScope = CoroutineScope(Dispatchers.IO + Job())
protected abstract val tag: String
protected abstract val currentAppVersion: String
protected abstract val managers: List<SensorManager>
@Inject
lateinit var integrationUseCase: IntegrationRepository
private val chargingActions = listOf(
Intent.ACTION_BATTERY_LOW,
Intent.ACTION_BATTERY_OKAY,
Intent.ACTION_POWER_CONNECTED,
Intent.ACTION_POWER_DISCONNECTED
)
protected abstract val skippableActions: Map<String, String>
override fun onReceive(context: Context, intent: Intent) {
if (skippableActions.containsKey(intent.action)) {
val sensor = skippableActions[intent.action]
if (!isSensorEnabled(context, sensor!!)) {
Log.d(
tag,
String.format
(
"Sensor %s corresponding to received event %s is disabled, skipping sensors update",
sensor,
intent.action
)
)
return
}
}
if (isSensorEnabled(context, LastUpdateManager.lastUpdate.id)) {
LastUpdateManager().sendLastUpdate(context, intent.action)
val sensorDao = AppDatabase.getInstance(context).sensorDao()
val allSettings = sensorDao.getSettings(LastUpdateManager.lastUpdate.id)
for (setting in allSettings) {
if (setting.value != "" && intent.action == setting.value) {
val eventData = intent.extras?.keySet()?.map { it.toString() to intent.extras?.get(it).toString() }?.toMap()?.plus("intent" to intent.action.toString())
?: mapOf("intent" to intent.action.toString())
Log.d(tag, "Event data: $eventData")
ioScope.launch {
try {
integrationUseCase.fireEvent(
"android.intent_received",
eventData as Map<String, Any>
)
Log.d(tag, "Event successfully sent to Home Assistant")
} catch (e: Exception) {
Log.e(
tag,
"Unable to send event data to Home Assistant",
e
)
}
}
}
}
}
ioScope.launch {
updateSensors(context, integrationUseCase)
if (chargingActions.contains(intent.action)) {
// Add a 5 second delay to perform another update so charging state updates completely.
// This is necessary as the system needs a few seconds to verify the charger.
delay(5000L)
updateSensors(context, integrationUseCase)
}
}
}
protected fun isSensorEnabled(context: Context, id: String): Boolean {
return AppDatabase.getInstance(context).sensorDao().get(id)?.enabled == true
}
suspend fun updateSensors(
context: Context,
integrationUseCase: IntegrationRepository
) {
val sensorDao = AppDatabase.getInstance(context).sensorDao()
val enabledRegistrations = mutableListOf<SensorRegistration<Any>>()
val checkDeviceRegistration = integrationUseCase.getRegistration()
if (checkDeviceRegistration.appVersion == null) {
Log.w(tag, "Device not registered, skipping sensor update/registration")
return
}
val currentHAversion = integrationUseCase.getHomeAssistantVersion()
managers.forEach { manager ->
// Since we don't have this manager injected it doesn't fulfil it's injects, manually
// inject for now I guess?
if (manager is LocationSensorManagerBase)
manager.integrationUseCase = integrationUseCase
try {
manager.requestSensorUpdate(context)
} catch (e: Exception) {
Log.e(tag, "Issue requesting updates for ${context.getString(manager.name)}", e)
}
manager.getAvailableSensors(context).forEach { basicSensor ->
val fullSensor = sensorDao.getFull(basicSensor.id)
val sensor = fullSensor?.sensor
val sensorCoreRegistration = sensor?.coreRegistration
val sensorAppRegistration = sensor?.appRegistration
// Always register enabled sensors in case of available entity updates
// when app or core version change is detected every 4 hours
if (sensor?.enabled == true && sensor.type.isNotBlank() && sensor.icon.isNotBlank() &&
(currentAppVersion != sensorAppRegistration || currentHAversion != sensorCoreRegistration || !sensor.registered)
) {
val reg = fullSensor.toSensorRegistration()
val config = Configuration(context.resources.configuration)
config.setLocale(Locale("en"))
reg.name = context.createConfigurationContext(config).resources.getString(basicSensor.name)
try {
integrationUseCase.registerSensor(reg)
sensor.registered = true
sensor.coreRegistration = currentHAversion
sensor.appRegistration = currentAppVersion
sensorDao.update(sensor)
} catch (e: Exception) {
Log.e(tag, "Issue registering sensor: ${reg.uniqueId}", e)
}
}
if (sensor?.enabled == true && sensor.registered && sensor.state != sensor.lastSentState) {
enabledRegistrations.add(fullSensor.toSensorRegistration())
}
}
}
if (enabledRegistrations.isNotEmpty()) {
var success = false
try {
success = integrationUseCase.updateSensors(enabledRegistrations.toTypedArray())
enabledRegistrations.forEach {
sensorDao.updateLastSendState(it.uniqueId, it.state.toString())
}
} catch (e: Exception) {
Log.e(tag, "Exception while updating sensors.", e)
}
// We failed to update a sensor, we should re register next time
if (!success) {
enabledRegistrations.forEach {
val sensor = sensorDao.get(it.uniqueId)
if (sensor != null) {
sensor.registered = false
sensor.lastSentState = ""
sensorDao.update(sensor)
}
}
}
} else Log.d(tag, "Nothing to update")
}
}

View file

@ -0,0 +1,71 @@
package io.homeassistant.companion.android.common.sensors
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Context
import android.content.Context.NOTIFICATION_SERVICE
import android.os.Build
import android.util.Log
import androidx.core.app.NotificationCompat
import androidx.work.CoroutineWorker
import androidx.work.ForegroundInfo
import androidx.work.WorkerParameters
import io.homeassistant.companion.android.common.R
import io.homeassistant.companion.android.common.data.integration.IntegrationRepository
import io.homeassistant.companion.android.database.AppDatabase
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
abstract class SensorWorkerBase(
val appContext: Context,
workerParams: WorkerParameters
) : CoroutineWorker(appContext, workerParams) {
protected abstract val integrationUseCase: IntegrationRepository
protected abstract val sensorReceiver: SensorReceiverBase
companion object {
const val TAG = "SensorWorker"
const val channelId = "Sensor Worker"
const val NOTIFICATION_ID = 42
}
private val notificationManager = appContext.getSystemService(NOTIFICATION_SERVICE) as NotificationManager
override suspend fun doWork(): Result = withContext(Dispatchers.IO) {
val sensorDao = AppDatabase.getInstance(applicationContext).sensorDao()
val enabledSensorCount = sensorDao.getEnabledCount() ?: 0
if (enabledSensorCount > 0) {
Log.d(TAG, "Updating all Sensors.")
createNotificationChannel()
val notification = NotificationCompat.Builder(applicationContext, channelId)
.setSmallIcon(R.drawable.ic_stat_ic_notification)
.setContentTitle(appContext.getString(R.string.updating_sensors))
.setPriority(NotificationCompat.PRIORITY_LOW)
.build()
val foregroundInfo = ForegroundInfo(NOTIFICATION_ID, notification)
setForeground(foregroundInfo)
val lastUpdateSensor = sensorDao.get(LastUpdateManager.lastUpdate.id)
if (lastUpdateSensor != null) {
if (lastUpdateSensor.enabled)
LastUpdateManager().sendLastUpdate(appContext, TAG)
}
sensorReceiver.updateSensors(appContext, integrationUseCase)
}
Result.success()
}
protected fun createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
var notificationChannel =
notificationManager.getNotificationChannel(channelId)
if (notificationChannel == null) {
notificationChannel = NotificationChannel(
channelId, TAG, NotificationManager.IMPORTANCE_LOW
)
notificationManager.createNotificationChannel(notificationChannel)
}
}
}
}

View file

@ -18,7 +18,7 @@ import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
import io.homeassistant.companion.android.R
import io.homeassistant.companion.android.common.R
import io.homeassistant.companion.android.common.data.integration.IntegrationRepository
import io.homeassistant.companion.android.database.authentication.Authentication
import io.homeassistant.companion.android.database.authentication.AuthenticationDao

View file

@ -1,21 +1,21 @@
package io.homeassistant.companion.android.database.qs
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "qs_tiles")
data class TileEntity(
@PrimaryKey(autoGenerate = true)
val id: Int,
@ColumnInfo(name = "tileId")
val tileId: String,
@ColumnInfo(name = "icon_id")
val iconId: Int?,
@ColumnInfo(name = "entityId")
val entityId: String,
@ColumnInfo(name = "label")
val label: String,
@ColumnInfo(name = "subtitle")
val subtitle: String?
)
package io.homeassistant.companion.android.database.qs
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "qs_tiles")
data class TileEntity(
@PrimaryKey(autoGenerate = true)
val id: Int,
@ColumnInfo(name = "tileId")
val tileId: String,
@ColumnInfo(name = "icon_id")
val iconId: Int?,
@ColumnInfo(name = "entityId")
val entityId: String,
@ColumnInfo(name = "label")
val label: String,
@ColumnInfo(name = "subtitle")
val subtitle: String?
)

View file

Before

Width:  |  Height:  |  Size: 934 B

After

Width:  |  Height:  |  Size: 934 B

View file

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View file

Before

Width:  |  Height:  |  Size: 620 B

After

Width:  |  Height:  |  Size: 620 B

View file

Before

Width:  |  Height:  |  Size: 910 B

After

Width:  |  Height:  |  Size: 910 B

View file

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View file

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

View file

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

View file

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

View file

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

View file

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

View file

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="basic_sensor_name_battery_health">Battery Health</string>
<string name="basic_sensor_name_battery_level">Battery Level</string>
<string name="basic_sensor_name_battery_state">Battery State</string>
<string name="basic_sensor_name_battery_temperature">Battery Temperature</string>
<string name="basic_sensor_name_charger_type">Charger Type</string>
<string name="basic_sensor_name_charging">Is Charging</string>
<string name="basic_sensor_name_last_update">Last Update Trigger</string>
<string name="database_event_failure">Unable to send migration failure event to Home Assistant</string>
<string name="database_migration_failed">Database migration failed, all sensor and widget data has been reset. Please re-enable your sensors and recreate your widgets.</string>
<string name="sensor">Sensor</string>
<string name="sensors">Sensors</string>
<string name="sensor_description_none">No description</string>
<string name="updating_sensors">Updating Sensors</string>
<string name="sensor_description_battery_health">The health of the battery</string>
<string name="sensor_description_battery_level">The current battery level of the device</string>
<string name="sensor_description_battery_state">The current charging state of the battery</string>
<string name="sensor_description_battery_temperature">The current battery temperature</string>
<string name="sensor_description_charger_type">The type of charger plugged into the device currently</string>
<string name="sensor_description_charging">Whether or not the device is actively charging</string>
<string name="sensor_description_last_update">The intent for the last update that was sent, periodic updates will show as \"SensorWorker\". Enabling the \"Add New Intent\" toggle will create 1 setting to allow you to register for a broadcast intent. The toggle will switch back to off once a new setting is created so you will need to turn it back on to save more intents. You can also clear out the setting value to remove the setting in the next update. You must restart the application after making changes to these settings to take effect.</string>
<string name="sensor_name_battery">Battery Sensors</string>
<string name="sensor_name_last_update">Last Update Sensor</string>
</resources>

View file

@ -69,6 +69,10 @@ android {
lint {
disable("MissingTranslation")
}
kapt {
correctErrorTypes = true
}
}
play {
@ -102,6 +106,7 @@ dependencies {
implementation("com.mikepenz:community-material-typeface:6.4.95.0-kotlin@aar")
implementation("com.mikepenz:iconics-compose:5.3.3")
implementation("androidx.activity:activity:1.4.0")
implementation("androidx.activity:activity-compose:1.4.0")
implementation("androidx.compose.compiler:compiler:1.0.5")
implementation("androidx.compose.foundation:foundation:1.0.5")

View file

@ -18,6 +18,18 @@
android:supportsRtl="true"
android:theme="@style/Theme.HomeAssistant"
android:fullBackupContent="@xml/backup_rules">
<!-- Start things like SensorWorker on device boot -->
<receiver android:name=".sensors.SensorReceiver"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
<action android:name="android.intent.action.MY_PACKAGE_SUSPENDED" />
<action android:name="android.intent.action.MY_PACKAGE_UNSUSPENDED" />
<action android:name="android.intent.action.QUICKBOOT_POWERON" />
<action android:name="com.htc.intent.action.QUICKBOOT_POWERON" />
</intent-filter>
</receiver>
<uses-library
android:name="com.google.android.wearable"
android:required="true" />

View file

@ -1,7 +1,28 @@
package io.homeassistant.companion.android
import android.app.Application
import android.content.Intent
import android.content.IntentFilter
import dagger.hilt.android.HiltAndroidApp
import io.homeassistant.companion.android.sensors.SensorReceiver
@HiltAndroidApp
open class HomeAssistantApplication : Application()
open class HomeAssistantApplication : Application() {
override fun onCreate() {
super.onCreate()
val sensorReceiver = SensorReceiver()
// This will cause the sensor to be updated every time the OS broadcasts that a cable was plugged/unplugged.
// This should be nearly instantaneous allowing automations to fire immediately when a phone is plugged
// in or unplugged. Updates will also be triggered when the system reports low battery and when it recovers.
registerReceiver(
sensorReceiver,
IntentFilter().apply {
addAction(Intent.ACTION_BATTERY_LOW)
addAction(Intent.ACTION_BATTERY_OKAY)
addAction(Intent.ACTION_POWER_CONNECTED)
addAction(Intent.ACTION_POWER_DISCONNECTED)
}
)
}
}

View file

@ -11,6 +11,8 @@ 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
@AndroidEntryPoint
@ -46,6 +48,22 @@ class HomeActivity : ComponentActivity(), HomeView {
override fun onResume() {
mainViewModel.updateFavorites()
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)
}
}
}
override fun onPause() {
super.onPause()
SensorWorker.start(this)
}
override fun onDestroy() {

View file

@ -0,0 +1,35 @@
package io.homeassistant.companion.android.sensors
import android.annotation.SuppressLint
import dagger.hilt.android.AndroidEntryPoint
import io.homeassistant.companion.android.BuildConfig
import io.homeassistant.companion.android.common.sensors.BatterySensorManager
import io.homeassistant.companion.android.common.sensors.SensorManager
import io.homeassistant.companion.android.common.sensors.SensorReceiverBase
@AndroidEntryPoint
class SensorReceiver : SensorReceiverBase() {
override val tag: String
get() = TAG
override val currentAppVersion: String
get() = BuildConfig.VERSION_NAME
override val managers: List<SensorManager>
get() = MANAGERS
companion object {
const val TAG = "SensorReceiver"
val MANAGERS = listOf(
BatterySensorManager()
)
const val ACTION_REQUEST_SENSORS_UPDATE =
"io.homeassistant.companion.android.background.REQUEST_SENSORS_UPDATE"
}
// Suppress Lint because we only register for the receiver if the android version matches the intent
@SuppressLint("InlinedApi")
override val skippableActions = mapOf<String, String>()
}

View file

@ -0,0 +1,58 @@
package io.homeassistant.companion.android.sensors
import android.content.Context
import androidx.work.Constraints
import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.NetworkType
import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkManager
import androidx.work.WorkerParameters
import dagger.hilt.EntryPoint
import dagger.hilt.InstallIn
import dagger.hilt.android.EntryPointAccessors
import dagger.hilt.components.SingletonComponent
import io.homeassistant.companion.android.common.data.integration.IntegrationRepository
import io.homeassistant.companion.android.common.sensors.SensorReceiverBase
import io.homeassistant.companion.android.common.sensors.SensorWorkerBase
import java.util.concurrent.TimeUnit
class SensorWorker(
appContext: Context,
workerParams: WorkerParameters
) : SensorWorkerBase(appContext, workerParams) {
companion object {
fun start(context: Context) {
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED).build()
val sensorWorker =
PeriodicWorkRequestBuilder<SensorWorker>(15, TimeUnit.MINUTES)
.setConstraints(constraints)
.build()
WorkManager.getInstance(context)
.enqueueUniquePeriodicWork(TAG, ExistingPeriodicWorkPolicy.REPLACE, sensorWorker)
}
}
@EntryPoint
@InstallIn(SingletonComponent::class)
interface SensorWorkerEntryPoint {
fun integrationRepository(): IntegrationRepository
}
override val integrationUseCase: IntegrationRepository
get() {
return EntryPointAccessors.fromApplication(
appContext,
SensorWorkerEntryPoint::class.java
)
.integrationRepository()
}
override val sensorReceiver: SensorReceiverBase
get() {
return SensorReceiver()
}
}