Skip to content

Commit

Permalink
Add CarInfo sensors (#3399)
Browse files Browse the repository at this point in the history
* Add CarInfoSensorManager

* use overrideLibrary instead of increasing minSdk

* use fullImplementation

* ensure that the sensor is enabled

* check sensorid for required permissions

* fix formatting

* start app notification

* merge CarInfoSensorManager into AndroidAutoSensorManager

* fix formatting

* fix merge error

* add other sensors

car name, manufacturer, manufacturing year, odometer, ev status (connected, charging port open/close)

* prevent multiple notifications

* apply review comments

* add "needs to be started" state to sensors

* apply review comments

* add android auto channel

* set car battery level as diagnostic sensor

* remove onNewIntent (not needed)

* formatting

* remove notification code

* replace "android_auto" prefix by "car"

* move carinfo sensors to main instead of full

* move android car sdk override to main

* update unavailable message

* add hasSensor method

* hide sensors for automotive

* move automotive check and limit sensors to full version

* remove _level from sensor names and ids

* remove alreadySentMessage
  • Loading branch information
drosoCode authored Jul 17, 2023
1 parent 4b1be89 commit 33dfc69
Show file tree
Hide file tree
Showing 7 changed files with 319 additions and 1 deletion.
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" />

<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" />

<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,296 @@
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.BuildConfig
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",
"sensor",
R.string.basic_sensor_name_car_fuel,
R.string.sensor_description_car_fuel,
"mdi:barrel",
unitOfMeasurement = "%",
stateClass = SensorManager.STATE_CLASS_MEASUREMENT,
deviceClass = "battery"
)
private val batteryLevel = SensorManager.BasicSensor(
"car_battery",
"sensor",
R.string.basic_sensor_name_car_battery,
R.string.sensor_description_car_battery,
"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
)

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> {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
sensorsList
} else {
emptyList()
}
}

override fun hasSensor(context: Context): Boolean {
// TODO: show sensors for automotive (except odometer) once
// we can ask for special automotive permissions in requiredPermissions
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.O &&
!context.packageManager.hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE) &&
BuildConfig.FLAVOR == "full"
}

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 {
sensorsList.forEach {
if (isEnabled(context, it)) {
onSensorUpdated(
context,
it,
context.getString(R.string.car_data_unavailable),
it.statelessIcon,
mapOf()
)
}
}
}
}
}

@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
12 changes: 12 additions & 0 deletions common/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,11 @@
<string name="basic_sensor_name_bluetooth_ble_beacon_monitor">Beacon Monitor</string>
<string name="basic_sensor_name_bluetooth_state">Bluetooth State</string>
<string name="basic_sensor_name_bluetooth">Bluetooth Connection</string>
<string name="basic_sensor_name_car_battery">Car Battery</string>
<string name="basic_sensor_name_car_name">Car Name</string>
<string name="basic_sensor_name_car_charging_status">Charging Status</string>
<string name="basic_sensor_name_car_fuel">Fuel</string>
<string name="basic_sensor_name_car_odometer">Odometer</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>
Expand Down Expand Up @@ -523,6 +528,11 @@
<string name="sensor_description_bluetooth_ble_beacon_monitor">Scans for iBeacons and shows the IDs of nearby beacons and their distance in meters. A notification will be shown on the device when scanning is actively running.\n\nWarning: this can affect battery life, especially with a short \"Scan Interval\".\n\nSettings allow for specifying:\n- \"Filter Iterations\" (should be 1 - 100, default: 10), higher values will result in more stable measurements but also less responsiveness.\n- \"Filter RSSI Multiplier\" (should be 1.0 - 2.0, default: 1.05), can be used to archive more stable measurements when beacons are farther away. This will also affect responsiveness.\n- \"Scan Interval\" (default: 500) milliseconds between scans. Shorter intervals will drain the battery more quickly.\n- \"Scan Period\" (default: 1100) milliseconds to scan for beacons. Most beacons will send a signal every second so this value should be at least 1100ms.\n- \"UUID Filter\" allows to restrict the reported beacons by including (or excluding) those with the selected UUIDs.\n- \"Exclude selected UUIDs\", if false (default) only the beacons with the selected UUIDs are reported. If true all beacons except the selected ones are reported. Not available when \"UUID Filter\" is empty.\n\nNote:\nAdditionally a separate setting exists (\"Enable Beacon Monitor\") to stop or start scanning.</string>
<string name="sensor_description_bluetooth_connection">Information about currently connected Bluetooth devices</string>
<string name="sensor_description_bluetooth_state">Whether Bluetooth is enabled on the device</string>
<string name="sensor_description_car_battery">Remaining car battery percentage</string>
<string name="sensor_description_car_name">Car name, manufacturer and year</string>
<string name="sensor_description_car_charging_status">Charging status (only for EVs)</string>
<string name="sensor_description_car_fuel">Remaining car fuel percentage</string>
<string name="sensor_description_car_odometer">Odometer value</string>
<string name="sensor_description_charger_type">The type of charger plugged into the device currently</string>
<string name="sensor_description_charging">Whether the device is actively charging</string>
<string name="sensor_description_current_time_zone">The current time zone the device is in</string>
Expand Down Expand Up @@ -594,6 +604,7 @@
<string name="sensor_name_audio">Audio Sensors</string>
<string name="sensor_name_battery">Battery Sensors</string>
<string name="sensor_name_bluetooth">Bluetooth Sensors</string>
<string name="sensor_name_car">Car Sensors</string>
<string name="sensor_name_device_policy">Device Policy</string>
<string name="sensor_name_dnd">Do Not Disturb Sensor</string>
<string name="sensor_name_geolocation">Geolocation Sensors</string>
Expand Down Expand Up @@ -1100,4 +1111,5 @@
<string name="ha_assist">HA: Assist</string>
<string name="only_favorites">Only Show Favorites</string>
<string name="beacon_scanning">Beacon Monitor Scanning</string>
<string name="car_data_unavailable">Open Home Assistant app to activate the sensor</string>
</resources>

0 comments on commit 33dfc69

Please sign in to comment.