mirror of
https://github.com/home-assistant/android
synced 2024-10-02 22:34:46 +00:00
Sync sensor enabled state with core 2022.6+ (#2547)
* Sync sensor enabled state from app to core - Change sensor 'registered' to indicate not registered, registered as disabled or registered as enabled - When a sensor is disabled, set it's status as such on core 2022.6 servers - In the sensor registration, use the basic sensor details in case there is no info in the database that can be used * Clean up version check - Now that there is a standard function to get the version, re-use it * Improve which sensors are synced to core - Update database migration to reset registration state on db upgrade to make sure the information is correct for all sensors - Only sync sensors that are actually available on the device - Fix registering all sensors on core <2022.6 on app/core version change - Fix unnecessary sensor updates due to location sensors which are never registered always triggering update on core >=2022.6 * Sync sensor enabled state from core to app - After pushing app enabled changes to core, if on core 2022.6 check if the enabled state for any of the sensors is different from what is registered in the app and try to update the sensors. This is done by reading the entire config, because otherwise we wouldn't know about sensors that have been enabled and the app already updates all sensors at the same time so no need for tracking individual updates in the sensor update response. - Restore getting config using webhooks, because only the webhook config contains information about entity enabled state - Add support for opening a specific sensor's settings page, used for the notification when trying to enable a sensor that requires granting (additional) permissions
This commit is contained in:
parent
c532b74a94
commit
c98b291d30
|
@ -3,6 +3,7 @@ package io.homeassistant.companion.android.sensors
|
|||
import android.annotation.SuppressLint
|
||||
import android.app.NotificationManager
|
||||
import android.bluetooth.BluetoothAdapter
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.media.AudioManager
|
||||
import android.net.wifi.WifiManager
|
||||
|
@ -14,6 +15,7 @@ import io.homeassistant.companion.android.common.sensors.LastUpdateManager
|
|||
import io.homeassistant.companion.android.common.sensors.NetworkSensorManager
|
||||
import io.homeassistant.companion.android.common.sensors.SensorManager
|
||||
import io.homeassistant.companion.android.common.sensors.SensorReceiverBase
|
||||
import io.homeassistant.companion.android.settings.SettingsActivity
|
||||
|
||||
@AndroidEntryPoint
|
||||
class SensorReceiver : SensorReceiverBase() {
|
||||
|
@ -83,4 +85,12 @@ class SensorReceiver : SensorReceiverBase() {
|
|||
Intent.ACTION_MANAGED_PROFILE_AVAILABLE to DevicePolicyManager.isWorkProfile.id,
|
||||
WifiManager.WIFI_STATE_CHANGED_ACTION to NetworkSensorManager.wifiState.id,
|
||||
)
|
||||
|
||||
override fun getSensorSettingsIntent(context: Context, id: String): Intent? {
|
||||
return SettingsActivity.newInstance(context).apply {
|
||||
putExtra("fragment", "sensors/$id")
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,6 +14,7 @@ import dagger.hilt.android.components.ActivityComponent
|
|||
import io.homeassistant.companion.android.BaseActivity
|
||||
import io.homeassistant.companion.android.R
|
||||
import io.homeassistant.companion.android.settings.notification.NotificationHistoryFragment
|
||||
import io.homeassistant.companion.android.settings.sensor.SensorDetailFragment
|
||||
import io.homeassistant.companion.android.settings.websocket.WebsocketSettingFragment
|
||||
import io.homeassistant.companion.android.common.R as commonR
|
||||
|
||||
|
@ -52,16 +53,16 @@ class SettingsActivity : BaseActivity() {
|
|||
.beginTransaction()
|
||||
.replace(
|
||||
R.id.content,
|
||||
if (settingsNavigation == null)
|
||||
SettingsFragment::class.java
|
||||
else {
|
||||
when (settingsNavigation) {
|
||||
"websocket" -> WebsocketSettingFragment::class.java
|
||||
"notification_history" -> NotificationHistoryFragment::class.java
|
||||
else -> SettingsFragment::class.java
|
||||
}
|
||||
when {
|
||||
settingsNavigation == "websocket" -> WebsocketSettingFragment::class.java
|
||||
settingsNavigation == "notification_history" -> NotificationHistoryFragment::class.java
|
||||
settingsNavigation?.startsWith("sensors/") == true -> SensorDetailFragment::class.java
|
||||
else -> SettingsFragment::class.java
|
||||
},
|
||||
null
|
||||
if (settingsNavigation?.startsWith("sensors/") == true) {
|
||||
val sensorId = settingsNavigation.split("/")[1]
|
||||
SensorDetailFragment.newInstance(sensorId).arguments
|
||||
} else null
|
||||
)
|
||||
.commit()
|
||||
}
|
||||
|
|
|
@ -72,19 +72,12 @@ class WebViewPresenterImpl @Inject constructor(
|
|||
|
||||
override fun checkSecurityVersion() {
|
||||
mainScope.launch {
|
||||
|
||||
try {
|
||||
val version = integrationUseCase.getHomeAssistantVersion().split(".")
|
||||
if (version.size >= 3) {
|
||||
val year = Integer.parseInt(version[0])
|
||||
val month = Integer.parseInt(version[1])
|
||||
val release = Integer.parseInt(version[2])
|
||||
if (year < 2021 || (year == 2021 && month == 1 && release < 5)) {
|
||||
if (integrationUseCase.shouldNotifySecurityWarning()) {
|
||||
view.showError(WebView.ErrorType.SECURITY_WARNING)
|
||||
} else {
|
||||
Log.w(TAG, "Still not updated but have already notified.")
|
||||
}
|
||||
if (!integrationUseCase.isHomeAssistantVersionAtLeast(2021, 1, 5)) {
|
||||
if (integrationUseCase.shouldNotifySecurityWarning()) {
|
||||
view.showError(WebView.ErrorType.SECURITY_WARNING)
|
||||
} else {
|
||||
Log.w(TAG, "Still not updated but have already notified.")
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
|
|
|
@ -0,0 +1,679 @@
|
|||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 28,
|
||||
"identityHash": "dbee144b17c2b37aba9ab9701ea0e715",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "sensor_attributes",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sensor_id` TEXT NOT NULL, `name` TEXT NOT NULL, `value` TEXT NOT NULL, `value_type` TEXT NOT NULL, PRIMARY KEY(`sensor_id`, `name`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "sensorId",
|
||||
"columnName": "sensor_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "value",
|
||||
"columnName": "value",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "valueType",
|
||||
"columnName": "value_type",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"sensor_id",
|
||||
"name"
|
||||
],
|
||||
"autoGenerate": false
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "Authentication_List",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`host` TEXT NOT NULL, `Username` TEXT NOT NULL, `Password` TEXT NOT NULL, PRIMARY KEY(`host`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "host",
|
||||
"columnName": "host",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "username",
|
||||
"columnName": "Username",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "password",
|
||||
"columnName": "Password",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"host"
|
||||
],
|
||||
"autoGenerate": false
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "sensors",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `enabled` INTEGER NOT NULL, `registered` INTEGER DEFAULT NULL, `state` TEXT NOT NULL, `last_sent_state` TEXT NOT NULL, `state_type` TEXT NOT NULL, `type` TEXT NOT NULL, `icon` TEXT NOT NULL, `name` TEXT NOT NULL, `device_class` TEXT, `unit_of_measurement` TEXT, `state_class` TEXT, `entity_category` TEXT, `core_registration` TEXT, `app_registration` TEXT, PRIMARY KEY(`id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "enabled",
|
||||
"columnName": "enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "registered",
|
||||
"columnName": "registered",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false,
|
||||
"defaultValue": "NULL"
|
||||
},
|
||||
{
|
||||
"fieldPath": "state",
|
||||
"columnName": "state",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastSentState",
|
||||
"columnName": "last_sent_state",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "stateType",
|
||||
"columnName": "state_type",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "type",
|
||||
"columnName": "type",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "icon",
|
||||
"columnName": "icon",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "deviceClass",
|
||||
"columnName": "device_class",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "unitOfMeasurement",
|
||||
"columnName": "unit_of_measurement",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "stateClass",
|
||||
"columnName": "state_class",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "entityCategory",
|
||||
"columnName": "entity_category",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "coreRegistration",
|
||||
"columnName": "core_registration",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "appRegistration",
|
||||
"columnName": "app_registration",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"autoGenerate": false
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "sensor_settings",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sensor_id` TEXT NOT NULL, `name` TEXT NOT NULL, `value` TEXT NOT NULL, `value_type` TEXT NOT NULL, `enabled` INTEGER NOT NULL, `entries` TEXT NOT NULL, PRIMARY KEY(`sensor_id`, `name`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "sensorId",
|
||||
"columnName": "sensor_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "value",
|
||||
"columnName": "value",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "valueType",
|
||||
"columnName": "value_type",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "enabled",
|
||||
"columnName": "enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "entries",
|
||||
"columnName": "entries",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"sensor_id",
|
||||
"name"
|
||||
],
|
||||
"autoGenerate": false
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "button_widgets",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `icon_id` INTEGER NOT NULL, `domain` TEXT NOT NULL, `service` TEXT NOT NULL, `service_data` TEXT NOT NULL, `label` TEXT, `background_type` TEXT NOT NULL DEFAULT 'DAYNIGHT', `text_color` TEXT, PRIMARY KEY(`id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "iconId",
|
||||
"columnName": "icon_id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "domain",
|
||||
"columnName": "domain",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "service",
|
||||
"columnName": "service",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "serviceData",
|
||||
"columnName": "service_data",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "label",
|
||||
"columnName": "label",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "backgroundType",
|
||||
"columnName": "background_type",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true,
|
||||
"defaultValue": "'DAYNIGHT'"
|
||||
},
|
||||
{
|
||||
"fieldPath": "textColor",
|
||||
"columnName": "text_color",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"autoGenerate": false
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "camera_widgets",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `entityId` TEXT NOT NULL, PRIMARY KEY(`id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "entityId",
|
||||
"columnName": "entityId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"autoGenerate": false
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "mediaplayctrls_widgets",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `entityId` TEXT NOT NULL, `label` TEXT, `showSkip` INTEGER NOT NULL, `showSeek` INTEGER NOT NULL, `showVolume` INTEGER NOT NULL, `showSource` INTEGER NOT NULL DEFAULT false, `background_type` TEXT NOT NULL DEFAULT 'DAYNIGHT', `text_color` TEXT, PRIMARY KEY(`id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "entityId",
|
||||
"columnName": "entityId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "label",
|
||||
"columnName": "label",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "showSkip",
|
||||
"columnName": "showSkip",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "showSeek",
|
||||
"columnName": "showSeek",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "showVolume",
|
||||
"columnName": "showVolume",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "showSource",
|
||||
"columnName": "showSource",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "backgroundType",
|
||||
"columnName": "background_type",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true,
|
||||
"defaultValue": "'DAYNIGHT'"
|
||||
},
|
||||
{
|
||||
"fieldPath": "textColor",
|
||||
"columnName": "text_color",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"autoGenerate": false
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "static_widget",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `entity_id` TEXT NOT NULL, `attribute_ids` TEXT, `label` TEXT, `text_size` REAL NOT NULL, `state_separator` TEXT NOT NULL, `attribute_separator` TEXT NOT NULL, `last_update` TEXT NOT NULL, `background_type` TEXT NOT NULL DEFAULT 'DAYNIGHT', `text_color` TEXT, PRIMARY KEY(`id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "entityId",
|
||||
"columnName": "entity_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "attributeIds",
|
||||
"columnName": "attribute_ids",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "label",
|
||||
"columnName": "label",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "textSize",
|
||||
"columnName": "text_size",
|
||||
"affinity": "REAL",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "stateSeparator",
|
||||
"columnName": "state_separator",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "attributeSeparator",
|
||||
"columnName": "attribute_separator",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastUpdate",
|
||||
"columnName": "last_update",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "backgroundType",
|
||||
"columnName": "background_type",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true,
|
||||
"defaultValue": "'DAYNIGHT'"
|
||||
},
|
||||
{
|
||||
"fieldPath": "textColor",
|
||||
"columnName": "text_color",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"autoGenerate": false
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "template_widgets",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `template` TEXT NOT NULL, `text_size` REAL NOT NULL DEFAULT 12.0, `last_update` TEXT NOT NULL, `background_type` TEXT NOT NULL DEFAULT 'DAYNIGHT', `text_color` TEXT, PRIMARY KEY(`id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "template",
|
||||
"columnName": "template",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "textSize",
|
||||
"columnName": "text_size",
|
||||
"affinity": "REAL",
|
||||
"notNull": true,
|
||||
"defaultValue": "12.0"
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastUpdate",
|
||||
"columnName": "last_update",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "backgroundType",
|
||||
"columnName": "background_type",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true,
|
||||
"defaultValue": "'DAYNIGHT'"
|
||||
},
|
||||
{
|
||||
"fieldPath": "textColor",
|
||||
"columnName": "text_color",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"autoGenerate": false
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "notification_history",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `received` INTEGER NOT NULL, `message` TEXT NOT NULL, `data` TEXT NOT NULL, `source` TEXT NOT NULL)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "received",
|
||||
"columnName": "received",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "message",
|
||||
"columnName": "message",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "data",
|
||||
"columnName": "data",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "source",
|
||||
"columnName": "source",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"autoGenerate": true
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "qs_tiles",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `tileId` TEXT NOT NULL, `icon_id` INTEGER, `entityId` TEXT NOT NULL, `label` TEXT NOT NULL, `subtitle` TEXT)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "tileId",
|
||||
"columnName": "tileId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "iconId",
|
||||
"columnName": "icon_id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "entityId",
|
||||
"columnName": "entityId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "label",
|
||||
"columnName": "label",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "subtitle",
|
||||
"columnName": "subtitle",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"autoGenerate": true
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "favorites",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `position` INTEGER NOT NULL, PRIMARY KEY(`id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "position",
|
||||
"columnName": "position",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"autoGenerate": false
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "settings",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `websocketSetting` TEXT NOT NULL, `sensorUpdateFrequency` TEXT NOT NULL, PRIMARY KEY(`id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "websocketSetting",
|
||||
"columnName": "websocketSetting",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "sensorUpdateFrequency",
|
||||
"columnName": "sensorUpdateFrequency",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"autoGenerate": false
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
}
|
||||
],
|
||||
"views": [],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'dbee144b17c2b37aba9ab9701ea0e715')"
|
||||
]
|
||||
}
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
package io.homeassistant.companion.android.common.data.integration
|
||||
|
||||
import io.homeassistant.companion.android.common.data.integration.impl.entities.RateLimitResponse
|
||||
import io.homeassistant.companion.android.common.data.websocket.impl.entities.GetConfigResponse
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface IntegrationRepository {
|
||||
|
@ -54,7 +55,9 @@ interface IntegrationRepository {
|
|||
suspend fun setShowShortcutTextEnabled(enabled: Boolean)
|
||||
|
||||
suspend fun getHomeAssistantVersion(): String
|
||||
suspend fun isHomeAssistantVersionAtLeast(year: Int, month: Int, release: Int): Boolean
|
||||
|
||||
suspend fun getConfig(): GetConfigResponse
|
||||
suspend fun getServices(): List<Service>?
|
||||
|
||||
suspend fun getEntities(): List<Entity<Any>>?
|
||||
|
|
|
@ -10,6 +10,6 @@ data class SensorRegistration<T>(
|
|||
val deviceClass: String? = null,
|
||||
val unitOfMeasurement: String? = null,
|
||||
val stateClass: String? = null,
|
||||
val entityCategory: String? = null
|
||||
|
||||
val entityCategory: String? = null,
|
||||
val disabled: Boolean
|
||||
)
|
||||
|
|
|
@ -30,6 +30,7 @@ import kotlinx.coroutines.flow.filter
|
|||
import kotlinx.coroutines.flow.map
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||
import org.json.JSONArray
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.regex.Pattern
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Named
|
||||
|
@ -475,6 +476,58 @@ class IntegrationRepositoryImpl @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
override suspend fun isHomeAssistantVersionAtLeast(
|
||||
year: Int,
|
||||
month: Int,
|
||||
release: Int
|
||||
): Boolean {
|
||||
if (!isRegistered()) return false
|
||||
|
||||
val version = getHomeAssistantVersion()
|
||||
val matches = VERSION_PATTERN.matcher(version)
|
||||
var result = false
|
||||
if (matches.find() && matches.matches()) {
|
||||
val coreYear = matches.group(1)?.toIntOrNull() ?: 0
|
||||
val coreMonth = matches.group(2)?.toIntOrNull() ?: 0
|
||||
val coreRelease = matches.group(3)?.toIntOrNull() ?: 0
|
||||
result =
|
||||
coreYear > year || (coreYear == year && (coreMonth > month || (coreMonth == month && coreRelease >= release)))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
override suspend fun getConfig(): GetConfigResponse {
|
||||
val getConfigRequest =
|
||||
IntegrationRequest(
|
||||
"get_config",
|
||||
null
|
||||
)
|
||||
var response: GetConfigResponse? = null
|
||||
var causeException: Exception? = null
|
||||
|
||||
for (it in urlRepository.getApiUrls()) {
|
||||
try {
|
||||
response = integrationService.getConfig(it.toHttpUrlOrNull()!!, getConfigRequest)
|
||||
} catch (e: Exception) {
|
||||
if (causeException == null) causeException = e
|
||||
// Ignore failure until we are out of URLS to try, but use the first exception as cause exception
|
||||
}
|
||||
|
||||
if (response != null) {
|
||||
// If we have a valid response, also update the cached version
|
||||
localStorage.putString(PREF_HA_VERSION, response.version)
|
||||
localStorage.putLong(
|
||||
PREF_CHECK_SENSOR_REGISTRATION_NEXT,
|
||||
System.currentTimeMillis() + TimeUnit.HOURS.toMillis(4)
|
||||
)
|
||||
return response
|
||||
}
|
||||
}
|
||||
|
||||
if (causeException != null) throw IntegrationException(causeException)
|
||||
else throw IntegrationException("Error calling integration request get_config")
|
||||
}
|
||||
|
||||
override suspend fun getServices(): List<Service>? {
|
||||
val response = webSocketRepository.getServices()
|
||||
|
||||
|
@ -538,28 +591,14 @@ class IntegrationRepositoryImpl @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private suspend fun canRegisterEntityCategoryStateClass(): Boolean {
|
||||
val version = getHomeAssistantVersion()
|
||||
val matches = VERSION_PATTERN.matcher(version)
|
||||
var canRegisterCategoryStateClass = false
|
||||
if (matches.find() && matches.matches()) {
|
||||
val year = Integer.parseInt(matches.group(1) ?: "0")
|
||||
val month = Integer.parseInt(matches.group(2) ?: "0")
|
||||
val release = Integer.parseInt(matches.group(3) ?: "0")
|
||||
canRegisterCategoryStateClass =
|
||||
year > 2021 || (year == 2021 && month >= 11 && release >= 0)
|
||||
}
|
||||
return canRegisterCategoryStateClass
|
||||
}
|
||||
|
||||
override suspend fun registerSensor(sensorRegistration: SensorRegistration<Any>) {
|
||||
|
||||
val canRegisterCategoryStateClass = canRegisterEntityCategoryStateClass()
|
||||
val canRegisterCategoryStateClass = isHomeAssistantVersionAtLeast(2021, 11, 0)
|
||||
val canRegisterEntityDisabledState = isHomeAssistantVersionAtLeast(2022, 6, 0)
|
||||
val integrationRequest = IntegrationRequest(
|
||||
"register_sensor",
|
||||
SensorRequest(
|
||||
sensorRegistration.uniqueId,
|
||||
sensorRegistration.state,
|
||||
if (canRegisterEntityDisabledState && sensorRegistration.disabled) null else sensorRegistration.state,
|
||||
sensorRegistration.type,
|
||||
sensorRegistration.icon,
|
||||
sensorRegistration.attributes,
|
||||
|
@ -567,7 +606,8 @@ class IntegrationRepositoryImpl @Inject constructor(
|
|||
sensorRegistration.deviceClass,
|
||||
sensorRegistration.unitOfMeasurement,
|
||||
if (canRegisterCategoryStateClass) sensorRegistration.stateClass else null,
|
||||
if (canRegisterCategoryStateClass) sensorRegistration.entityCategory else null
|
||||
if (canRegisterCategoryStateClass) sensorRegistration.entityCategory else null,
|
||||
if (canRegisterEntityDisabledState) sensorRegistration.disabled else null
|
||||
)
|
||||
)
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@ import io.homeassistant.companion.android.common.data.integration.impl.entities.
|
|||
import io.homeassistant.companion.android.common.data.integration.impl.entities.RateLimitRequest
|
||||
import io.homeassistant.companion.android.common.data.integration.impl.entities.RegisterDeviceRequest
|
||||
import io.homeassistant.companion.android.common.data.integration.impl.entities.RegisterDeviceResponse
|
||||
import io.homeassistant.companion.android.common.data.websocket.impl.entities.GetConfigResponse
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.ResponseBody
|
||||
import retrofit2.Response
|
||||
|
@ -49,6 +50,12 @@ interface IntegrationService {
|
|||
@Body request: IntegrationRequest
|
||||
): Array<EntityResponse<ZoneAttributes>>
|
||||
|
||||
@POST
|
||||
suspend fun getConfig(
|
||||
@Url url: HttpUrl,
|
||||
@Body request: IntegrationRequest
|
||||
): GetConfigResponse
|
||||
|
||||
@POST
|
||||
suspend fun getRateLimit(
|
||||
@Url url: String,
|
||||
|
|
|
@ -13,5 +13,6 @@ data class SensorRequest<T>(
|
|||
val deviceClass: String? = null,
|
||||
val unitOfMeasurement: String? = null,
|
||||
val stateClass: String? = null,
|
||||
val entityCategory: String? = null
|
||||
val entityCategory: String? = null,
|
||||
val disabled: Boolean? = null
|
||||
)
|
||||
|
|
|
@ -12,4 +12,5 @@ data class GetConfigResponse(
|
|||
val timeZone: String,
|
||||
val components: List<String>,
|
||||
val version: String,
|
||||
val entities: Map<String, Map<String, Any>>? // only on core >= 2022.6 when using webhook
|
||||
)
|
||||
|
|
|
@ -1,14 +1,23 @@
|
|||
package io.homeassistant.companion.android.common.sensors
|
||||
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.content.res.Configuration
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.content.getSystemService
|
||||
import io.homeassistant.companion.android.common.R
|
||||
import io.homeassistant.companion.android.common.data.integration.IntegrationRepository
|
||||
import io.homeassistant.companion.android.common.data.integration.SensorRegistration
|
||||
import io.homeassistant.companion.android.common.util.sensorCoreSyncChannel
|
||||
import io.homeassistant.companion.android.database.AppDatabase
|
||||
import io.homeassistant.companion.android.database.sensor.SensorWithAttributes
|
||||
import io.homeassistant.companion.android.database.settings.SensorUpdateFrequencySetting
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
@ -52,6 +61,8 @@ abstract class SensorReceiverBase : BroadcastReceiver() {
|
|||
|
||||
protected abstract val skippableActions: Map<String, String>
|
||||
|
||||
protected abstract fun getSensorSettingsIntent(context: Context, id: String): Intent?
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
Log.d(tag, "Received intent: ${intent.action}")
|
||||
if (skippableActions.containsKey(intent.action)) {
|
||||
|
@ -132,6 +143,21 @@ abstract class SensorReceiverBase : BroadcastReceiver() {
|
|||
}
|
||||
|
||||
val currentHAversion = integrationUseCase.getHomeAssistantVersion()
|
||||
val supportsDisabledSensors = integrationUseCase.isHomeAssistantVersionAtLeast(2022, 6, 0)
|
||||
val coreSensorStatus: Map<String, Boolean> = if (supportsDisabledSensors) {
|
||||
try {
|
||||
val config = integrationUseCase.getConfig().entities
|
||||
config
|
||||
?.filter { it.value["disabled"] != null }
|
||||
?.mapValues { !(it.value["disabled"] as Boolean) }
|
||||
.orEmpty() // Map to sensor id -> enabled
|
||||
} catch (e: Exception) {
|
||||
Log.e(tag, "Error while getting core config to sync sensor status", e)
|
||||
emptyMap()
|
||||
}
|
||||
} else {
|
||||
emptyMap()
|
||||
}
|
||||
|
||||
managers.forEach { manager ->
|
||||
|
||||
|
@ -140,39 +166,122 @@ abstract class SensorReceiverBase : BroadcastReceiver() {
|
|||
if (manager is LocationSensorManagerBase)
|
||||
manager.integrationUseCase = integrationUseCase
|
||||
|
||||
try {
|
||||
manager.requestSensorUpdate(context, intent)
|
||||
} catch (e: Exception) {
|
||||
Log.e(tag, "Issue requesting updates for ${context.getString(manager.name)}", e)
|
||||
val hasSensor = manager.hasSensor(context)
|
||||
if (hasSensor) {
|
||||
try {
|
||||
manager.requestSensorUpdate(context, intent)
|
||||
} catch (e: Exception) {
|
||||
Log.e(tag, "Issue requesting updates for ${context.getString(manager.name)}", e)
|
||||
}
|
||||
}
|
||||
manager.getAvailableSensors(context).forEach { basicSensor ->
|
||||
manager.getAvailableSensors(context).forEach sensorForEach@{ basicSensor ->
|
||||
val fullSensor = sensorDao.getFull(basicSensor.id)
|
||||
val sensor = fullSensor?.sensor
|
||||
val sensorCoreRegistration = sensor?.coreRegistration
|
||||
val sensorAppRegistration = sensor?.appRegistration
|
||||
val sensor = fullSensor?.sensor ?: return@sensorForEach
|
||||
val sensorCoreEnabled = coreSensorStatus[basicSensor.id]
|
||||
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 canBeRegistered =
|
||||
hasSensor &&
|
||||
basicSensor.type.isNotBlank() &&
|
||||
basicSensor.statelessIcon.isNotBlank()
|
||||
|
||||
// Register sensor and/or update the sensor enabled state. Priority is:
|
||||
// 1. There is a new sensor or change in enabled state according to the app
|
||||
// 2. There is a change in enabled state according to core (user changed in frontend)
|
||||
// 3. There is no change in enabled state, but app/core version has changed
|
||||
if (
|
||||
canBeRegistered &&
|
||||
(
|
||||
(sensor.registered == null && (sensor.enabled || supportsDisabledSensors)) ||
|
||||
(sensor.enabled != sensor.registered && supportsDisabledSensors)
|
||||
)
|
||||
) {
|
||||
val reg = fullSensor.toSensorRegistration()
|
||||
val config = Configuration(context.resources.configuration)
|
||||
config.setLocale(Locale("en"))
|
||||
reg.name = context.createConfigurationContext(config).resources.getString(basicSensor.name)
|
||||
|
||||
// 1. (Re-)register sensors with core when they can be registered and:
|
||||
// - sensor isn't registered, but is enabled or on core >=2022.6
|
||||
// - sensor enabled has changed from registered enabled state on core >=2022.6
|
||||
try {
|
||||
integrationUseCase.registerSensor(reg)
|
||||
sensor.registered = true
|
||||
registerSensor(context, integrationUseCase, fullSensor, basicSensor)
|
||||
sensor.registered = sensor.enabled
|
||||
sensor.coreRegistration = currentHAversion
|
||||
sensor.appRegistration = currentAppVersion
|
||||
sensorDao.update(sensor)
|
||||
} catch (e: Exception) {
|
||||
Log.e(tag, "Issue registering sensor: ${reg.uniqueId}", e)
|
||||
Log.e(tag, "Issue registering sensor ${basicSensor.id}", e)
|
||||
}
|
||||
} else if (
|
||||
canBeRegistered &&
|
||||
supportsDisabledSensors &&
|
||||
sensorCoreEnabled != null &&
|
||||
sensorCoreEnabled != sensor.registered
|
||||
) {
|
||||
// 2. Try updating the sensor enabled state to match core state when it's different from
|
||||
// the app, if the sensor can be registered and on core >= 2022.6
|
||||
try {
|
||||
if (sensorCoreEnabled) { // App disabled, should enable
|
||||
if (manager.checkPermission(context.applicationContext, basicSensor.id)) {
|
||||
sensor.enabled = true
|
||||
sensor.registered = true
|
||||
} else {
|
||||
// Can't enable due to missing permission(s), 'override' core and notify user
|
||||
registerSensor(context, integrationUseCase, fullSensor, basicSensor)
|
||||
|
||||
context.getSystemService<NotificationManager>()?.let { notificationManager ->
|
||||
createNotificationChannel(context)
|
||||
val notificationId = "$sensorCoreSyncChannel-${basicSensor.id}".hashCode()
|
||||
val notificationIntent = getSensorSettingsIntent(context, basicSensor.id)?.let {
|
||||
PendingIntent.getActivity(context, notificationId, it, PendingIntent.FLAG_IMMUTABLE)
|
||||
}
|
||||
val notification = NotificationCompat.Builder(context, sensorCoreSyncChannel)
|
||||
.setSmallIcon(R.drawable.ic_stat_ic_notification)
|
||||
.setContentTitle(context.getString(basicSensor.name))
|
||||
.setContentText(context.getString(R.string.sensor_worker_sync_missing_permissions))
|
||||
.setContentIntent(notificationIntent)
|
||||
.setAutoCancel(true)
|
||||
.build()
|
||||
notificationManager.notify(notificationId, notification)
|
||||
}
|
||||
}
|
||||
} else { // App enabled, should disable
|
||||
sensor.enabled = false
|
||||
sensor.registered = false
|
||||
}
|
||||
|
||||
sensor.coreRegistration = currentHAversion
|
||||
sensor.appRegistration = currentAppVersion
|
||||
sensorDao.update(sensor)
|
||||
} catch (e: Exception) {
|
||||
Log.e(tag, "Issue enabling/disabling sensor ${basicSensor.id}", e)
|
||||
}
|
||||
} else if (
|
||||
canBeRegistered &&
|
||||
(sensor.enabled || supportsDisabledSensors) &&
|
||||
(currentAppVersion != sensorAppRegistration || currentHAversion != sensorCoreRegistration)
|
||||
) {
|
||||
// 3. Re-register sensors with core when they can be registered and are enabled or on
|
||||
// core >= 2022.6, and app or core version change is detected
|
||||
try {
|
||||
registerSensor(context, integrationUseCase, fullSensor, basicSensor)
|
||||
sensor.registered = sensor.enabled
|
||||
sensor.coreRegistration = currentHAversion
|
||||
sensor.appRegistration = currentAppVersion
|
||||
sensorDao.update(sensor)
|
||||
} catch (e: Exception) {
|
||||
Log.e(tag, "Issue re-registering sensor ${basicSensor.id}", e)
|
||||
}
|
||||
} else if (
|
||||
supportsDisabledSensors &&
|
||||
sensor.enabled != sensor.registered &&
|
||||
(!hasSensor || basicSensor.type.isBlank())
|
||||
) {
|
||||
// Unsupported sensors or sensors without a type (= location sensors) in the database shouldn't/can't
|
||||
// be registered but they will have a 'registered' state. Manually update when on core >=2022.6 by
|
||||
// setting it to the enabled state to stop the app from continuing to do updates because of these sensors.
|
||||
sensor.registered = sensor.enabled
|
||||
sensorDao.update(sensor)
|
||||
}
|
||||
if (sensor?.enabled == true && sensor.registered && sensor.state != sensor.lastSentState) {
|
||||
enabledRegistrations.add(fullSensor.toSensorRegistration())
|
||||
if (canBeRegistered && sensor.enabled && sensor.registered != null && sensor.state != sensor.lastSentState) {
|
||||
enabledRegistrations.add(fullSensor.toSensorRegistration(basicSensor))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -193,7 +302,7 @@ abstract class SensorReceiverBase : BroadcastReceiver() {
|
|||
enabledRegistrations.forEach {
|
||||
val sensor = sensorDao.get(it.uniqueId)
|
||||
if (sensor != null) {
|
||||
sensor.registered = false
|
||||
sensor.registered = null
|
||||
sensor.lastSentState = ""
|
||||
sensorDao.update(sensor)
|
||||
}
|
||||
|
@ -201,4 +310,32 @@ abstract class SensorReceiverBase : BroadcastReceiver() {
|
|||
}
|
||||
} else Log.d(tag, "Nothing to update")
|
||||
}
|
||||
|
||||
private suspend fun registerSensor(
|
||||
context: Context,
|
||||
integrationUseCase: IntegrationRepository,
|
||||
fullSensor: SensorWithAttributes,
|
||||
basicSensor: SensorManager.BasicSensor
|
||||
) {
|
||||
val reg = fullSensor.toSensorRegistration(basicSensor)
|
||||
val config = Configuration(context.resources.configuration)
|
||||
config.setLocale(Locale("en"))
|
||||
reg.name = context.createConfigurationContext(config).resources.getString(basicSensor.name)
|
||||
|
||||
integrationUseCase.registerSensor(reg)
|
||||
}
|
||||
|
||||
private fun createNotificationChannel(context: Context) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val notificationManager = context.getSystemService<NotificationManager>() ?: return
|
||||
var notificationChannel =
|
||||
notificationManager.getNotificationChannel(sensorCoreSyncChannel)
|
||||
if (notificationChannel == null) {
|
||||
notificationChannel = NotificationChannel(
|
||||
sensorCoreSyncChannel, sensorCoreSyncChannel, NotificationManager.IMPORTANCE_DEFAULT
|
||||
)
|
||||
notificationManager.createNotificationChannel(notificationChannel)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -35,7 +35,9 @@ abstract class SensorWorkerBase(
|
|||
override suspend fun doWork(): Result = withContext(Dispatchers.IO) {
|
||||
val sensorDao = AppDatabase.getInstance(applicationContext).sensorDao()
|
||||
val enabledSensorCount = sensorDao.getEnabledCount() ?: 0
|
||||
if (enabledSensorCount > 0) {
|
||||
val currentCoreSupportsDisabledSensors = integrationUseCase.isHomeAssistantVersionAtLeast(2022, 6, 0)
|
||||
val enabledNotInSyncSensorCount = sensorDao.getEnabledNotInSyncCount() ?: 0
|
||||
if (enabledSensorCount > 0 || (currentCoreSupportsDisabledSensors && enabledNotInSyncSensorCount > 0)) {
|
||||
Log.d(TAG, "Updating all Sensors.")
|
||||
createNotificationChannel()
|
||||
val notification = NotificationCompat.Builder(applicationContext, sensorWorkerChannel)
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package io.homeassistant.companion.android.common.util
|
||||
|
||||
const val sensorWorkerChannel = "Sensor Worker"
|
||||
const val sensorCoreSyncChannel = "Sensor Sync"
|
||||
const val websocketChannel = "Websocket"
|
||||
const val highAccuracyChannel = "High accuracy location"
|
||||
const val databaseChannel = "App Database"
|
||||
|
@ -9,6 +10,7 @@ const val generalChannel = "general"
|
|||
|
||||
val appCreatedChannels = listOf(
|
||||
sensorWorkerChannel,
|
||||
sensorCoreSyncChannel,
|
||||
websocketChannel,
|
||||
highAccuracyChannel,
|
||||
databaseChannel,
|
||||
|
|
|
@ -18,6 +18,7 @@ import androidx.room.OnConflictStrategy
|
|||
import androidx.room.Room
|
||||
import androidx.room.RoomDatabase
|
||||
import androidx.room.TypeConverters
|
||||
import androidx.room.migration.AutoMigrationSpec
|
||||
import androidx.room.migration.Migration
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
import io.homeassistant.companion.android.common.data.integration.IntegrationRepository
|
||||
|
@ -70,11 +71,12 @@ import io.homeassistant.companion.android.common.R as commonR
|
|||
Favorites::class,
|
||||
Setting::class
|
||||
],
|
||||
version = 27,
|
||||
version = 28,
|
||||
autoMigrations = [
|
||||
AutoMigration(from = 24, to = 25),
|
||||
AutoMigration(from = 25, to = 26),
|
||||
AutoMigration(from = 26, to = 27)
|
||||
AutoMigration(from = 26, to = 27),
|
||||
AutoMigration(from = 27, to = 28, spec = AppDatabase.Companion.Migration27to28::class)
|
||||
]
|
||||
)
|
||||
@TypeConverters(
|
||||
|
@ -501,6 +503,14 @@ abstract class AppDatabase : RoomDatabase() {
|
|||
}
|
||||
}
|
||||
|
||||
class Migration27to28 : AutoMigrationSpec {
|
||||
override fun onPostMigrate(db: SupportSQLiteDatabase) {
|
||||
// Update 'registered' in the sensors table to set the value to null instead of the previous default of 0
|
||||
// This will force an update to indicate whether a sensor is not registered (null) or registered as disabled (0)
|
||||
db.execSQL("UPDATE `sensors` SET `registered` = NULL")
|
||||
}
|
||||
}
|
||||
|
||||
private fun createNotificationChannel() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val notificationManager = appContext.getSystemService<NotificationManager>()!!
|
||||
|
|
|
@ -11,8 +11,8 @@ data class Sensor(
|
|||
var id: String,
|
||||
@ColumnInfo(name = "enabled")
|
||||
var enabled: Boolean,
|
||||
@ColumnInfo(name = "registered")
|
||||
var registered: Boolean,
|
||||
@ColumnInfo(name = "registered", defaultValue = "NULL")
|
||||
var registered: Boolean? = null,
|
||||
@ColumnInfo(name = "state")
|
||||
var state: String,
|
||||
@ColumnInfo(name = "last_sent_state")
|
||||
|
|
|
@ -75,6 +75,9 @@ interface SensorDao {
|
|||
@Query("SELECT COUNT(id) FROM sensors WHERE enabled = 1")
|
||||
suspend fun getEnabledCount(): Int?
|
||||
|
||||
@Query("SELECT COUNT(id) FROM sensors WHERE enabled != registered")
|
||||
suspend fun getEnabledNotInSyncCount(): Int?
|
||||
|
||||
@Transaction
|
||||
suspend fun setSensorsEnabled(sensorIds: List<String>, enabled: Boolean) {
|
||||
coroutineScope {
|
||||
|
@ -84,7 +87,7 @@ interface SensorDao {
|
|||
if (sensorEntity != null) {
|
||||
update(sensorEntity.copy(enabled = enabled, lastSentState = ""))
|
||||
} else {
|
||||
add(Sensor(sensorId, enabled, registered = false, state = ""))
|
||||
add(Sensor(sensorId, enabled, state = ""))
|
||||
}
|
||||
}
|
||||
}.awaitAll()
|
||||
|
@ -97,7 +100,7 @@ interface SensorDao {
|
|||
|
||||
if (sensor == null) {
|
||||
// If we haven't created the entity yet do so and default to enabled if required
|
||||
sensor = Sensor(sensorId, enabled = permission && enabledByDefault, registered = false, state = "")
|
||||
sensor = Sensor(sensorId, enabled = permission && enabledByDefault, state = "")
|
||||
add(sensor)
|
||||
} else if (sensor.enabled && !permission) {
|
||||
// If we don't have permission but we are still enabled then we aren't really enabled.
|
||||
|
|
|
@ -6,6 +6,7 @@ import com.fasterxml.jackson.databind.ObjectMapper
|
|||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||
import com.fasterxml.jackson.module.kotlin.readValue
|
||||
import io.homeassistant.companion.android.common.data.integration.SensorRegistration
|
||||
import io.homeassistant.companion.android.common.sensors.SensorManager
|
||||
|
||||
data class SensorWithAttributes(
|
||||
@Embedded
|
||||
|
@ -16,7 +17,7 @@ data class SensorWithAttributes(
|
|||
)
|
||||
val attributes: List<Attribute>
|
||||
) {
|
||||
fun toSensorRegistration(): SensorRegistration<Any> {
|
||||
fun toSensorRegistration(basicSensor: SensorManager.BasicSensor): SensorRegistration<Any> {
|
||||
var objectMapper: ObjectMapper? = null
|
||||
val attributes = attributes.map {
|
||||
val attributeValue = when (it.valueType) {
|
||||
|
@ -52,14 +53,15 @@ data class SensorWithAttributes(
|
|||
return SensorRegistration(
|
||||
sensor.id,
|
||||
state,
|
||||
sensor.type,
|
||||
sensor.icon,
|
||||
sensor.type.ifBlank { basicSensor.type },
|
||||
sensor.icon.ifBlank { basicSensor.statelessIcon },
|
||||
attributes,
|
||||
sensor.name,
|
||||
sensor.deviceClass,
|
||||
sensor.unitOfMeasurement,
|
||||
sensor.stateClass,
|
||||
sensor.entityCategory
|
||||
sensor.deviceClass?.ifBlank { null } ?: basicSensor.deviceClass,
|
||||
sensor.unitOfMeasurement?.ifBlank { null } ?: basicSensor.unitOfMeasurement,
|
||||
sensor.stateClass?.ifBlank { null } ?: basicSensor.stateClass,
|
||||
sensor.entityCategory?.ifBlank { null } ?: basicSensor.entityCategory,
|
||||
!sensor.enabled
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -817,6 +817,7 @@
|
|||
<string name="sensor_update_frequency_fast_always">Fast Always\n\nSensors will update every minute, always.</string>
|
||||
<string name="sensor_update_notification">In order for the app to send sensor updates in the background a notification will be created during the update. You may use the button below to manage the appearance of this notification. It is recommended to minimize the notification to hide the icon.</string>
|
||||
<string name="sensor_worker_notification_channel">Manage Sensor Worker Notification</string>
|
||||
<string name="sensor_worker_sync_missing_permissions">To enable this sensor, turn it on in the app settings</string>
|
||||
<string name="sensor_update_frequency_summary">Change the frequency of updates for sensors that do not update instantly.</string>
|
||||
<string name="sensor_update_type_chip_intent">Updates instantly</string>
|
||||
<string name="sensor_update_type_chip_worker_fast_always">Updates every minute</string>
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
package io.homeassistant.companion.android.sensors
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import io.homeassistant.companion.android.BuildConfig
|
||||
import io.homeassistant.companion.android.common.sensors.BatterySensorManager
|
||||
|
@ -32,4 +34,6 @@ class SensorReceiver : SensorReceiverBase() {
|
|||
// Suppress Lint because we only register for the receiver if the android version matches the intent
|
||||
@SuppressLint("InlinedApi")
|
||||
override val skippableActions = mapOf<String, String>()
|
||||
|
||||
override fun getSensorSettingsIntent(context: Context, id: String): Intent? = null
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue