Skip to content

Commit

Permalink
Add support for 'Colortemperaturepicker' widget
Browse files Browse the repository at this point in the history
Signed-off-by: Danny Baumann <[email protected]>
  • Loading branch information
maniac103 committed Dec 1, 2024
1 parent ec93eec commit a523f8c
Show file tree
Hide file tree
Showing 6 changed files with 165 additions and 10 deletions.
1 change: 1 addition & 0 deletions mobile/src/main/java/org/openhab/habdroid/model/Widget.kt
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ data class Widget(
Input,
Buttongrid,
Button,
Colortemperaturepicker,
Unknown
}

Expand Down
40 changes: 34 additions & 6 deletions mobile/src/main/java/org/openhab/habdroid/ui/WidgetAdapter.kt
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ import org.openhab.habdroid.util.IconBackground
import org.openhab.habdroid.util.ImageConversionPolicy
import org.openhab.habdroid.util.MjpegStreamer
import org.openhab.habdroid.util.PrefKeys
import org.openhab.habdroid.util.asColorTemperatureToColor
import org.openhab.habdroid.util.beautify
import org.openhab.habdroid.util.determineDataUsagePolicy
import org.openhab.habdroid.util.getChartTheme
Expand All @@ -120,6 +121,7 @@ import org.openhab.habdroid.util.getImageWidgetScalingType
import org.openhab.habdroid.util.getPrefs
import org.openhab.habdroid.util.orDefaultIfEmpty
import org.openhab.habdroid.util.resolveThemedColor
import org.openhab.habdroid.util.toColoredRoundedRect

/**
* This class provides openHAB widgets adapter for list view.
Expand Down Expand Up @@ -239,6 +241,7 @@ class WidgetAdapter(
TYPE_VIDEO -> VideoViewHolder(initData)
TYPE_WEB -> WebViewHolder(initData)
TYPE_COLOR -> ColorViewHolder(initData)
TYPE_COLORTEMPERATURE -> ColorTemperatureViewHolder(initData)
TYPE_VIDEO_MJPEG -> MjpegVideoViewHolder(initData)
TYPE_LOCATION -> MapViewHelper.createViewHolder(initData)
TYPE_INPUT -> InputViewHolder(initData)
Expand Down Expand Up @@ -389,6 +392,7 @@ class WidgetAdapter(
}
Widget.Type.Webview -> TYPE_WEB
Widget.Type.Colorpicker -> TYPE_COLOR
Widget.Type.Colortemperaturepicker -> TYPE_COLORTEMPERATURE
Widget.Type.Mapview -> TYPE_LOCATION
Widget.Type.Input -> if (widget.shouldUseDateTimePickerForInput()) TYPE_DATETIMEINPUT else TYPE_INPUT
Widget.Type.Buttongrid -> TYPE_BUTTONGRID
Expand Down Expand Up @@ -1702,6 +1706,29 @@ class WidgetAdapter(
}
}

class ColorTemperatureViewHolder internal constructor(initData: ViewHolderInitData) : LabeledItemBaseViewHolder(
initData,
R.layout.widgetlist_colortemperatureitem,
R.layout.widgetlist_colortemperatureitem_compact
) {
private val previewImage = itemView.findViewById<ImageView>(R.id.current_temperature)

override fun bind(widget: Widget) {
super.bind(widget)
val drawable = (widget.state ?: widget.item?.state)?.asNumber?.let { state ->
// if not Kelvin, assume Mirek
val kelvin = if (state.unit == "K") state.value else 1000000F / state.value
kelvin.asColorTemperatureToColor().toColoredRoundedRect(previewImage.context)
}
previewImage.setImageDrawable(drawable)
}

override fun handleRowClick() {
val widget = boundWidget ?: return
fragmentPresenter.showBottomSheet(ColorTemperatureSliderBottomSheet(), widget)
}
}

class MjpegVideoViewHolder internal constructor(initData: ViewHolderInitData) :
HeavyDataViewHolder(initData, R.layout.widgetlist_videomjpegitem) {
private val imageView = widgetContentView as WidgetImageView
Expand Down Expand Up @@ -1808,12 +1835,13 @@ class WidgetAdapter(
private const val TYPE_VIDEO = 15
private const val TYPE_WEB = 16
private const val TYPE_COLOR = 17
private const val TYPE_VIDEO_MJPEG = 18
private const val TYPE_LOCATION = 19
private const val TYPE_INPUT = 20
private const val TYPE_DATETIMEINPUT = 21
private const val TYPE_BUTTONGRID = 22
private const val TYPE_INVISIBLE = 23
private const val TYPE_COLORTEMPERATURE = 18
private const val TYPE_VIDEO_MJPEG = 19
private const val TYPE_LOCATION = 20
private const val TYPE_INPUT = 21
private const val TYPE_DATETIMEINPUT = 22
private const val TYPE_BUTTONGRID = 23
private const val TYPE_INVISIBLE = 24

private fun toInternalViewType(viewType: Int, compactMode: Boolean): Int {
return viewType or (if (compactMode) 0x100 else 0)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@

package org.openhab.habdroid.ui

import android.graphics.LinearGradient
import android.graphics.Paint
import android.graphics.Shader
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
Expand All @@ -29,16 +32,19 @@ import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.google.android.material.slider.LabelFormatter
import com.google.android.material.slider.Slider
import kotlin.math.roundToInt
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancel
import org.openhab.habdroid.R
import org.openhab.habdroid.core.connection.Connection
import org.openhab.habdroid.model.ParsedState
import org.openhab.habdroid.model.Widget
import org.openhab.habdroid.model.withValue
import org.openhab.habdroid.ui.widget.WidgetSlider
import org.openhab.habdroid.util.ColorPickerHelper
import org.openhab.habdroid.util.asColorTemperatureToColor
import org.openhab.habdroid.util.parcelable

open class AbstractWidgetBottomSheet : BottomSheetDialogFragment() {
Expand Down Expand Up @@ -67,10 +73,8 @@ open class AbstractWidgetBottomSheet : BottomSheetDialogFragment() {
}
}

class SliderBottomSheet : AbstractWidgetBottomSheet(), WidgetSlider.UpdateListener {
private var updateJob: Job? = null

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
open class SliderBottomSheet : AbstractWidgetBottomSheet(), WidgetSlider.UpdateListener {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
val view = inflater.inflate(R.layout.bottom_sheet_setpoint, container, false)

view.findViewById<WidgetSlider>(R.id.slider).apply {
Expand Down Expand Up @@ -98,6 +102,51 @@ class SliderBottomSheet : AbstractWidgetBottomSheet(), WidgetSlider.UpdateListen
}
}

class ColorTemperatureSliderBottomSheet : SliderBottomSheet(), View.OnLayoutChangeListener {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
val v = super.onCreateView(inflater, container, savedInstanceState)
v.findViewById<WidgetSlider>(R.id.slider).apply {
addOnLayoutChangeListener(this@ColorTemperatureSliderBottomSheet)
setLabelFormatter { value -> "${value.roundToInt()} K" }
}
return v
}

override suspend fun onValueUpdate(value: Float) {
val item = widget.item ?: return
val state = ParsedState.NumberState(value, "K", "%.0f %unit%")
Log.d(TAG, "Send state $state for ${item.name}")
connection?.httpClient?.sendItemUpdate(item, state)
}

override fun onLayoutChange(view: View, l: Int, t: Int, r: Int, b: Int, ol: Int, ot: Int, or: Int, ob: Int) {
applyColorTemperatureGradientToTrack(view as WidgetSlider, r - l, b - t)
}

private fun applyColorTemperatureGradientToTrack(slider: WidgetSlider, width: Int, height: Int) {
val min = widget.minValue
val max = widget.maxValue
val steps = 20
val positions = (0 until steps).map { 1F * it / steps }.toFloatArray()
val colors = positions
.map { it * (max - min) + min }
.map { it.asColorTemperatureToColor() }
.toIntArray()
val shader = LinearGradient(0F, 0F, width.toFloat(), height.toFloat(), colors, positions, Shader.TileMode.CLAMP)

listOf("activeTrackPaint", "inactiveTrackPaint")
.map { Slider::class.java.superclass.getDeclaredField(it) }
.forEach { field ->
field.isAccessible = true
(field.get(slider) as Paint).shader = shader
}
}

companion object {
private val TAG = SliderBottomSheet::class.java.simpleName
}
}

class SelectionBottomSheet : AbstractWidgetBottomSheet() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = inflater.inflate(R.layout.bottom_sheet_selection, container, false)
Expand Down
38 changes: 38 additions & 0 deletions mobile/src/main/java/org/openhab/habdroid/util/ExtensionFuncs.kt
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,13 @@ import android.content.Intent
import android.content.IntentFilter
import android.content.SharedPreferences
import android.content.pm.PackageManager
import android.content.res.ColorStateList
import android.content.res.Configuration
import android.content.res.Resources
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.Canvas
import android.graphics.Color
import android.net.ConnectivityManager
import android.net.Network
import android.net.Uri
Expand All @@ -53,6 +55,9 @@ import com.caverock.androidsvg.RenderOptions
import com.caverock.androidsvg.SVG
import com.google.android.material.color.DynamicColors
import com.google.android.material.color.MaterialColors
import com.google.android.material.shape.MaterialShapeDrawable
import com.google.android.material.shape.RelativeCornerSize
import com.google.android.material.shape.ShapeAppearanceModel
import java.io.EOFException
import java.io.IOException
import java.io.InputStream
Expand All @@ -68,8 +73,10 @@ import javax.jmdns.ServiceInfo
import javax.net.ssl.SSLException
import javax.net.ssl.SSLHandshakeException
import javax.net.ssl.SSLPeerUnverifiedException
import kotlin.math.ln
import kotlin.math.max
import kotlin.math.round
import kotlin.math.roundToInt
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
Expand Down Expand Up @@ -151,6 +158,37 @@ fun HttpUrl.toRelativeUrl(): String {
return this.toString().substring(base.toString().length - 1)
}

fun Float.asColorTemperatureToColor(): Int {
// For algorithm, see https://web.archive.org/web/20151024031939/http://www.zombieprototypes.com/?p=210
val temp = (this.coerceIn(1000F, 10000F) / 100F).toDouble()
// calculates a + bx + c * ln(x)
val approximate = { x: Double, a: Double, b: Double, c: Double -> a + b * x + c * ln(x) }
val red = when {
temp <= 66 -> 255.0
else -> approximate(temp - 55, 351.97690566805693, 0.114206453784165, -40.25366309332127)
}.coerceIn(0.0, 255.0)

val green = when {
temp <= 66 -> approximate(temp - 2, -155.25485562709179, -0.44596950469579133, 104.49216199393888)
else -> approximate(temp - 50, 325.4494125711974, 0.07943456536662342, -28.0852963507957)
}.coerceIn(0.0, 255.0)

val blue = when {
temp < 20 -> 0.0
temp > 66 -> 255.0
else -> approximate(temp - 10, -254.76935184120902, 0.8274096064007395, 115.67994401066147)
}.coerceIn(0.0, 255.0)

return Color.argb(255, red.roundToInt(), green.roundToInt(), blue.roundToInt())
}

fun Int.toColoredRoundedRect(context: Context) = MaterialShapeDrawable.createWithElevationOverlay(context).apply {
fillColor = ColorStateList.valueOf(this@toColoredRoundedRect)
shapeAppearanceModel = ShapeAppearanceModel.Builder()
.setAllCornerSizes(RelativeCornerSize(0.15f))
.build()
}

/**
* This method converts dp unit to equivalent pixels, depending on device density.
*
Expand Down
20 changes: 20 additions & 0 deletions mobile/src/main/res/layout/widgetlist_colortemperatureitem.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:descendantFocusability="blocksDescendants"
style="@style/WidgetListItemContainer">

<include layout="@layout/widgetlist_icontext" />

<ImageView
android:id="@+id/current_temperature"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_gravity="center_vertical"
android:layout_marginEnd="8dp"
android:scaleType="centerInside"
tools:src="@android:color/black" />

</LinearLayout>
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:descendantFocusability="blocksDescendants"
style="@style/WidgetListItemContainerCompact">

<include layout="@layout/widgetlist_icontext_compact" />

<ImageView
android:id="@+id/current_temperature"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_gravity="center_vertical"
android:layout_marginStart="8dp"
tools:src="@android:color/black" />

</LinearLayout>

0 comments on commit a523f8c

Please sign in to comment.