Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Add support for 'Colortemperaturepicker' widget #3830

Merged
merged 2 commits into from
Dec 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import java.util.Locale
import java.util.regex.Pattern
import kotlin.math.roundToInt
import kotlinx.parcelize.Parcelize
import org.openhab.habdroid.util.asColorTemperatureToKelvin

@Parcelize
data class HsvState internal constructor(val hue: Float, val saturation: Float, val value: Float) : Parcelable {
Expand Down Expand Up @@ -206,6 +207,9 @@ fun ParsedState.NumberState?.withValue(value: Float): ParsedState.NumberState {
return ParsedState.NumberState(value, this?.unit, this?.format)
}

fun ParsedState.NumberState.toColorTemperatureInKelvin() =
ParsedState.NumberState(value.asColorTemperatureToKelvin(), "K", "%.0f %unit%")

/**
* Parses a state string into the parsed representation.
*
Expand Down
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
52 changes: 45 additions & 7 deletions mobile/src/main/java/org/openhab/habdroid/ui/WidgetAdapter.kt
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ import org.openhab.habdroid.model.Item
import org.openhab.habdroid.model.LabeledValue
import org.openhab.habdroid.model.ParsedState
import org.openhab.habdroid.model.Widget
import org.openhab.habdroid.model.toColorTemperatureInKelvin
import org.openhab.habdroid.model.withValue
import org.openhab.habdroid.ui.widget.AutoHeightPlayerView
import org.openhab.habdroid.ui.widget.ContextMenuAwareRecyclerView
Expand All @@ -112,6 +113,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.asColorTemperatureInKelvinToColor
import org.openhab.habdroid.util.beautify
import org.openhab.habdroid.util.determineDataUsagePolicy
import org.openhab.habdroid.util.getChartTheme
Expand All @@ -120,6 +122,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 +242,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 +393,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 @@ -1643,6 +1648,7 @@ class WidgetAdapter(
View.OnLongClickListener {
private val upButton = itemView.findViewById<View>(R.id.up_button)
private val downButton = itemView.findViewById<View>(R.id.down_button)
private val selectColorButton = itemView.findViewById<ImageView>(R.id.select_color_button)

data class UpDownButtonState(
val item: Item?,
Expand All @@ -1656,7 +1662,6 @@ class WidgetAdapter(
b.setOnClickListener(this)
b.setOnLongClickListener(this)
}
val selectColorButton = itemView.findViewById<View>(R.id.select_color_button)
selectColorButton.setOnClickListener { handleRowClick() }
}

Expand All @@ -1670,6 +1675,14 @@ class WidgetAdapter(
downButton.tag = UpDownButtonState(widget.item, "OFF", "DECREASE")
}
super.bind(widget)

val hsv = widget.state?.asHsv
val color = hsv?.toColor()
if (color == null || hsv.value == 0F) {
selectColorButton.setImageResource(R.drawable.ic_palette_outline_themed_24dp)
} else {
selectColorButton.setImageDrawable(color.toColoredRoundedRect(selectColorButton.context))
}
}

override fun onClick(view: View) {
Expand Down Expand Up @@ -1702,6 +1715,30 @@ 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
?.toColorTemperatureInKelvin()
?.value
?.asColorTemperatureInKelvinToColor()
?.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 +1845,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,20 @@ 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.Widget
import org.openhab.habdroid.model.toColorTemperatureInKelvin
import org.openhab.habdroid.model.withValue
import org.openhab.habdroid.ui.widget.WidgetSlider
import org.openhab.habdroid.util.ColorPickerHelper
import org.openhab.habdroid.util.asColorTemperatureInKelvinToColor
import org.openhab.habdroid.util.asColorTemperatureToKelvin
import org.openhab.habdroid.util.parcelable

open class AbstractWidgetBottomSheet : BottomSheetDialogFragment() {
Expand Down Expand Up @@ -67,10 +74,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 +103,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 = widget.state?.asNumber?.toColorTemperatureInKelvin()?.withValue(value)
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.asColorTemperatureToKelvin()
val max = widget.maxValue.asColorTemperatureToKelvin()
val steps = 20
val positions = (0 until steps).map { 1F * it / steps }.toFloatArray()
val colors = positions
.map { it * (max - min) + min }
.map { it.asColorTemperatureInKelvinToColor() }
.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
45 changes: 45 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,44 @@ fun HttpUrl.toRelativeUrl(): String {
return this.toString().substring(base.toString().length - 1)
}

fun Float.asColorTemperatureToKelvin(): Float = if (this < 1000) {
// likely Mirek
1000000F / this
} else {
this
}

fun Float.asColorTemperatureInKelvinToColor(): 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
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
android:viewportHeight="24"
android:tint="?colorOnBackground">
<path
android:fillColor="#FF000000"
android:pathData="M12,22C6.49,22 2,17.51 2,12S6.49,2 12,2s10,4.04 10,9c0,3.31 -2.69,6 -6,6h-1.77c-0.28,0 -0.5,0.22 -0.5,0.5 0,0.12 0.05,0.23 0.13,0.33 0.41,0.47 0.64,1.06 0.64,1.67 0,1.38 -1.12,2.5 -2.5,2.5zM12,4c-4.41,0 -8,3.59 -8,8s3.59,8 8,8c0.28,0 0.5,-0.22 0.5,-0.5 0,-0.16 -0.08,-0.28 -0.14,-0.35 -0.41,-0.46 -0.63,-1.05 -0.63,-1.65 0,-1.38 1.12,-2.5 2.5,-2.5L16,15c2.21,0 4,-1.79 4,-4 0,-3.86 -3.59,-7 -8,-7z"/>
Expand Down
8 changes: 5 additions & 3 deletions mobile/src/main/res/layout/widgetlist_coloritem_buttons.xml
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@
android:contentDescription="@string/content_description_color_up"
app:icon="@drawable/ic_keyboard_arrow_up_themed_24dp" />

<Button
<ImageView
style="@style/openHAB.ActionButton"
android:id="@+id/select_color_button"
android:contentDescription="@string/content_description_open_color_wheel"
app:icon="@drawable/ic_palette_outline_themed_24dp" />
android:background="@null"
android:padding="8dp"
android:scaleType="centerInside"
android:contentDescription="@string/content_description_open_color_wheel" />

<Button
style="@style/openHAB.ActionButton"
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>