Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add CarInfo sensors #3399

Merged
merged 31 commits into from
Jul 17, 2023
Merged
Show file tree
Hide file tree
Changes from 28 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
7a17d91
Add CarInfoSensorManager
drosoCode Mar 5, 2023
dad295e
use overrideLibrary instead of increasing minSdk
drosoCode Mar 6, 2023
34a3558
use fullImplementation
drosoCode Mar 6, 2023
455613b
ensure that the sensor is enabled
drosoCode Mar 8, 2023
4d002e8
check sensorid for required permissions
drosoCode Mar 8, 2023
64144cb
fix formatting
drosoCode Mar 8, 2023
4a2fc59
start app notification
drosoCode Mar 13, 2023
adca139
merge CarInfoSensorManager into AndroidAutoSensorManager
drosoCode Apr 1, 2023
a3b352d
fix formatting
drosoCode Apr 1, 2023
033d1c0
Merge branch 'master' into feature/carinfo
drosoCode Apr 1, 2023
0ea0531
fix merge error
drosoCode Apr 1, 2023
5cfbdd2
add other sensors
drosoCode Apr 9, 2023
c015f57
prevent multiple notifications
drosoCode Apr 9, 2023
4184a61
apply review comments
drosoCode Apr 12, 2023
741f473
add "needs to be started" state to sensors
drosoCode Apr 12, 2023
3a35c37
apply review comments
drosoCode Apr 17, 2023
4193420
add android auto channel
drosoCode Jun 20, 2023
9f92ff7
set car battery level as diagnostic sensor
drosoCode Jun 20, 2023
14b3caf
remove onNewIntent (not needed)
drosoCode Jun 20, 2023
b285a07
formatting
drosoCode Jun 20, 2023
d4e0ff3
remove notification code
drosoCode Jul 12, 2023
ae416e6
replace "android_auto" prefix by "car"
drosoCode Jul 12, 2023
1c384ab
Merge branch 'master' into feature/carinfo
drosoCode Jul 12, 2023
c0ce4e7
move carinfo sensors to main instead of full
drosoCode Jul 12, 2023
46ef3a2
move android car sdk override to main
drosoCode Jul 12, 2023
a7617b1
update unavailable message
drosoCode Jul 12, 2023
a15b678
add hasSensor method
drosoCode Jul 13, 2023
fefda68
hide sensors for automotive
drosoCode Jul 13, 2023
dedac34
move automotive check and limit sensors to full version
drosoCode Jul 13, 2023
596b162
remove _level from sensor names and ids
drosoCode Jul 15, 2023
0fe85a1
remove alreadySentMessage
drosoCode Jul 15, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,7 @@ dependencies {
implementation("com.github.AppDevNext:ChangeLog:3.4")

implementation("androidx.car.app:app:1.3.0-rc01")
"fullImplementation"("androidx.car.app:app-projected:1.2.0")
}

// Disable to fix memory leak and be compatible with the configuration cache.
Expand Down
2 changes: 2 additions & 0 deletions app/src/full/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
xmlns:tools="http://schemas.android.com/tools">

<uses-sdk tools:overrideLibrary="com.google.android.gms.threadnetwork" />
<uses-permission android:name="com.google.android.gms.permission.CAR_FUEL" />
<uses-permission android:name="com.google.android.gms.permission.CAR_MILEAGE" />
dshokouhi marked this conversation as resolved.
Show resolved Hide resolved

<queries>
<!-- For GMS Core/Play service -->
Expand Down
2 changes: 1 addition & 1 deletion app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">

<uses-sdk tools:overrideLibrary="androidx.wear.remote.interactions" />
<uses-sdk tools:overrideLibrary="androidx.wear.remote.interactions,androidx.car.app.projected" />
jpelgrom marked this conversation as resolved.
Show resolved Hide resolved

<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,307 @@
package io.homeassistant.companion.android.sensors

import android.content.Context
import android.content.pm.PackageManager
import android.os.Build
import android.util.Log
import androidx.annotation.RequiresApi
import androidx.car.app.hardware.common.CarValue
import androidx.car.app.hardware.info.EnergyLevel
import androidx.car.app.hardware.info.EvStatus
import androidx.car.app.hardware.info.Mileage
import androidx.car.app.hardware.info.Model
import androidx.core.content.ContextCompat
import androidx.lifecycle.DefaultLifecycleObserver
import io.homeassistant.companion.android.common.R
import io.homeassistant.companion.android.common.sensors.SensorManager
import io.homeassistant.companion.android.vehicle.HaCarAppService

class CarSensorManager :
SensorManager,
DefaultLifecycleObserver {

companion object {
internal const val TAG = "CarSM"

private val fuelLevel = SensorManager.BasicSensor(
"car_fuel_level",
jpelgrom marked this conversation as resolved.
Show resolved Hide resolved
"sensor",
R.string.basic_sensor_name_car_fuel_level,
R.string.sensor_description_car_fuel_level,
"mdi:barrel",
unitOfMeasurement = "%",
stateClass = SensorManager.STATE_CLASS_MEASUREMENT,
deviceClass = "battery"
)
private val batteryLevel = SensorManager.BasicSensor(
"car_battery_level",
jpelgrom marked this conversation as resolved.
Show resolved Hide resolved
"sensor",
R.string.basic_sensor_name_car_battery_level,
R.string.sensor_description_car_battery_level,
"mdi:car-battery",
unitOfMeasurement = "%",
stateClass = SensorManager.STATE_CLASS_MEASUREMENT,
deviceClass = "battery",
entityCategory = SensorManager.ENTITY_CATEGORY_DIAGNOSTIC
)
private val carName = SensorManager.BasicSensor(
"car_name",
"sensor",
R.string.basic_sensor_name_car_name,
R.string.sensor_description_car_name,
"mdi:car-info"
)
private val carStatus = SensorManager.BasicSensor(
"car_charging_status",
"sensor",
R.string.basic_sensor_name_car_charging_status,
R.string.sensor_description_car_charging_status,
"mdi:ev-station",
deviceClass = "plug"
)
private val odometerValue = SensorManager.BasicSensor(
"car_odometer",
"sensor",
R.string.basic_sensor_name_car_odometer,
R.string.sensor_description_car_odometer,
"mdi:map-marker-distance",
unitOfMeasurement = "m",
stateClass = SensorManager.STATE_CLASS_TOTAL_INCREASING,
deviceClass = "distance"
)

private val sensorsList = listOf(
batteryLevel,
carName,
carStatus,
fuelLevel,
odometerValue
)

// track if we have already sent the "open app" message for each sensor
private val connectList = mutableMapOf(
batteryLevel to false,
carName to false,
carStatus to false,
fuelLevel to false,
odometerValue to false
)

private enum class Listener {
ENERGY, MODEL, MILEAGE, STATUS,
}

private val listenerSensors = mapOf(
Listener.ENERGY to listOf(batteryLevel, fuelLevel),
Listener.MODEL to listOf(carName),
Listener.STATUS to listOf(carStatus),
Listener.MILEAGE to listOf(odometerValue)
)
private val listenerLastRegistered = mutableMapOf(
Listener.ENERGY to -1L,
Listener.MODEL to -1L,
Listener.STATUS to -1L,
Listener.MILEAGE to -1L
)
}

override val name: Int
get() = R.string.sensor_name_car

override suspend fun getAvailableSensors(context: Context): List<SensorManager.BasicSensor> {
// TODO: show sensors for automotive (except odometer) once
// we can ask for special automotive permissions in requiredPermissions
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O &&
!context.packageManager.hasSystemFeature(
PackageManager.FEATURE_AUTOMOTIVE
)
) {
sensorsList
} else {
emptyList()
}
}

override fun hasSensor(context: Context): Boolean {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
dshokouhi marked this conversation as resolved.
Show resolved Hide resolved
}

override fun requiredPermissions(sensorId: String): Array<String> {
return when {
(sensorId == fuelLevel.id || sensorId == batteryLevel.id) -> {
arrayOf("com.google.android.gms.permission.CAR_FUEL")
}
sensorId == odometerValue.id -> {
arrayOf("com.google.android.gms.permission.CAR_MILEAGE")
}
else -> emptyArray()
}
}

private lateinit var context: Context

private fun allDisabled(): Boolean = sensorsList.none { isEnabled(context, it) }

@RequiresApi(Build.VERSION_CODES.O)
private fun connected(): Boolean = HaCarAppService.carInfo != null

override fun requestSensorUpdate(context: Context) {
this.context = context.applicationContext
if (allDisabled()) {
return
}

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
if (connected()) {
updateCarInfo()
} else {
connectList.forEach { (sensor, alreadySentMessage) ->
jpelgrom marked this conversation as resolved.
Show resolved Hide resolved
if (isEnabled(context, sensor) && !alreadySentMessage) {
onSensorUpdated(
context,
sensor,
context.getString(R.string.car_data_unavailable),
sensor.statelessIcon,
mapOf()
)
connectList[sensor] = true
}
}
}
}
}

@RequiresApi(Build.VERSION_CODES.O)
@androidx.annotation.OptIn(androidx.car.app.annotations.ExperimentalCarApi::class)
private fun setListener(l: Listener, enable: Boolean) {
if (enable) {
Log.d(TAG, "registering CarInfo $l listener")
} else {
Log.d(TAG, "unregistering CarInfo $l listener")
}

val car = HaCarAppService.carInfo ?: return
when (l) {
Listener.ENERGY -> {
if (enable) {
car.addEnergyLevelListener(ContextCompat.getMainExecutor(context), ::onEnergyAvailable)
} else {
car.removeEnergyLevelListener(::onEnergyAvailable)
}
}
Listener.MILEAGE -> {
if (enable) {
car.addMileageListener(ContextCompat.getMainExecutor(context), ::onMileageAvailable)
} else {
car.removeMileageListener(::onMileageAvailable)
}
}
Listener.MODEL -> {
if (enable) {
car.fetchModel(ContextCompat.getMainExecutor(context), ::onModelAvailable)
}
}
Listener.STATUS -> {
if (enable) {
car.addEvStatusListener(ContextCompat.getMainExecutor(context), ::onStatusAvailable)
} else {
car.removeEvStatusListener(::onStatusAvailable)
}
}
}

if (enable) {
listenerLastRegistered[l] = System.currentTimeMillis()
} else {
listenerLastRegistered[l] = -1L
}
}

@RequiresApi(Build.VERSION_CODES.O)
private fun updateCarInfo() {
for (l in listenerSensors) {
if (l.value.any { isEnabled(context, it) }) {
if (listenerLastRegistered[l.key] != -1L && listenerLastRegistered[l.key]!! + SensorManager.SENSOR_LISTENER_TIMEOUT < System.currentTimeMillis()) {
Log.d(TAG, "Re-registering CarInfo ${l.key} listener as it appears to be stuck")
setListener(l.key, false)
}

if (listenerLastRegistered[l.key] == -1L) {
setListener(l.key, true)
}
}
}
}

@RequiresApi(Build.VERSION_CODES.O)
fun onEnergyAvailable(data: EnergyLevel) {
if (data.fuelPercent.status == CarValue.STATUS_SUCCESS && isEnabled(context, fuelLevel)) {
onSensorUpdated(
context,
fuelLevel,
data.fuelPercent.value!!,
fuelLevel.statelessIcon,
mapOf()
)
}
if (data.batteryPercent.status == CarValue.STATUS_SUCCESS && isEnabled(context, batteryLevel)) {
onSensorUpdated(
context,
batteryLevel,
data.batteryPercent.value!!,
batteryLevel.statelessIcon,
mapOf()
)
}
setListener(Listener.ENERGY, false)
}

@RequiresApi(Build.VERSION_CODES.O)
fun onModelAvailable(data: Model) {
if (data.name.status == CarValue.STATUS_SUCCESS && isEnabled(context, carName)) {
onSensorUpdated(
context,
carName,
data.name.value!!,
carName.statelessIcon,
mapOf(
"car_manufacturer" to data.manufacturer.value,
"car_manufactured_year" to data.year.value
)
)
}
setListener(Listener.MODEL, false)
}

@RequiresApi(Build.VERSION_CODES.O)
@androidx.annotation.OptIn(androidx.car.app.annotations.ExperimentalCarApi::class)
fun onStatusAvailable(data: EvStatus) {
if (data.evChargePortConnected.status == CarValue.STATUS_SUCCESS && isEnabled(context, carStatus)) {
onSensorUpdated(
context,
carStatus,
data.evChargePortConnected.value == true,
carStatus.statelessIcon,
mapOf(
"car_charge_port_open" to (data.evChargePortOpen.value == true)
)
)
}
setListener(Listener.STATUS, false)
}

@RequiresApi(Build.VERSION_CODES.O)
@androidx.annotation.OptIn(androidx.car.app.annotations.ExperimentalCarApi::class)
fun onMileageAvailable(data: Mileage) {
if (data.odometerMeters.status == CarValue.STATUS_SUCCESS && isEnabled(context, odometerValue)) {
onSensorUpdated(
context,
odometerValue,
data.odometerMeters.value!!,
odometerValue.statelessIcon,
mapOf()
)
}
setListener(Listener.MILEAGE, false)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ class SensorReceiver : SensorReceiverBase() {
AudioSensorManager(),
BatterySensorManager(),
BluetoothSensorManager(),
CarSensorManager(),
DisplaySensorManager(),
DNDSensorManager(),
DynamicColorSensorManager(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import androidx.car.app.Screen
import androidx.car.app.ScreenManager
import androidx.car.app.Session
import androidx.car.app.SessionInfo
import androidx.car.app.hardware.CarHardwareManager
import androidx.car.app.hardware.info.CarInfo
import androidx.car.app.validation.HostValidator
import androidx.lifecycle.lifecycleScope
import dagger.hilt.android.AndroidEntryPoint
Expand All @@ -32,6 +34,8 @@ class HaCarAppService : CarAppService() {

companion object {
private const val TAG = "HaCarAppService"
var carInfo: CarInfo? = null
private set
}

@Inject
Expand Down Expand Up @@ -67,6 +71,8 @@ class HaCarAppService : CarAppService() {
)

override fun onCreateScreen(intent: Intent): Screen {
carInfo = carContext.getCarService(CarHardwareManager::class.java).carInfo

if (intent.getBooleanExtra("TRANSITION_LAUNCH", false)) {
carContext
.getCarService(ScreenManager::class.java).run {
Expand Down
Loading