Skip to content
Open
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 @@ -361,6 +361,7 @@ class CarEditorFragment : BaseFragment() {
"car_smart_44_white_silver",
"car_smart_44_fl_black",
"car_zoe_black",
"car_egolf_white",
"car_vwup_black",
"car_vwup_blue",
"car_vwup_red",
Expand Down
120 changes: 119 additions & 1 deletion app/src/main/java/com/openvehicles/OVMS/ui2/pages/ClimateFragment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@ import android.view.ViewGroup
import android.view.animation.Animation
import android.view.animation.AnimationUtils
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import android.widget.Toast
import androidx.core.content.ContextCompat
import com.google.android.material.slider.Slider
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.openvehicles.OVMS.R
Expand All @@ -33,6 +35,14 @@ class ClimateFragment : BaseFragment(), OnResultCommandListener {

private lateinit var climateActionsAdapter: QuickActionsAdapter

/**
* Right-hand button column. Used only where the card carries the target
* temperature slider (VW e-Golf): the start button sits there because that
* is where the thumb falls with the phone in the right hand. Everywhere
* else it stays empty and gone, so those vehicles keep the layout they had.
*/
private lateinit var climateActionsRightAdapter: QuickActionsAdapter




Expand All @@ -52,10 +62,80 @@ class ClimateFragment : BaseFragment(), OnResultCommandListener {
climateActionsRecyclerView.layoutManager = LinearLayoutManager(requireContext(), LinearLayoutManager.VERTICAL, false)
climateActionsRecyclerView.adapter = climateActionsAdapter

val rightRecyclerView = findViewById(R.id.climateActionsRight) as RecyclerView
climateActionsRightAdapter = QuickActionsAdapter(context)
rightRecyclerView.layoutManager = LinearLayoutManager(requireContext(), LinearLayoutManager.VERTICAL, false)
rightRecyclerView.adapter = climateActionsRightAdapter

initialiseTargetTempSlider()
initialiseCarRendering(carData)
initialiseClimateControls(carData)
}

/**
* Target temperature for pre-conditioning.
*
* Only shown for vehicles whose module can set it — currently the VW e-Golf,
* where the value lives in the car's stored BatteryControl profile and the
* module writes it back with `xvg cctemp`. Set up once here rather than in
* [initialiseClimateControls], which runs again on every data update and
* would otherwise stack listeners.
*/
private fun initialiseTargetTempSlider() {
val group = findViewById(R.id.ccTempGroup) as LinearLayout
if (carData?.car_type != "VWEG") {
group.visibility = View.GONE
return
}
group.visibility = View.VISIBLE

val slider = findViewById(R.id.ccTempSlider) as Slider
slider.addOnChangeListener { _, value, _ -> showTargetTemp(value) }
slider.addOnSliderTouchListener(object : Slider.OnSliderTouchListener {
override fun onStartTrackingTouch(s: Slider) {}
override fun onStopTrackingTouch(s: Slider) {
// Send on release only — sending while dragging would put a write
// on the car's comfort bus for every step.
sendCommand(
getString(R.string.climate_target_temp, formatTemp(s.value)),
"7,xvg cctemp " + formatTemp(s.value),
this@ClimateFragment
)
}
})
showTargetTemp(slider.value)

// The target temperature is not carried by the v2 protocol, so it cannot
// come in with the metrics — ask the module for it when the tab opens.
sendCommand("", "7,xvg ccstatus", this)
}

private fun formatTemp(value: Float): String = DecimalFormat("0.0").format(value)

private fun showTargetTemp(value: Float) {
val label = findViewById(R.id.ccTempLabel) as TextView
// Just the value — the slider directly beneath it makes clear what it is.
label.text = formatTemp(value) + " °C"
}

/**
* Applies `cctemp=22.0 current=32 valid=1` as reported by the module.
*
* `valid=0` means the module has not read the car's profile yet, so the
* value carries no information — leave the slider where it is rather than
* snapping it to a placeholder.
*/
private fun applyClimateStatus(text: String) {
if (Regex("valid=0").containsMatchIn(text)) return
Regex("cctemp=([0-9.]+)").find(text)?.groupValues?.get(1)?.toFloatOrNull()?.let {
val slider = findViewById(R.id.ccTempSlider) as Slider
if (it >= slider.valueFrom && it <= slider.valueTo) {
slider.value = it
showTargetTemp(it)
}
}
}

private fun initialiseCarRendering(carData: CarData?) {
val carImageView = findViewById(R.id.battIndicatorImg) as ImageView
val layers = carData?.let { CarRenderingUtils.getTopDownCarLayers(it, requireContext(), climate = true, heat = carData.car_hvac_on) }
Expand Down Expand Up @@ -167,13 +247,32 @@ class ClimateFragment : BaseFragment(), OnResultCommandListener {

climateActionsAdapter.mData.clear()
climateActionsAdapter.setCarData(carData)
climateActionsAdapter.mData += ClimateQuickAction({getService()})
climateActionsRightAdapter.mData.clear()
climateActionsRightAdapter.setCarData(carData)

val leftColumn = findViewById(R.id.climateActions) as RecyclerView
val rightColumn = findViewById(R.id.climateActionsRight) as RecyclerView
if (carData?.car_type == "VWEG") {
// Start on the right, where the thumb falls with the phone in the right
// hand. There is deliberately no "climatise without the cable" button:
// that profile bit is owned by the module, which sets it for a climate
// command and clears it for a charge — a user-facing toggle would fight
// the firmware and show a state that changes under the user's hands.
climateActionsRightAdapter.mData += ClimateQuickAction({getService()})
leftColumn.visibility = View.GONE
rightColumn.visibility = View.VISIBLE
} else {
climateActionsAdapter.mData += ClimateQuickAction({getService()})
leftColumn.visibility = View.VISIBLE
rightColumn.visibility = View.GONE
}
if (carData?.car_type in listOf("NL","SE","SQ","VWUP","VWUP.T26","RZ","RZ2")
|| carData?.car_type.orEmpty().startsWith("VA")
|| carData?.car_type.orEmpty().startsWith("VB")
|| carData?.car_type.orEmpty().startsWith("OAE"))
climateActionsAdapter.mData += ClimateScheduleQuickAction({getService()})
climateActionsAdapter.notifyDataSetChanged()
climateActionsRightAdapter.notifyDataSetChanged()
}

override fun update(carData: CarData?) {
Expand All @@ -187,6 +286,25 @@ class ClimateFragment : BaseFragment(), OnResultCommandListener {
val resCode = result[1].toInt()
val resText = if (result.size > 2) result[2] else ""
val cmdMessage = getSentCommandMessage(result[0])
// Status reply from `xvg ccstatus`: sync slider and button.
if (resCode == 0 && resText.contains("cctemp=")) {
applyClimateStatus(resText)
cancelCommand()
return
}
// Anything else we sent for this car changes the stored profile, and the
// module may well have refused it — a sleeping car cannot be written to.
// Never leave the slider showing a value the car does not hold: ask what
// it actually is now. The reply lands in the branch above.
// result[0] is the command *code*, not the text we sent — BaseFragment
// keys its message map on command.split(",")[0]. 7 is "execute command",
// and for this vehicle the only ones this tab sends are the xvg writes.
if (carData?.car_type == "VWEG" && result[0] == "7") {
if (resText.isNotEmpty())
Toast.makeText(activity, resText, Toast.LENGTH_LONG).show()
sendCommand("", "7,xvg ccstatus", this)
return
}
val context: Context? = activity
if (context != null) {
when (resCode) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ class CarEditorFragment : BaseFragment() {
VehicleType("smart_eq", "Smart EQ (ForTwo)", listOf("red", "black", "white", "fl_black", "fl_white", "fl_red", "cabrio_black", "cabrio_crystalwhite", "cabrio_grey", "cabrio_lavaorange")),
VehicleType("smart_44", "Smart ForFour", listOf("black", "white_silver", "fl_black")),
VehicleType("vwup", "VW e-Up", listOf("black", "blue", "red", "silver", "white", "yellow")),
VehicleType("egolf", "VW e-Golf", listOf("white")),
VehicleType("zoe", "Renault Zoe", listOf("black", "brown", "grey", "hellblau", "lila", "red", "white", "ytriumgrau")),
VehicleType("mgzs", "MG ZS EV", listOf("white", "blue", "lightblue", "red", "black")),
VehicleType("edeliver3", "Maxus eDeliver 3", listOf("white")),
Expand Down Expand Up @@ -818,6 +819,7 @@ class CarEditorFragment : BaseFragment() {
"map_car_twizy_snowwhiteandflameorange",
"map_car_twizy_snowwhiteandurbanblue",
"map_car_twizy_snowwhitewithblack",
"map_car_egolf_white",
"map_car_vwup_black",
"map_car_vwup_blue",
"map_car_vwup_red",
Expand Down Expand Up @@ -897,6 +899,7 @@ class CarEditorFragment : BaseFragment() {
"car_thinkcity_classicblack",
"car_thinkcity_skyblue",
"car_twizy",
"car_egolf_white",
"car_vwup_black",
"car_vwup_blue",
"car_vwup_red",
Expand Down Expand Up @@ -982,6 +985,7 @@ class CarEditorFragment : BaseFragment() {
"car_smart_44_white_silver",
"car_smart_44_fl_black",
"car_zoe_black",
"car_egolf_white",
"car_vwup_black",
"car_vwup_blue",
"car_vwup_red",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package com.openvehicles.OVMS.ui2.rendering

import android.content.Context
import android.graphics.Canvas
import android.graphics.ColorFilter
import android.graphics.PixelFormat
import android.graphics.Rect
import android.graphics.drawable.Drawable
import android.graphics.drawable.Animatable
import android.graphics.drawable.VectorDrawable
Expand Down Expand Up @@ -310,18 +314,82 @@ object CarRenderingUtils {
finalDrawable.start()
}

layers = layers.plus(finalDrawable)
layers = layers.plus(fitToBase(finalDrawable, layers.first()))
} else {
Log.e("DrawableError", "Could not load R.drawable.avd_animated_ac_arrows")
val staticArrows = ContextCompat.getDrawable(context, R.drawable.topview_ac_arrows)
if (staticArrows != null) {
val vectorDrawable = staticArrows.mutate() as VectorDrawable
vectorDrawable.setTint(tintColor)
layers = layers.plus(vectorDrawable)
layers = layers.plus(fitToBase(vectorDrawable, layers.first()))
}
}
}

return layers
}

/**
* The AC arrows are a vector sized in dp and drawn for the Leaf's 320x560 top
* view. Every other car image is a nodpi bitmap, so the arrows come out far
* bigger in pixels, the LayerDrawable takes their size as its own, and the car
* underneath gets stretched to their aspect ratio. Reporting the base image's
* size keeps the composite unchanged whether the overlay is there or not.
*/
private fun fitToBase(overlay: Drawable, base: Drawable): Drawable {
if (base.intrinsicWidth <= 0 || base.intrinsicHeight <= 0) return overlay
return FittedOverlayDrawable(overlay, base.intrinsicWidth, base.intrinsicHeight)
}

private class FittedOverlayDrawable(
private val inner: Drawable,
private val width: Int,
private val height: Int
) : Drawable(), Drawable.Callback {

init {
inner.callback = this
}

override fun getIntrinsicWidth() = width

override fun getIntrinsicHeight() = height

override fun onBoundsChange(bounds: Rect) {
// Fit centred, keeping the overlay's own aspect ratio, so the arrows
// stay over the cabin instead of being stretched across the car.
val iw = if (inner.intrinsicWidth > 0) inner.intrinsicWidth else bounds.width()
val ih = if (inner.intrinsicHeight > 0) inner.intrinsicHeight else bounds.height()
if (iw <= 0 || ih <= 0) {
inner.bounds = bounds
return
}
val scale = minOf(bounds.width() / iw.toFloat(), bounds.height() / ih.toFloat())
val w = (iw * scale).toInt()
val h = (ih * scale).toInt()
val left = bounds.left + (bounds.width() - w) / 2
val top = bounds.top + (bounds.height() - h) / 2
inner.setBounds(left, top, left + w, top + h)
}

override fun draw(canvas: Canvas) = inner.draw(canvas)

override fun setAlpha(alpha: Int) {
inner.alpha = alpha
}

override fun setColorFilter(colorFilter: ColorFilter?) {
inner.colorFilter = colorFilter
}

@Deprecated("Deprecated in Drawable", ReplaceWith("PixelFormat.TRANSLUCENT"))
override fun getOpacity() = PixelFormat.TRANSLUCENT

override fun invalidateDrawable(who: Drawable) = invalidateSelf()

override fun scheduleDrawable(who: Drawable, what: Runnable, `when`: Long) =
scheduleSelf(what, `when`)

override fun unscheduleDrawable(who: Drawable, what: Runnable) = unscheduleSelf(what)
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading