diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index f32435df433..cfc804f9089 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -143,6 +143,19 @@ android:resource="@xml/entity_widget_info" /> + + + + + + + + + + @@ -206,6 +219,13 @@ + + + + + diff --git a/app/src/main/java/io/homeassistant/companion/android/settings/widgets/ManageWidgetsViewModel.kt b/app/src/main/java/io/homeassistant/companion/android/settings/widgets/ManageWidgetsViewModel.kt index 87917e40d88..cc1046fc17f 100755 --- a/app/src/main/java/io/homeassistant/companion/android/settings/widgets/ManageWidgetsViewModel.kt +++ b/app/src/main/java/io/homeassistant/companion/android/settings/widgets/ManageWidgetsViewModel.kt @@ -13,6 +13,7 @@ import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import io.homeassistant.companion.android.database.widget.ButtonWidgetDao import io.homeassistant.companion.android.database.widget.CameraWidgetDao +import io.homeassistant.companion.android.database.widget.HistoryWidgetDao import io.homeassistant.companion.android.database.widget.MediaPlayerControlsWidgetDao import io.homeassistant.companion.android.database.widget.StaticWidgetDao import io.homeassistant.companion.android.database.widget.TemplateWidgetDao @@ -24,6 +25,7 @@ import kotlinx.coroutines.launch class ManageWidgetsViewModel @Inject constructor( buttonWidgetDao: ButtonWidgetDao, cameraWidgetDao: CameraWidgetDao, + historyWidgetDao: HistoryWidgetDao, staticWidgetDao: StaticWidgetDao, mediaPlayerControlsWidgetDao: MediaPlayerControlsWidgetDao, templateWidgetDao: TemplateWidgetDao, @@ -38,6 +40,7 @@ class ManageWidgetsViewModel @Inject constructor( val buttonWidgetList = buttonWidgetDao.getAllFlow().collectAsState() val cameraWidgetList = cameraWidgetDao.getAllFlow().collectAsState() + val historyWidgetList = historyWidgetDao.getAllFlow().collectAsState() val staticWidgetList = staticWidgetDao.getAllFlow().collectAsState() val mediaWidgetList = mediaPlayerControlsWidgetDao.getAllFlow().collectAsState() val templateWidgetList = templateWidgetDao.getAllFlow().collectAsState() diff --git a/app/src/main/java/io/homeassistant/companion/android/settings/widgets/views/ManageWidgetsView.kt b/app/src/main/java/io/homeassistant/companion/android/settings/widgets/views/ManageWidgetsView.kt index d1c6575d4b9..9c20874b649 100755 --- a/app/src/main/java/io/homeassistant/companion/android/settings/widgets/views/ManageWidgetsView.kt +++ b/app/src/main/java/io/homeassistant/companion/android/settings/widgets/views/ManageWidgetsView.kt @@ -42,6 +42,7 @@ import io.homeassistant.companion.android.util.compose.MdcAlertDialog import io.homeassistant.companion.android.widgets.button.ButtonWidgetConfigureActivity import io.homeassistant.companion.android.widgets.camera.CameraWidgetConfigureActivity import io.homeassistant.companion.android.widgets.entity.EntityWidgetConfigureActivity +import io.homeassistant.companion.android.widgets.history.HistoryWidgetConfigureActivity import io.homeassistant.companion.android.widgets.mediaplayer.MediaPlayerControlsWidgetConfigureActivity import io.homeassistant.companion.android.widgets.template.TemplateWidgetConfigureActivity @@ -50,7 +51,8 @@ enum class WidgetType(val widgetIcon: IIcon) { CAMERA(CommunityMaterial.Icon.cmd_camera_image), STATE(CommunityMaterial.Icon3.cmd_shape), MEDIA(CommunityMaterial.Icon3.cmd_play_box_multiple), - TEMPLATE(CommunityMaterial.Icon.cmd_code_braces); + TEMPLATE(CommunityMaterial.Icon.cmd_code_braces), + HISTORY(CommunityMaterial.Icon3.cmd_sun_clock); fun configureActivity() = when (this) { BUTTON -> ButtonWidgetConfigureActivity::class.java @@ -58,6 +60,7 @@ enum class WidgetType(val widgetIcon: IIcon) { MEDIA -> MediaPlayerControlsWidgetConfigureActivity::class.java STATE -> EntityWidgetConfigureActivity::class.java TEMPLATE -> TemplateWidgetConfigureActivity::class.java + HISTORY -> HistoryWidgetConfigureActivity::class.java } } @@ -81,6 +84,7 @@ fun ManageWidgetsView( val availableWidgets = listOf( stringResource(R.string.widget_button_image_description) to WidgetType.BUTTON, stringResource(R.string.widget_camera_description) to WidgetType.CAMERA, + stringResource(R.string.widget_history_description) to WidgetType.HISTORY, stringResource(R.string.widget_static_image_description) to WidgetType.STATE, stringResource(R.string.widget_media_player_description) to WidgetType.MEDIA, stringResource(R.string.template_widget) to WidgetType.TEMPLATE @@ -110,7 +114,7 @@ fun ManageWidgetsView( ) { if (viewModel.buttonWidgetList.value.isEmpty() && viewModel.staticWidgetList.value.isEmpty() && viewModel.mediaWidgetList.value.isEmpty() && viewModel.templateWidgetList.value.isEmpty() && - viewModel.cameraWidgetList.value.isEmpty() + viewModel.cameraWidgetList.value.isEmpty() && viewModel.historyWidgetList.value.isEmpty() ) { item { EmptyState( @@ -135,6 +139,12 @@ fun ManageWidgetsView( title = R.string.camera_widgets, widgetLabel = { item -> item.entityId } ) + widgetItems( + viewModel.historyWidgetList.value, + widgetType = WidgetType.HISTORY, + title = R.string.history_widgets, + widgetLabel = { item -> item.entityId } + ) widgetItems( viewModel.staticWidgetList.value, widgetType = WidgetType.STATE, diff --git a/app/src/main/java/io/homeassistant/companion/android/widgets/history/HistoryWidget.kt b/app/src/main/java/io/homeassistant/companion/android/widgets/history/HistoryWidget.kt new file mode 100644 index 00000000000..3c443b3613c --- /dev/null +++ b/app/src/main/java/io/homeassistant/companion/android/widgets/history/HistoryWidget.kt @@ -0,0 +1,256 @@ +package io.homeassistant.companion.android.widgets.history + +import android.app.PendingIntent +import android.appwidget.AppWidgetManager +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.graphics.Color +import android.os.Bundle +import android.util.Log +import android.util.TypedValue +import android.view.View +import android.widget.RemoteViews +import androidx.core.content.ContextCompat +import androidx.core.graphics.toColorInt +import androidx.core.os.BundleCompat +import com.google.android.material.color.DynamicColors +import dagger.hilt.android.AndroidEntryPoint +import io.homeassistant.companion.android.R +import io.homeassistant.companion.android.common.R as commonR +import io.homeassistant.companion.android.common.data.integration.Entity +import io.homeassistant.companion.android.common.data.integration.canSupportPrecision +import io.homeassistant.companion.android.common.data.integration.friendlyName +import io.homeassistant.companion.android.common.data.integration.friendlyState +import io.homeassistant.companion.android.common.data.websocket.impl.entities.EntityRegistryOptions +import io.homeassistant.companion.android.database.widget.HistoryWidgetDao +import io.homeassistant.companion.android.database.widget.HistoryWidgetEntity +import io.homeassistant.companion.android.database.widget.WidgetBackgroundType +import io.homeassistant.companion.android.util.getAttribute +import io.homeassistant.companion.android.widgets.BaseWidgetProvider +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter +import javax.inject.Inject +import kotlinx.coroutines.launch + +@AndroidEntryPoint +class HistoryWidget : BaseWidgetProvider() { + + companion object { + private const val TAG = "HistoryWidget" + + internal const val EXTRA_SERVER_ID = "EXTRA_SERVER_ID" + internal const val EXTRA_ENTITY_ID = "EXTRA_ENTITY_ID" + internal const val EXTRA_LABEL = "EXTRA_LABEL" + internal const val EXTRA_TEXT_SIZE = "EXTRA_TEXT_SIZE" + internal const val EXTRA_BACKGROUND_TYPE = "EXTRA_BACKGROUND_TYPE" + internal const val EXTRA_TEXT_COLOR = "EXTRA_TEXT_COLOR" + internal const val DEFAULT_TEXT_SIZE = 30F + internal const val DEFAULT_ENTITY_ID_SEPARATOR = "," + + private data class ResolvedText(val text: CharSequence?, val exception: Boolean = false) + } + + @Inject + lateinit var historyWidgetDao: HistoryWidgetDao + + override fun getWidgetProvider(context: Context): ComponentName = + ComponentName(context, HistoryWidget::class.java) + + override suspend fun getWidgetRemoteViews(context: Context, appWidgetId: Int, suggestedEntity: Entity>?): RemoteViews { + val widget = historyWidgetDao.get(appWidgetId) + + val intent = Intent(context, HistoryWidget::class.java).apply { + action = UPDATE_VIEW + putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId) + } + + val useDynamicColors = widget?.backgroundType == WidgetBackgroundType.DYNAMICCOLOR && DynamicColors.isDynamicColorAvailable() + val views = RemoteViews(context.packageName, if (useDynamicColors) R.layout.widget_history_wrapper_dynamiccolor else R.layout.widget_history_wrapper_default).apply { + if (widget != null) { + val serverId = widget.serverId + val entityIds: String = widget.entityId + val label: String? = widget.label + val textSize: Float = widget.textSize + + // Theming + if (widget.backgroundType == WidgetBackgroundType.TRANSPARENT) { + var textColor = context.getAttribute(R.attr.colorWidgetOnBackground, ContextCompat.getColor(context, commonR.color.colorWidgetButtonLabel)) + widget.textColor?.let { textColor = it.toColorInt() } + + setInt(R.id.widgetLayout, "setBackgroundColor", Color.TRANSPARENT) + setTextColor(R.id.widgetText, textColor) + setTextColor(R.id.widgetLabel, textColor) + } + + // Content + setViewVisibility( + R.id.widgetTextLayout, + View.VISIBLE + ) + setViewVisibility( + R.id.widgetProgressBar, + View.INVISIBLE + ) + val resolvedText = resolveTextToShow( + context, + serverId, + entityIds, + suggestedEntity, + appWidgetId + ) + setTextViewTextSize( + R.id.widgetText, + TypedValue.COMPLEX_UNIT_SP, + textSize + ) + setTextViewText( + R.id.widgetText, + resolvedText.text + ) + setTextViewText( + R.id.widgetLabel, + label ?: entityIds + ) + setViewVisibility( + R.id.widgetStaticError, + if (resolvedText.exception) View.VISIBLE else View.GONE + ) + setOnClickPendingIntent( + R.id.widgetTextLayout, + PendingIntent.getBroadcast( + context, + appWidgetId, + intent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) + ) + } else { + setTextViewText(R.id.widgetText, "") + setTextViewText(R.id.widgetLabel, "") + } + } + + return views + } + + override suspend fun getAllWidgetIdsWithEntities(context: Context): Map>> = + historyWidgetDao.getAll().associate { it.id to (it.serverId to listOf(it.entityId)) } + + private suspend fun resolveTextToShow( + context: Context, + serverId: Int, + entityIds: String?, + suggestedEntity: Entity>?, + appWidgetId: Int + ): ResolvedText { + var entitiesStatesList: List>>>? = null + var entityCaughtException = false + try { + if (suggestedEntity != null) { + entitiesStatesList = serverManager.integrationRepository(serverId).getHistory(listOf(suggestedEntity.entityId)) + } else { + entityIds?.let { ids -> entitiesStatesList = serverManager.integrationRepository(serverId).getHistory(ids.split(DEFAULT_ENTITY_ID_SEPARATOR)) } + } + } catch (e: Exception) { + Log.e(TAG, "Unable to fetch entity", e) + entityCaughtException = true + } + + val entityOptionsList = mutableMapOf() + + entitiesStatesList?.forEach { entityStateList -> + if (entityStateList.all { it.canSupportPrecision() && serverManager.getServer(serverId)?.version?.isAtLeast(2023, 3) == true }) { + entityOptionsList[entityStateList.first().entityId] = serverManager.webSocketRepository(serverId).getEntityRegistryFor(entityStateList.first().entityId)?.options + } + } + + try { + val textBuilder = StringBuilder() + entitiesStatesList?.forEachIndexed { index, entityStatesList -> + if (index > 0) textBuilder.append("\n---\n") + textBuilder.append(getLastUpdateFromEntityStatesList(entityStatesList, context, entityOptionsList[entityStatesList.first().entityId])) + } + historyWidgetDao.updateWidgetLastUpdate( + appWidgetId, + textBuilder.toString() + ) + return ResolvedText(historyWidgetDao.get(appWidgetId)?.lastUpdate, entityCaughtException) + } catch (e: Exception) { + Log.e(TAG, "Unable to fetch entity state and attributes", e) + return ResolvedText(historyWidgetDao.get(appWidgetId)?.lastUpdate, true) + } + } + + override fun saveEntityConfiguration(context: Context, extras: Bundle?, appWidgetId: Int) { + if (extras == null) return + + val serverId = if (extras.containsKey(EXTRA_SERVER_ID)) extras.getInt(EXTRA_SERVER_ID) else null + val entityIds: String? = extras.getString(EXTRA_ENTITY_ID) + val labelSelection: String? = extras.getString(EXTRA_LABEL) + val textSizeSelection: String? = extras.getString(EXTRA_TEXT_SIZE) + val backgroundTypeSelection = BundleCompat.getSerializable(extras, EXTRA_BACKGROUND_TYPE, WidgetBackgroundType::class.java) + ?: WidgetBackgroundType.DAYNIGHT + val textColorSelection: String? = extras.getString(EXTRA_TEXT_COLOR) + + if (serverId == null || entityIds == null) { + Log.e(TAG, "Did not receive complete service call data") + return + } + + widgetScope?.launch { + Log.d( + TAG, + "Saving entity state config data:" + System.lineSeparator() + + "entity id: " + entityIds + System.lineSeparator() + ) + historyWidgetDao.add( + HistoryWidgetEntity( + appWidgetId, + serverId, + entityIds, + labelSelection, + textSizeSelection?.toFloatOrNull() ?: DEFAULT_TEXT_SIZE, + historyWidgetDao.get(appWidgetId)?.lastUpdate ?: "", + backgroundTypeSelection, + textColorSelection + ) + ) + + onUpdate(context, AppWidgetManager.getInstance(context), intArrayOf(appWidgetId)) + } + } + + override suspend fun onEntityStateChanged(context: Context, appWidgetId: Int, entity: Entity<*>) { + widgetScope?.launch { + val views = getWidgetRemoteViews(context, appWidgetId, entity as Entity>) + AppWidgetManager.getInstance(context).updateAppWidget(appWidgetId, views) + } + } + + override fun onDeleted(context: Context, appWidgetIds: IntArray) { + widgetScope?.launch { + historyWidgetDao.deleteAll(appWidgetIds) + appWidgetIds.forEach { removeSubscription(it) } + } + } + + private fun getLastUpdateFromEntityStatesList( + entityList: List>>?, + context: Context, + entityOptions: EntityRegistryOptions? + ): String = entityList?.fold("") { acc, entity -> + + val localDate = with(entity.lastUpdated) { + LocalDateTime.ofInstant(toInstant(), timeZone.toZoneId()) + } + val entityTextToVisualize = StringBuilder("(") + entityTextToVisualize.append(localDate.format(DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm:ss"))).append(") ") + entityTextToVisualize.append(entity.friendlyState(context, entityOptions)) + if (acc.isEmpty()) { + acc.plus(entity.friendlyName).plus(": ").plus(entityTextToVisualize) + } else { + acc.plus("\n").plus(entity.friendlyName).plus(": ").plus(entityTextToVisualize) + } + } ?: "" +} diff --git a/app/src/main/java/io/homeassistant/companion/android/widgets/history/HistoryWidgetConfigureActivity.kt b/app/src/main/java/io/homeassistant/companion/android/widgets/history/HistoryWidgetConfigureActivity.kt new file mode 100644 index 00000000000..8e021c50eb5 --- /dev/null +++ b/app/src/main/java/io/homeassistant/companion/android/widgets/history/HistoryWidgetConfigureActivity.kt @@ -0,0 +1,338 @@ +package io.homeassistant.companion.android.widgets.history + +import android.app.PendingIntent +import android.appwidget.AppWidgetManager +import android.content.ComponentName +import android.content.Intent +import android.os.Build +import android.os.Bundle +import android.text.Editable +import android.text.TextWatcher +import android.util.Log +import android.view.View +import android.widget.AdapterView +import android.widget.ArrayAdapter +import android.widget.AutoCompleteTextView +import android.widget.MultiAutoCompleteTextView +import android.widget.Spinner +import android.widget.Toast +import androidx.core.content.ContextCompat +import androidx.core.content.getSystemService +import androidx.core.graphics.toColorInt +import androidx.lifecycle.lifecycleScope +import com.google.android.material.color.DynamicColors +import dagger.hilt.android.AndroidEntryPoint +import io.homeassistant.companion.android.common.R as commonR +import io.homeassistant.companion.android.common.data.integration.Entity +import io.homeassistant.companion.android.database.widget.HistoryWidgetDao +import io.homeassistant.companion.android.database.widget.WidgetBackgroundType +import io.homeassistant.companion.android.databinding.WidgetHistoryConfigureBinding +import io.homeassistant.companion.android.settings.widgets.ManageWidgetsViewModel +import io.homeassistant.companion.android.util.getHexForColor +import io.homeassistant.companion.android.widgets.BaseWidgetConfigureActivity +import io.homeassistant.companion.android.widgets.BaseWidgetProvider +import io.homeassistant.companion.android.widgets.common.SingleItemArrayAdapter +import java.util.LinkedList +import javax.inject.Inject +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking + +@AndroidEntryPoint +class HistoryWidgetConfigureActivity : BaseWidgetConfigureActivity() { + + companion object { + private const val TAG: String = "HistoryWidgetConfigAct" + private const val PIN_WIDGET_CALLBACK = "io.homeassistant.companion.android.widgets.entity.HistoryWidgetConfigureActivity.PIN_WIDGET_CALLBACK" + } + + @Inject + lateinit var historyWidgetDao: HistoryWidgetDao + override val dao get() = historyWidgetDao + + private var entities = mutableMapOf>>() + private var selectedEntities: LinkedList?> = LinkedList() + + private var labelFromEntity = false + + private lateinit var binding: WidgetHistoryConfigureBinding + + override val serverSelect: View + get() = binding.serverSelect + + override val serverSelectList: Spinner + get() = binding.serverSelectList + + private var requestLauncherSetup = false + + private var entityAdapter: SingleItemArrayAdapter>? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + // Set the result to CANCELED. This will cause the widget host to cancel + // out of the widget placement if the user presses the back button. + setResult(RESULT_CANCELED) + + binding = WidgetHistoryConfigureBinding.inflate(layoutInflater) + setContentView(binding.root) + + binding.addButton.setOnClickListener { + if (requestLauncherSetup) { + if ( + Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && + isValidServerId() && + binding.widgetTextConfigEntityId.text.split(",").any { + entities[selectedServerId!!].orEmpty().any { e -> e.entityId == it.trim() } + } + ) { + getSystemService()?.requestPinAppWidget( + ComponentName(this, HistoryWidget::class.java), + null, + PendingIntent.getActivity( + this, + System.currentTimeMillis().toInt(), + Intent(this, HistoryWidgetConfigureActivity::class.java).putExtra(PIN_WIDGET_CALLBACK, true).setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP), + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE + ) + ) + } else { + showAddWidgetError() + } + } else { + onAddWidget() + } + } + + // Find the widget id from the intent. + val intent = intent + val extras = intent.extras + if (extras != null) { + appWidgetId = extras.getInt( + AppWidgetManager.EXTRA_APPWIDGET_ID, + AppWidgetManager.INVALID_APPWIDGET_ID + ) + requestLauncherSetup = extras.getBoolean( + ManageWidgetsViewModel.CONFIGURE_REQUEST_LAUNCHER, + false + ) + } + + // If this activity was started with an intent without an app widget ID, finish with an error. + if (appWidgetId == AppWidgetManager.INVALID_APPWIDGET_ID && !requestLauncherSetup) { + finish() + return + } + + val historyWidget = historyWidgetDao.get(appWidgetId) + + val backgroundTypeValues = mutableListOf( + getString(commonR.string.widget_background_type_daynight), + getString(commonR.string.widget_background_type_transparent) + ) + if (DynamicColors.isDynamicColorAvailable()) { + backgroundTypeValues.add(0, getString(commonR.string.widget_background_type_dynamiccolor)) + } + binding.backgroundType.adapter = ArrayAdapter(this, android.R.layout.simple_spinner_dropdown_item, backgroundTypeValues) + + if (historyWidget != null) { + binding.widgetTextConfigEntityId.setText(historyWidget.entityId) + binding.label.setText(historyWidget.label) + binding.textSize.setText(historyWidget.textSize.toInt().toString()) + + binding.backgroundType.setSelection( + when { + historyWidget.backgroundType == WidgetBackgroundType.DYNAMICCOLOR && DynamicColors.isDynamicColorAvailable() -> + backgroundTypeValues.indexOf(getString(commonR.string.widget_background_type_dynamiccolor)) + historyWidget.backgroundType == WidgetBackgroundType.TRANSPARENT -> + backgroundTypeValues.indexOf(getString(commonR.string.widget_background_type_transparent)) + else -> + backgroundTypeValues.indexOf(getString(commonR.string.widget_background_type_daynight)) + } + ) + binding.textColor.visibility = if (historyWidget.backgroundType == WidgetBackgroundType.TRANSPARENT) View.VISIBLE else View.GONE + binding.textColorWhite.isChecked = + historyWidget.textColor?.let { it.toColorInt() == ContextCompat.getColor(this, android.R.color.white) } ?: true + binding.textColorBlack.isChecked = + historyWidget.textColor?.let { it.toColorInt() == ContextCompat.getColor(this, commonR.color.colorWidgetButtonLabelBlack) } ?: false + + val entities = runBlocking { + try { + historyWidget.entityId.split(",").map { s -> + serverManager.integrationRepository(historyWidget.serverId).getEntity(s.trim()) + } + } catch (e: Exception) { + Log.e(TAG, "Unable to get entity information", e) + Toast.makeText(applicationContext, commonR.string.widget_entity_fetch_error, Toast.LENGTH_LONG) + .show() + null + } + } + if (entities != null) { + selectedEntities.addAll(entities) + } + binding.addButton.setText(commonR.string.update_widget) + } else { + binding.textSize.setText(HistoryWidget.DEFAULT_TEXT_SIZE.toInt().toString()) + binding.backgroundType.setSelection(0) + } + entityAdapter = SingleItemArrayAdapter(this) { it?.entityId ?: "" } + + setupServerSelect(historyWidget?.serverId) + + binding.widgetTextConfigEntityId.setAdapter(entityAdapter) + binding.widgetTextConfigEntityId.setTokenizer(MultiAutoCompleteTextView.CommaTokenizer()) + binding.widgetTextConfigEntityId.onFocusChangeListener = dropDownOnFocus + + binding.label.addTextChangedListener(labelTextChanged) + + binding.backgroundType.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { + override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { + binding.textColor.visibility = + if (parent?.adapter?.getItem(position) == getString(commonR.string.widget_background_type_transparent)) { + View.VISIBLE + } else { + View.GONE + } + } + + override fun onNothingSelected(parent: AdapterView<*>?) { + binding.textColor.visibility = View.GONE + } + } + + serverManager.defaultServers.forEach { server -> + lifecycleScope.launch { + try { + val fetchedEntities = serverManager.integrationRepository(server.id).getEntities().orEmpty() + entities[server.id] = fetchedEntities + if (server.id == selectedServerId) setAdapterEntities(server.id) + } catch (e: Exception) { + // If entities fail to load, it's okay to pass + // an empty map to the dynamicFieldAdapter + Log.e(TAG, "Failed to query entities", e) + } + } + } + } + + override fun onServerSelected(serverId: Int) { + selectedEntities.clear() + binding.widgetTextConfigEntityId.setText("") + setAdapterEntities(serverId) + } + + private fun setAdapterEntities(serverId: Int) { + entityAdapter?.let { adapter -> + adapter.clearAll() + if (entities[serverId] != null) { + adapter.addAll(entities[serverId].orEmpty().toMutableList()) + adapter.sort() + } + runOnUiThread { adapter.notifyDataSetChanged() } + } + } + + private val dropDownOnFocus = View.OnFocusChangeListener { view, hasFocus -> + if (hasFocus && view is AutoCompleteTextView) { + view.showDropDown() + } + } + + private val labelTextChanged = object : TextWatcher { + override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) { + // Not implemented + } + + override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { + // Not implemented + } + + override fun afterTextChanged(s: Editable?) { + labelFromEntity = false + } + } + + private fun onAddWidget() { + if (appWidgetId == AppWidgetManager.INVALID_APPWIDGET_ID) { + showAddWidgetError() + return + } + try { + val context = this@HistoryWidgetConfigureActivity + + // Set up a broadcast intent and pass the service call data as extras + val intent = Intent() + intent.action = BaseWidgetProvider.RECEIVE_DATA + intent.component = ComponentName(context, HistoryWidget::class.java) + + intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId) + + intent.putExtra( + HistoryWidget.EXTRA_SERVER_ID, + selectedServerId!! + ) + + selectedEntities = LinkedList() + val se = binding.widgetTextConfigEntityId.text.split(",") + se.forEach { + val entity = entities[selectedServerId]!!.firstOrNull { e -> e.entityId == it.trim() } + if (entity != null) selectedEntities.add(entity) + } + intent.putExtra( + HistoryWidget.EXTRA_ENTITY_ID, + selectedEntities.map { e -> e?.entityId }.reduce { a, b -> "$a,$b" } + ) + + intent.putExtra( + HistoryWidget.EXTRA_LABEL, + binding.label.text.toString() + ) + + intent.putExtra( + HistoryWidget.EXTRA_TEXT_SIZE, + binding.textSize.text.toString() + ) + + intent.putExtra( + HistoryWidget.EXTRA_BACKGROUND_TYPE, + when (binding.backgroundType.selectedItem as String?) { + getString(commonR.string.widget_background_type_dynamiccolor) -> WidgetBackgroundType.DYNAMICCOLOR + getString(commonR.string.widget_background_type_transparent) -> WidgetBackgroundType.TRANSPARENT + else -> WidgetBackgroundType.DAYNIGHT + } + ) + + intent.putExtra( + HistoryWidget.EXTRA_TEXT_COLOR, + if (binding.backgroundType.selectedItem as String? == getString(commonR.string.widget_background_type_transparent)) { + getHexForColor(if (binding.textColorWhite.isChecked) android.R.color.white else commonR.color.colorWidgetButtonLabelBlack) + } else { + null + } + ) + + context.sendBroadcast(intent) + + // Make sure we pass back the original appWidgetId + setResult( + RESULT_OK, + Intent().putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId) + ) + finish() + } catch (e: Exception) { + Log.e(TAG, "Issue configuring widget", e) + showAddWidgetError() + } + } + + override fun onNewIntent(intent: Intent) { + super.onNewIntent(intent) + if (intent.extras != null && intent.hasExtra(PIN_WIDGET_CALLBACK)) { + appWidgetId = intent.extras!!.getInt( + AppWidgetManager.EXTRA_APPWIDGET_ID, + AppWidgetManager.INVALID_APPWIDGET_ID + ) + onAddWidget() + } + } +} diff --git a/app/src/main/res/layout-v31/widget_history_wrapper_dynamiccolor.xml b/app/src/main/res/layout-v31/widget_history_wrapper_dynamiccolor.xml new file mode 100644 index 00000000000..dab86b50c55 --- /dev/null +++ b/app/src/main/res/layout-v31/widget_history_wrapper_dynamiccolor.xml @@ -0,0 +1,9 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/widget_history.xml b/app/src/main/res/layout/widget_history.xml new file mode 100644 index 00000000000..b78f28e4cd6 --- /dev/null +++ b/app/src/main/res/layout/widget_history.xml @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/widget_history_configure.xml b/app/src/main/res/layout/widget_history_configure.xml new file mode 100644 index 00000000000..692d71c0977 --- /dev/null +++ b/app/src/main/res/layout/widget_history_configure.xml @@ -0,0 +1,174 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/widget_history_wrapper_default.xml b/app/src/main/res/layout/widget_history_wrapper_default.xml new file mode 100644 index 00000000000..8afa0f6f7a1 --- /dev/null +++ b/app/src/main/res/layout/widget_history_wrapper_default.xml @@ -0,0 +1,9 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/history_widget_info.xml b/app/src/main/res/xml/history_widget_info.xml new file mode 100644 index 00000000000..9ee44878e0c --- /dev/null +++ b/app/src/main/res/xml/history_widget_info.xml @@ -0,0 +1,18 @@ + + \ No newline at end of file diff --git a/common/schemas/io.homeassistant.companion.android.database.AppDatabase/48.json b/common/schemas/io.homeassistant.companion.android.database.AppDatabase/48.json new file mode 100644 index 00000000000..f7c8f21f7d5 --- /dev/null +++ b/common/schemas/io.homeassistant.companion.android.database.AppDatabase/48.json @@ -0,0 +1,1191 @@ +{ + "formatVersion": 1, + "database": { + "version": 48, + "identityHash": "0f958ba55579b89c341335658fd01d2a", + "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": { + "autoGenerate": false, + "columnNames": [ + "sensor_id", + "name" + ] + }, + "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": { + "autoGenerate": false, + "columnNames": [ + "host" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "sensors", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `server_id` INTEGER NOT NULL DEFAULT 0, `enabled` INTEGER NOT NULL, `registered` INTEGER DEFAULT NULL, `state` TEXT NOT NULL, `last_sent_state` TEXT DEFAULT NULL, `last_sent_icon` TEXT DEFAULT 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`, `server_id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "serverId", + "columnName": "server_id", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "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": false, + "defaultValue": "NULL" + }, + { + "fieldPath": "lastSentIcon", + "columnName": "last_sent_icon", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "NULL" + }, + { + "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": { + "autoGenerate": false, + "columnNames": [ + "id", + "server_id" + ] + }, + "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": { + "autoGenerate": false, + "columnNames": [ + "sensor_id", + "name" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "button_widgets", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `server_id` INTEGER NOT NULL DEFAULT 0, `icon_name` TEXT 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, `require_authentication` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "serverId", + "columnName": "server_id", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "iconName", + "columnName": "icon_name", + "affinity": "TEXT", + "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 + }, + { + "fieldPath": "requireAuthentication", + "columnName": "require_authentication", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "camera_widgets", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `server_id` INTEGER NOT NULL DEFAULT 0, `entity_id` TEXT NOT NULL, `tap_action` TEXT NOT NULL DEFAULT 'REFRESH', PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "serverId", + "columnName": "server_id", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "entityId", + "columnName": "entity_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tapAction", + "columnName": "tap_action", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'REFRESH'" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "media_player_controls_widgets", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `server_id` INTEGER NOT NULL DEFAULT 0, `entity_id` TEXT NOT NULL, `label` TEXT, `show_skip` INTEGER NOT NULL, `show_seek` INTEGER NOT NULL, `show_volume` INTEGER NOT NULL, `show_source` 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": "serverId", + "columnName": "server_id", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "entityId", + "columnName": "entity_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "label", + "columnName": "label", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "showSkip", + "columnName": "show_skip", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "showSeek", + "columnName": "show_seek", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "showVolume", + "columnName": "show_volume", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "showSource", + "columnName": "show_source", + "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": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "static_widget", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `server_id` INTEGER NOT NULL DEFAULT 0, `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, `tap_action` TEXT NOT NULL DEFAULT 'REFRESH', `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": "serverId", + "columnName": "server_id", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "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": "tapAction", + "columnName": "tap_action", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'REFRESH'" + }, + { + "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": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "template_widgets", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `server_id` INTEGER NOT NULL DEFAULT 0, `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": "serverId", + "columnName": "server_id", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "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": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "history_widgets", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `server_id` INTEGER NOT NULL DEFAULT 0, `entity_id` TEXT NOT NULL, `label` TEXT, `text_size` REAL 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": "serverId", + "columnName": "server_id", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "entityId", + "columnName": "entity_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "label", + "columnName": "label", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "textSize", + "columnName": "text_size", + "affinity": "REAL", + "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": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "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, `server_id` INTEGER)", + "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 + }, + { + "fieldPath": "serverId", + "columnName": "server_id", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "location_history", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `created` INTEGER NOT NULL, `trigger` TEXT NOT NULL, `result` TEXT NOT NULL, `latitude` REAL, `longitude` REAL, `location_name` TEXT, `accuracy` INTEGER, `data` TEXT, `server_id` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "created", + "columnName": "created", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "trigger", + "columnName": "trigger", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "result", + "columnName": "result", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "latitude", + "columnName": "latitude", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "longitude", + "columnName": "longitude", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "locationName", + "columnName": "location_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "accuracy", + "columnName": "accuracy", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "data", + "columnName": "data", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "serverId", + "columnName": "server_id", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "qs_tiles", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `tile_id` TEXT NOT NULL, `added` INTEGER NOT NULL DEFAULT 1, `server_id` INTEGER NOT NULL DEFAULT 0, `icon_name` TEXT, `entity_id` TEXT NOT NULL, `label` TEXT NOT NULL, `subtitle` TEXT, `should_vibrate` INTEGER NOT NULL DEFAULT 0, `auth_required` INTEGER NOT NULL DEFAULT 0)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "tileId", + "columnName": "tile_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "added", + "columnName": "added", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "serverId", + "columnName": "server_id", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "iconName", + "columnName": "icon_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "entityId", + "columnName": "entity_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "label", + "columnName": "label", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "subtitle", + "columnName": "subtitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "shouldVibrate", + "columnName": "should_vibrate", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "authRequired", + "columnName": "auth_required", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "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": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "favorite_cache", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `friendly_name` TEXT NOT NULL, `icon` TEXT, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "friendlyName", + "columnName": "friendly_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "icon", + "columnName": "icon", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "camera_tiles", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `entity_id` TEXT, `refresh_interval` INTEGER, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "entityId", + "columnName": "entity_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "refreshInterval", + "columnName": "refresh_interval", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "entity_state_complications", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `entity_id` TEXT NOT NULL, `show_title` INTEGER NOT NULL DEFAULT 1, `show_unit` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "entityId", + "columnName": "entity_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "showTitle", + "columnName": "show_title", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "showUnit", + "columnName": "show_unit", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "servers", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `_name` TEXT NOT NULL, `name_override` TEXT, `_version` TEXT, `device_registry_id` TEXT, `list_order` INTEGER NOT NULL, `device_name` TEXT, `external_url` TEXT NOT NULL, `internal_url` TEXT, `cloud_url` TEXT, `webhook_id` TEXT, `secret` TEXT, `cloudhook_url` TEXT, `use_cloud` INTEGER NOT NULL, `internal_ssids` TEXT NOT NULL, `prioritize_internal` INTEGER NOT NULL, `access_token` TEXT, `refresh_token` TEXT, `token_expiration` INTEGER, `token_type` TEXT, `install_id` TEXT, `user_id` TEXT, `user_name` TEXT, `user_is_owner` INTEGER, `user_is_admin` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "_name", + "columnName": "_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "nameOverride", + "columnName": "name_override", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "_version", + "columnName": "_version", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "deviceRegistryId", + "columnName": "device_registry_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "listOrder", + "columnName": "list_order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "deviceName", + "columnName": "device_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "connection.externalUrl", + "columnName": "external_url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "connection.internalUrl", + "columnName": "internal_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "connection.cloudUrl", + "columnName": "cloud_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "connection.webhookId", + "columnName": "webhook_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "connection.secret", + "columnName": "secret", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "connection.cloudhookUrl", + "columnName": "cloudhook_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "connection.useCloud", + "columnName": "use_cloud", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "connection.internalSsids", + "columnName": "internal_ssids", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "connection.prioritizeInternal", + "columnName": "prioritize_internal", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "session.accessToken", + "columnName": "access_token", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "session.refreshToken", + "columnName": "refresh_token", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "session.tokenExpiration", + "columnName": "token_expiration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "session.tokenType", + "columnName": "token_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "session.installId", + "columnName": "install_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "user.id", + "columnName": "user_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "user.name", + "columnName": "user_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "user.isOwner", + "columnName": "user_is_owner", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "user.isAdmin", + "columnName": "user_is_admin", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "settings", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `websocket_setting` TEXT NOT NULL, `sensor_update_frequency` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "websocketSetting", + "columnName": "websocket_setting", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sensorUpdateFrequency", + "columnName": "sensor_update_frequency", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "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, '0f958ba55579b89c341335658fd01d2a')" + ] + } +} \ No newline at end of file diff --git a/common/src/main/java/io/homeassistant/companion/android/common/data/integration/IntegrationRepository.kt b/common/src/main/java/io/homeassistant/companion/android/common/data/integration/IntegrationRepository.kt index 9ce25f7174f..d8cb49cc1ef 100644 --- a/common/src/main/java/io/homeassistant/companion/android/common/data/integration/IntegrationRepository.kt +++ b/common/src/main/java/io/homeassistant/companion/android/common/data/integration/IntegrationRepository.kt @@ -41,6 +41,7 @@ interface IntegrationRepository { suspend fun getEntity(entityId: String): Entity>? suspend fun getEntityUpdates(): Flow>? suspend fun getEntityUpdates(entityIds: List): Flow>? + suspend fun getHistory(entityIds: List): List>>>? suspend fun callAction(domain: String, action: String, actionData: HashMap) diff --git a/common/src/main/java/io/homeassistant/companion/android/common/data/integration/history/HistoryRequestParams.kt b/common/src/main/java/io/homeassistant/companion/android/common/data/integration/history/HistoryRequestParams.kt new file mode 100644 index 00000000000..82234ae28d7 --- /dev/null +++ b/common/src/main/java/io/homeassistant/companion/android/common/data/integration/history/HistoryRequestParams.kt @@ -0,0 +1,42 @@ +package io.homeassistant.companion.android.common.data.integration.history + +import okhttp3.HttpUrl + +data class HistoryRequestParams( + val timestamp: String? = null, + val filterEntityIds: List, + val endTime: String? = null, + val minimalResponse: String? = null, + val noAttributes: String? = null, + val significantChangesOnly: String? = null +) { + + companion object { + const val FILTER_ENTITY_IDS = "filter_entity_id" + const val END_TIME = "end_time" + const val MINIMAL_RESPONSE = "minimal_response" + const val NO_ATTRIBUTES = "no_attributes" + const val SIGNIFICANT_CHANGES_ONLY = "significant_changes_only" + } + + fun addToUrl(url: HttpUrl): HttpUrl { + val builder = url.newBuilder() + if (timestamp != null) { + builder.addPathSegments("$timestamp") + } + builder.addQueryParameter(FILTER_ENTITY_IDS, filterEntityIds.joinToString(separator = ",")) + if (endTime != null) { + builder.addQueryParameter(END_TIME, endTime) + } + if (minimalResponse != null) { + builder.addQueryParameter(MINIMAL_RESPONSE, minimalResponse) + } + if (noAttributes != null) { + builder.addQueryParameter(NO_ATTRIBUTES, noAttributes) + } + if (significantChangesOnly != null) { + builder.addQueryParameter(SIGNIFICANT_CHANGES_ONLY, significantChangesOnly) + } + return builder.build() + } +} diff --git a/common/src/main/java/io/homeassistant/companion/android/common/data/integration/impl/IntegrationRepositoryImpl.kt b/common/src/main/java/io/homeassistant/companion/android/common/data/integration/impl/IntegrationRepositoryImpl.kt index 83f9c73e38d..f4450d40a30 100644 --- a/common/src/main/java/io/homeassistant/companion/android/common/data/integration/impl/IntegrationRepositoryImpl.kt +++ b/common/src/main/java/io/homeassistant/companion/android/common/data/integration/impl/IntegrationRepositoryImpl.kt @@ -14,6 +14,7 @@ import io.homeassistant.companion.android.common.data.integration.IntegrationRep import io.homeassistant.companion.android.common.data.integration.SensorRegistration import io.homeassistant.companion.android.common.data.integration.UpdateLocation import io.homeassistant.companion.android.common.data.integration.ZoneAttributes +import io.homeassistant.companion.android.common.data.integration.history.HistoryRequestParams import io.homeassistant.companion.android.common.data.integration.impl.entities.ActionRequest import io.homeassistant.companion.android.common.data.integration.impl.entities.EntityResponse import io.homeassistant.companion.android.common.data.integration.impl.entities.FireEventRequest @@ -695,6 +696,34 @@ class IntegrationRepositoryImpl @AssistedInject constructor( } } + override suspend fun getHistory(entityIds: List): List>>>? { + val url = server.connection.getUrl()?.toHttpUrlOrNull() + if (url == null) { + Log.e(TAG, "Unable to register device due to missing URL") + return null + } + val requestParams = HistoryRequestParams( + filterEntityIds = entityIds + ) + + val response = integrationService.getHistory( + requestParams.addToUrl(url.newBuilder().addPathSegments("api/history/period").build()), + serverManager.authenticationRepository(serverId).buildBearerToken() + ) + return response.map { statesList -> + statesList.map { + Entity( + it.entityId, + it.state, + it.attributes, + it.lastChanged, + it.lastUpdated, + it.context + ) + } + }.sortedBy { it.firstOrNull()?.entityId }.toList() + } + override suspend fun registerSensor(sensorRegistration: SensorRegistration) { // Version is read from server variable (cached) to prevent multiple failed requests in a // row and very long suspend if server is offline for a longer period of time. This function diff --git a/common/src/main/java/io/homeassistant/companion/android/common/data/integration/impl/IntegrationService.kt b/common/src/main/java/io/homeassistant/companion/android/common/data/integration/impl/IntegrationService.kt index 621449175a9..d4f8c5c8f24 100644 --- a/common/src/main/java/io/homeassistant/companion/android/common/data/integration/impl/IntegrationService.kt +++ b/common/src/main/java/io/homeassistant/companion/android/common/data/integration/impl/IntegrationService.kt @@ -32,6 +32,12 @@ interface IntegrationService { @Header("Authorization") auth: String ): EntityResponse> + @GET + suspend fun getHistory( + @Url url: HttpUrl, + @Header("Authorization") auth: String + ): Array>>> + @POST suspend fun callWebhook( @Url url: HttpUrl, diff --git a/common/src/main/java/io/homeassistant/companion/android/database/AppDatabase.kt b/common/src/main/java/io/homeassistant/companion/android/database/AppDatabase.kt index 3f3b64f5b60..bd5492fb95f 100644 --- a/common/src/main/java/io/homeassistant/companion/android/database/AppDatabase.kt +++ b/common/src/main/java/io/homeassistant/companion/android/database/AppDatabase.kt @@ -65,6 +65,8 @@ import io.homeassistant.companion.android.database.widget.ButtonWidgetDao import io.homeassistant.companion.android.database.widget.ButtonWidgetEntity import io.homeassistant.companion.android.database.widget.CameraWidgetDao import io.homeassistant.companion.android.database.widget.CameraWidgetEntity +import io.homeassistant.companion.android.database.widget.HistoryWidgetDao +import io.homeassistant.companion.android.database.widget.HistoryWidgetEntity import io.homeassistant.companion.android.database.widget.MediaPlayerControlsWidgetDao import io.homeassistant.companion.android.database.widget.MediaPlayerControlsWidgetEntity import io.homeassistant.companion.android.database.widget.StaticWidgetDao @@ -87,6 +89,7 @@ import kotlinx.coroutines.runBlocking MediaPlayerControlsWidgetEntity::class, StaticWidgetEntity::class, TemplateWidgetEntity::class, + HistoryWidgetEntity::class, NotificationItem::class, LocationHistoryItem::class, TileEntity::class, @@ -97,7 +100,7 @@ import kotlinx.coroutines.runBlocking Server::class, Setting::class ], - version = 47, + version = 48, autoMigrations = [ AutoMigration(from = 24, to = 25), AutoMigration(from = 25, to = 26), @@ -120,7 +123,8 @@ import kotlinx.coroutines.runBlocking AutoMigration(from = 43, to = 44), AutoMigration(from = 44, to = 45), AutoMigration(from = 45, to = 46), - AutoMigration(from = 46, to = 47) + AutoMigration(from = 46, to = 47), + AutoMigration(from = 47, to = 48) ] ) @TypeConverters( @@ -136,6 +140,7 @@ abstract class AppDatabase : RoomDatabase() { abstract fun sensorDao(): SensorDao abstract fun buttonWidgetDao(): ButtonWidgetDao abstract fun cameraWidgetDao(): CameraWidgetDao + abstract fun historyWidgetDao(): HistoryWidgetDao abstract fun mediaPlayCtrlWidgetDao(): MediaPlayerControlsWidgetDao abstract fun staticWidgetDao(): StaticWidgetDao abstract fun templateWidgetDao(): TemplateWidgetDao diff --git a/common/src/main/java/io/homeassistant/companion/android/database/DatabaseModule.kt b/common/src/main/java/io/homeassistant/companion/android/database/DatabaseModule.kt index 0f92c5473ca..80039bbd937 100644 --- a/common/src/main/java/io/homeassistant/companion/android/database/DatabaseModule.kt +++ b/common/src/main/java/io/homeassistant/companion/android/database/DatabaseModule.kt @@ -19,6 +19,7 @@ import io.homeassistant.companion.android.database.wear.FavoriteCachesDao import io.homeassistant.companion.android.database.wear.FavoritesDao import io.homeassistant.companion.android.database.widget.ButtonWidgetDao import io.homeassistant.companion.android.database.widget.CameraWidgetDao +import io.homeassistant.companion.android.database.widget.HistoryWidgetDao import io.homeassistant.companion.android.database.widget.MediaPlayerControlsWidgetDao import io.homeassistant.companion.android.database.widget.StaticWidgetDao import io.homeassistant.companion.android.database.widget.TemplateWidgetDao @@ -45,6 +46,9 @@ object DatabaseModule { @Provides fun provideCameraWidgetDao(database: AppDatabase): CameraWidgetDao = database.cameraWidgetDao() + @Provides + fun provideHistoryWidgetDao(database: AppDatabase): HistoryWidgetDao = database.historyWidgetDao() + @Provides fun provideMediaPlayCtrlWidgetDao(database: AppDatabase): MediaPlayerControlsWidgetDao = database.mediaPlayCtrlWidgetDao() diff --git a/common/src/main/java/io/homeassistant/companion/android/database/widget/HistoryWidgetDao.kt b/common/src/main/java/io/homeassistant/companion/android/database/widget/HistoryWidgetDao.kt new file mode 100644 index 00000000000..951be5ea47c --- /dev/null +++ b/common/src/main/java/io/homeassistant/companion/android/database/widget/HistoryWidgetDao.kt @@ -0,0 +1,32 @@ +package io.homeassistant.companion.android.database.widget + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import kotlinx.coroutines.flow.Flow + +@Dao +interface HistoryWidgetDao : WidgetDao { + + @Query("SELECT * FROM history_widgets WHERE id = :id") + fun get(id: Int): HistoryWidgetEntity? + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun add(historyWidgetEntity: HistoryWidgetEntity) + + @Query("DELETE FROM history_widgets WHERE id = :id") + override suspend fun delete(id: Int) + + @Query("DELETE FROM history_widgets WHERE id IN (:ids)") + suspend fun deleteAll(ids: IntArray) + + @Query("SELECT * FROM history_widgets") + suspend fun getAll(): List + + @Query("SELECT * FROM history_widgets") + fun getAllFlow(): Flow> + + @Query("UPDATE history_widgets SET last_update = :lastUpdate WHERE id = :widgetId") + suspend fun updateWidgetLastUpdate(widgetId: Int, lastUpdate: String) +} diff --git a/common/src/main/java/io/homeassistant/companion/android/database/widget/HistoryWidgetEntity.kt b/common/src/main/java/io/homeassistant/companion/android/database/widget/HistoryWidgetEntity.kt new file mode 100644 index 00000000000..979f7a170be --- /dev/null +++ b/common/src/main/java/io/homeassistant/companion/android/database/widget/HistoryWidgetEntity.kt @@ -0,0 +1,26 @@ +package io.homeassistant.companion.android.database.widget + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity(tableName = "history_widgets") +data class HistoryWidgetEntity( + @PrimaryKey + override val id: Int, + @ColumnInfo(name = "server_id", defaultValue = "0") + override val serverId: Int, + @ColumnInfo(name = "entity_id") + val entityId: String, + @ColumnInfo(name = "label") + val label: String?, + @ColumnInfo(name = "text_size") + val textSize: Float, + @ColumnInfo(name = "last_update") + val lastUpdate: String, + @ColumnInfo(name = "background_type", defaultValue = "DAYNIGHT") + override val backgroundType: WidgetBackgroundType = WidgetBackgroundType.DAYNIGHT, + @ColumnInfo(name = "text_color") + override val textColor: String? = null +) : WidgetEntity, + ThemeableWidgetEntity diff --git a/common/src/main/res/values/strings.xml b/common/src/main/res/values/strings.xml index 4b546c57782..73291de73dd 100644 --- a/common/src/main/res/values/strings.xml +++ b/common/src/main/res/values/strings.xml @@ -297,6 +297,7 @@ High accuracy location High accuracy (GPS) mode enabled History + View any entity\'s historical state Tap and hold to reorder Unable to find your\nHome Assistant instance Icon @@ -346,6 +347,7 @@ Lights Button widgets Entity state widgets + History widgets Instances Other Media player widgets @@ -997,6 +999,8 @@ A custom component is preventing action data from loading. Unable to create widget. Unable to fetch data for configured entity. + Separator between entity IDs: + History widget Home Assistant widget Toggle Living room