[BasicUI] Implement new sitemap element Colortemperaturepicker (#2851)

Related to openhab/openhab-core#3891

Signed-off-by: Laurent Garnier <lg.hc@free.fr>
pull/2863/head
lolodomo 2024-11-09 17:12:18 +01:00 committed by GitHub
parent 1e4e69e018
commit 9e1a25df68
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 561 additions and 2 deletions

View File

@ -0,0 +1,37 @@
<div class="mdl-form__row mdl-cell mdl-cell--%cells%-col mdl-cell--%cells_tablet%-col-tablet %visibility_class%">
<span class="mdl-form__icon">
%icon_snippet%
</span>
<span class="mdl-form__label">
%label%
</span>
<span class="mdl-form__value mdl-form__value--colortemppicker">
%value%
</span>
<div
class="mdl-form__control mdl-form__colortemppicker"
data-control-type="colortemppicker"
data-item="%item%"
data-has-value="%has_value%"
data-state="%currentKelvin%"
data-color="%currentRGB%"
data-widget-id="%widget_id%"
data-icon-with-state="%icon_with_state%"
data-label-color="%label_color%"
data-value-color="%value_color%"
data-icon-color="%icon_color%"
data-min="%minValue%"
data-max="%maxValue%"
data-gradient-colors="%gradientColors%"
>
<button class="mdl-button mdl-button--raised mdl-js-button mdl-js-ripple-effect mdl-form__colortemppicker--pick">
<!-- colorize -->
<i class="material-icons">&#xE3B8;</i>
</button>
<span class="mdl-form__colortemppicker--preview">
<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<circle cx="50" cy="50" r="44" fill="Gray" stroke="LightGray" stroke-width="6" />
</svg>
</span>
</div>
</div>

View File

@ -53,6 +53,16 @@
</div>
</div>
</script>
<script type="text/html" id="template-colortemppicker">
<div class="colortemppicker">
<div class="colortemppicker__controls">
<input class="colortemppicker__input" type="range" min="1000" max="10000" tabindex="0"/>
</div>
<div class="colortemppicker__buttons">
<button class="mdl-button mdl-button--colored mdl-js-button mdl-js-ripple-effect">OK</button>
</div>
</div>
</script>
<script type="text/html" id="template-offline-notify">
<div class="mdl-notify">
<div class="mdl-notify__text">

View File

@ -0,0 +1,214 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.ui.basic.internal.render;
import java.math.BigDecimal;
import javax.measure.Unit;
import org.eclipse.emf.common.util.ECollections;
import org.eclipse.emf.common.util.EList;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.i18n.LocaleProvider;
import org.openhab.core.i18n.TranslationProvider;
import org.openhab.core.items.Item;
import org.openhab.core.items.ItemNotFoundException;
import org.openhab.core.library.CoreItemFactory;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.unit.Units;
import org.openhab.core.model.sitemap.sitemap.Colortemperaturepicker;
import org.openhab.core.model.sitemap.sitemap.Widget;
import org.openhab.core.types.State;
import org.openhab.core.types.StateDescription;
import org.openhab.core.types.util.UnitUtils;
import org.openhab.core.ui.items.ItemUIRegistry;
import org.openhab.core.util.ColorUtil;
import org.openhab.ui.basic.render.RenderException;
import org.openhab.ui.basic.render.WidgetRenderer;
import org.osgi.framework.BundleContext;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* <p>
* This is an implementation of the {@link WidgetRenderer} interface, which can produce HTML code for
* Colortemperaturepicker widgets.
*
* @author Laurent Garnier - Initial contribution
*/
@Component(service = WidgetRenderer.class)
@NonNullByDefault
public class ColortemppickerRenderer extends AbstractWidgetRenderer {
private static final BigDecimal MIN_TEMPERATURE_KELVIN = BigDecimal.valueOf(1000);
private static final BigDecimal MAX_TEMPERATURE_KELVIN = BigDecimal.valueOf(10000);
private static final double GRADIENT_INCREMENT_PERCENT = 2.5;
private final Logger logger = LoggerFactory.getLogger(ColortemppickerRenderer.class);
@Activate
public ColortemppickerRenderer(final BundleContext bundleContext, final @Reference TranslationProvider i18nProvider,
final @Reference ItemUIRegistry itemUIRegistry, final @Reference LocaleProvider localeProvider) {
super(bundleContext, i18nProvider, itemUIRegistry, localeProvider);
}
@Override
public boolean canRender(Widget w) {
return w instanceof Colortemperaturepicker;
}
@Override
public EList<Widget> renderWidget(Widget w, StringBuilder sb, String sitemap) throws RenderException {
Colortemperaturepicker ctp = (Colortemperaturepicker) w;
BigDecimal currentK = null;
String currentRGB = null;
Unit<?> unit = UnitUtils.parseUnit(getUnitForWidget(w));
BigDecimal minK = MIN_TEMPERATURE_KELVIN;
BigDecimal maxK = MAX_TEMPERATURE_KELVIN;
String[] colorsRGB = null;
String itemType = null;
String itemName = w.getItem();
if (itemName != null) {
try {
Item item = itemUIRegistry.getItem(itemName);
itemType = item.getType();
if ((CoreItemFactory.NUMBER + ":Temperature").equals(itemType)) {
State state = itemUIRegistry.getState(w);
if (state instanceof QuantityType<?> quantity) {
if (unit == null) {
unit = quantity.getUnit();
}
quantity = quantity.toInvertibleUnit(Units.KELVIN);
if (quantity != null) {
currentK = quantity.toBigDecimal();
try {
int[] rgb = ColorUtil
.hsbToRgb(ColorUtil.xyToHsb(ColorUtil.kelvinToXY(currentK.doubleValue())));
logger.debug("Current {} K => RGB {} {} {}", currentK, rgb[0], rgb[1], rgb[2]);
currentRGB = "#%02x%02x%02x".formatted(rgb[0], rgb[1], rgb[2]);
} catch (IllegalArgumentException | IndexOutOfBoundsException e) {
logger.debug("Can't get RGB for {} Kelvin, bypassing color gradient!", currentK);
}
}
}
if (unit == Units.KELVIN || unit == Units.MIRED) {
minK = getMinimumInKelvin(ctp, unit, item.getStateDescription());
maxK = getMaximumInKelvin(ctp, unit, item.getStateDescription());
logger.debug("ColortemppickerRenderer current={} unit={} min={} max={}", currentK, unit, minK,
maxK);
double valueKelvin = 0d;
try {
colorsRGB = new String[(int) Math.round(100 / GRADIENT_INCREMENT_PERCENT) + 1];
for (int i = 0; Math.round(i * GRADIENT_INCREMENT_PERCENT) <= 100; i++) {
valueKelvin = (maxK.doubleValue() - minK.doubleValue()) * i * GRADIENT_INCREMENT_PERCENT
/ 100.0 + minK.doubleValue();
int[] rgb = ColorUtil.hsbToRgb(ColorUtil.xyToHsb(ColorUtil.kelvinToXY(valueKelvin)));
logger.debug("Gradient {}%: {} K => RGB {} {} {}", i * GRADIENT_INCREMENT_PERCENT,
valueKelvin, rgb[0], rgb[1], rgb[2]);
colorsRGB[i] = "#%02x%02x%02x".formatted(rgb[0], rgb[1], rgb[2]);
}
} catch (IllegalArgumentException | IndexOutOfBoundsException e) {
logger.debug("Can't get RGB for {} Kelvin, bypassing color gradient!", valueKelvin);
colorsRGB = null;
}
} else {
logger.warn("Invalid unit {} for Colortemperaturepicker element", unit);
}
} else {
logger.warn("Invalid item type {} for Colortemperaturepicker element", itemType);
}
} catch (ItemNotFoundException e) {
}
}
String snippet = getSnippet((CoreItemFactory.NUMBER + ":Temperature").equals(itemType)
&& (unit == Units.KELVIN || unit == Units.MIRED) ? "colortemppicker" : "text");
snippet = preprocessSnippet(snippet, w, true);
snippet = snippet.replace("%currentKelvin%", currentK == null ? "" : String.valueOf(currentK.doubleValue()));
snippet = snippet.replace("%currentRGB%", currentRGB == null ? "" : currentRGB);
snippet = snippet.replace("%minValue%", String.valueOf(minK.doubleValue()));
snippet = snippet.replace("%maxValue%", String.valueOf(maxK.doubleValue()));
snippet = snippet.replace("%gradientColors%", colorsRGB == null ? "" : String.join(",", colorsRGB));
// Process the color tags
snippet = processColor(w, snippet);
sb.append(snippet);
return ECollections.emptyEList();
}
private BigDecimal getMinimumInKelvin(Colortemperaturepicker widget, Unit<?> widgetUnit,
@Nullable StateDescription stateDescription) {
BigDecimal min = null;
BigDecimal val;
if (widgetUnit == Units.KELVIN) {
min = widget.getMinValue();
} else {
val = widget.getMaxValue();
QuantityType<?> quantity = val == null ? null
: QuantityType.valueOf(val.doubleValue(), Units.MIRED).toInvertibleUnit(Units.KELVIN);
min = quantity == null ? null : quantity.toBigDecimal();
}
// Search the min in the item state description if not defined in the widget
if (min == null && stateDescription != null) {
if (isUnitInKelvin(stateDescription)) {
min = stateDescription.getMinimum();
} else {
val = stateDescription.getMaximum();
QuantityType<?> quantity = val == null ? null
: QuantityType.valueOf(val.doubleValue(), Units.MIRED).toInvertibleUnit(Units.KELVIN);
min = quantity == null ? null : quantity.toBigDecimal();
}
}
return min != null ? min : MIN_TEMPERATURE_KELVIN;
}
private BigDecimal getMaximumInKelvin(Colortemperaturepicker widget, Unit<?> widgetUnit,
@Nullable StateDescription stateDescription) {
BigDecimal max = null;
BigDecimal val;
if (widgetUnit == Units.KELVIN) {
max = widget.getMaxValue();
} else {
val = widget.getMinValue();
QuantityType<?> quantity = val == null ? null
: QuantityType.valueOf(val.doubleValue(), Units.MIRED).toInvertibleUnit(Units.KELVIN);
max = quantity == null ? null : quantity.toBigDecimal();
}
// Search the max in the item state description if not defined in the widget
if (max == null && stateDescription != null) {
if (isUnitInKelvin(stateDescription)) {
max = stateDescription.getMaximum();
} else {
val = stateDescription.getMinimum();
QuantityType<?> quantity = val == null ? null
: QuantityType.valueOf(val.doubleValue(), Units.MIRED).toInvertibleUnit(Units.KELVIN);
max = quantity == null ? null : quantity.toBigDecimal();
}
}
return max != null ? max : MAX_TEMPERATURE_KELVIN;
}
private boolean isUnitInKelvin(StateDescription stateDescription) {
// If no pattern or pattern with no unit, assume unit for min and max is Kelvin
Unit<?> unit = UnitUtils.parseUnit(stateDescription.getPattern());
return unit == null || unit == Units.KELVIN;
}
}

View File

@ -174,7 +174,7 @@ public class WebAppServlet extends HttpServlet {
String label = sitemap.getLabel() != null ? sitemap.getLabel() : sitemapName;
EList<Widget> children = renderer.getItemUIRegistry().getChildren(sitemap);
result.append(renderer.processPage(sitemapName, sitemapName, label, children, async));
} else if (!"Colorpicker".equals(widgetId)) {
} else if (!"Colorpicker".equals(widgetId) && !"Colortemperaturepicker".equals(widgetId)) {
// we are on some subpage, so we have to render the children of the widget that has been selected
if (subscriptionId != null) {
if (subscriptions.exists(subscriptionId)) {

View File

@ -341,12 +341,20 @@
}
&__setpoint,
&__colorpicker,
&__colortemppicker,
&__rollerblind {
.mdl-button {
min-width: 0;
padding-top: 6px;
}
}
&__colortemppicker--preview {
svg {
width: 36px;
height: 36px;
object-fit: contain;
}
}
&__header {
padding-left: 8px;
padding-right: $form-row-desktop-padding;
@ -458,7 +466,8 @@
text-transform: uppercase;
}
&--setpoint,
&--rollerblind {
&--rollerblind,
&--colortemppicker {
padding: 0 10px 0 5px;
}
&--group {
@ -755,6 +764,84 @@ $colorpicker-mobile-size: 270px;
position: relative;
}
// Colortemppicker
$colortemppicker-thumb-width: 20px;
$colortemppicker-slider-height: 200px;
$colortemppicker-desktop-size: 500px;
$colortemppicker-mobile-size: 270px;
@mixin colortemppicker-slider-track() {
display: block;
height: $colortemppicker-slider-height;
border-radius: 0;
background: transparent;
color: transparent;
border: none;
}
@mixin colortemppicker-slider-thumb() {
-webkit-appearance: none;
width: $colortemppicker-thumb-width;
height: $colortemppicker-slider-height;
box-sizing: border-box;
-moz-box-sizing: border-box;
border-radius: 0;
background: transparent;
border: 3px solid #aaa;
}
.colortemppicker {
&__input {
-webkit-appearance: none;
position: relative;
padding: 0;
margin: 0;
border: 1px solid $item-separator-color;
width: $colortemppicker-desktop-size - 30px;
@media screen and (max-width: $layout-mobile-size-threshold) {
width: $colortemppicker-mobile-size - 30px;
}
height: $colortemppicker-slider-height;
&::-ms-track {
@include colortemppicker-slider-track();
}
&::-webkit-slider-runnable-track {
@include colortemppicker-slider-track();
}
&::-moz-range-track {
@include colortemppicker-slider-track();
}
&::-moz-range-thumb {
@include colortemppicker-slider-thumb();
}
&::-webkit-slider-thumb {
@include colortemppicker-slider-thumb();
}
&::-ms-thumb {
@include colortemppicker-slider-thumb();
}
&::-ms-fill-upper {
background: none;
}
&::-ms-fill-lower {
background: none;
}
&::-ms-ticks {
background: none;
display: none;
}
}
&__controls {
position: relative;
padding: 15px;
border-bottom: 1px solid $item-separator-color;
}
&__buttons {
padding: 5px;
}
position: relative;
}
h4 {
html.ui-bigger-font & {
font-size: 28px;
@ -805,6 +892,12 @@ h5 {
max-width: $colorpicker-mobile-size;
}
}
&--colortemppicker {
max-width: $colortemppicker-desktop-size;
@media screen and (max-width: $layout-mobile-size-threshold) {
max-width: $colortemppicker-mobile-size;
}
}
}
.mdl-notify {

View File

@ -2251,6 +2251,199 @@
smarthome.eventMapper.map(eventMap);
}
/* class Colortemppicker */
function Colortemppicker(parentNode, min, max, current, gradientColors, callback) {
var
_t = this,
lastColorTemperatureSent = null;
_t.container = parentNode;
_t.isBeingChanged = false;
_t.colortemppicker = _t.container.querySelector(o.colortemppicker.colortemppicker);
_t.slider = _t.container.querySelector(o.colortemppicker.slider);
if (gradientColors !== "") {
_t.slider.style.background = "linear-gradient(to right, " + gradientColors + ")";
}
_t.slider.min = Math.round(min);
_t.slider.max = Math.round(max);
function setCorTemperature(value) {
if (isNaN(value)) {
_t.value = NaN;
_t.slider.value = _t.slider.min;
} else {
if (value < _t.slider.min) {
_t.value = _t.slider.min;
} else if (value > _t.slider.max) {
_t.value = _t.slider.max;
} else {
_t.value = value;
}
_t.slider.value = _t.value;
}
if (_t.slider.MaterialSlider) {
_t.slider.MaterialSlider.change();
}
}
setCorTemperature(current);
_t.button = _t.container.querySelector(o.controlButton);
componentHandler.upgradeElement(_t.button, "MaterialButton");
componentHandler.upgradeElement(_t.button, "MaterialRipple");
_t.debounceProxy = new DebounceProxy(function() {
if (_t.value !== lastColorTemperatureSent) {
callback(_t.value);
lastColorTemperatureSent = _t.value;
}
}, 200);
_t.updateColorTemperature = function(value) {
if (_t.isBeingChanged) {
return;
}
setCorTemperature(value);
};
function onSliderChangeStart() {
lastColorTemperatureSent = null;
}
function onSliderChange() {
_t.debounceProxy.finish();
_t.value = _t.slider.value;
if (_t.value !== lastColorTemperatureSent) {
callback(_t.value);
lastColorTemperatureSent = _t.value;
}
}
function onSliderInput() {
_t.value = _t.slider.value;
_t.debounceProxy.call();
}
var
eventMap = [
[ _t.slider, "touchstart", onSliderChangeStart ],
[ _t.slider, "mousedown", onSliderChangeStart ],
[ _t.slider, "input", onSliderInput ],
[ _t.slider, "change", onSliderChange ]
];
_t.destroy = function() {
smarthome.eventMapper.unmap(eventMap);
componentHandler.downgradeElements([ _t.button ]);
};
smarthome.eventMapper.map(eventMap);
}
/* class ControlColortemppicker extends Control */
function ControlColortemppicker(parentNode) {
Control.call(this, parentNode);
var
_t = this,
color;
_t.valueNode = _t.parentNode.parentNode.querySelector(o.formValue);
_t.hasValue = _t.parentNode.getAttribute("data-has-value") === "true";
_t.value = _t.parentNode.getAttribute("data-state") === "" ? NaN : parseFloat(_t.parentNode.getAttribute("data-state"));
_t.min = parseFloat(_t.parentNode.getAttribute("data-min"));
_t.max = parseFloat(_t.parentNode.getAttribute("data-max"));
_t.gradientColors = _t.parentNode.getAttribute("data-gradient-colors");
_t.colors = (_t.gradientColors === "" ? "Gray" : _t.gradientColors).split(",");
_t.modalControl = null;
_t.buttonPick = _t.parentNode.querySelector(o.colortemppicker.pick);
_t.circle = _t.parentNode.querySelector(o.colortemppicker.circle);
color = _t.parentNode.getAttribute("data-color");
if (color !== "") {
_t.circle.setAttribute("fill", color);
}
_t.setValuePrivate = function(value, itemState) {
if (_t.hasValue) {
_t.valueNode.innerHTML = value;
}
// itemState contains value + unit in the display unit (in case unit is set in label pattern)
if (itemState === "NULL" || itemState === "UNDEF") {
_t.value = NaN;
} else if (itemState.indexOf(" ") > 0) {
var stateAndUnit = itemState.split(" ");
_t.value = parseFloat(stateAndUnit[0]);
_t.value = isNaN(_t.value) ? NaN : (stateAndUnit[1] === "K" ? _t.value : 1000000.0 / _t.value);
} else {
_t.value = itemState * 1;
}
// Set the color in the preview circle with the most approaching color used to generate the gradient
_t.circle.setAttribute("fill", (isNaN(_t.value) || _t.value < _t.min || _t.value > _t.max)
? "Gray"
: _t.colors[Math.round((_t.value - _t.min) * (_t.colors.length - 1) / (_t.max - _t.min))]);
if (_t.modalControl !== null) {
_t.modalControl.updateColorTemperature(_t.value);
}
};
function emitEvent(valueKelvin) {
var
command = valueKelvin + " K";
_t.parentNode.dispatchEvent(createEvent(
"control-change", {
item: _t.item,
value: command
}));
}
function onPick() {
var
button;
function onClick() {
_t.modal.hide();
}
_t.modal = new Modal(renderTemplate("template-colortemppicker"));
_t.modal.show();
_t.modal.container.classList.add(o.colortemppicker.modalClass);
_t.modal.onHide = function() {
button.removeEventListener("click", onClick);
_t.modalControl.destroy();
_t.modalControl = null;
_t.modal = null;
};
_t.modalControl = new Colortemppicker(_t.modal.container, _t.min, _t.max, _t.value, _t.gradientColors,
function(valueKelvin) {
emitEvent(valueKelvin);
}
);
button = _t.modal.container.querySelector(o.colortemppicker.button);
button.addEventListener("click", onClick);
}
var
eventMap = [
[ _t.buttonPick, "click", onPick ]
];
_t.destroy = function() {
smarthome.eventMapper.unmap(eventMap);
componentHandler.downgradeElements([ _t.buttonPick ]);
};
smarthome.eventMapper.map(eventMap);
}
/* class ControlSwitch extends Control */
function ControlSwitch(parentNode) {
Control.call(this, parentNode);
@ -2968,6 +3161,7 @@
case "setpoint":
case "rollerblind":
case "colorpicker":
case "colortemppicker":
case "buttons":
[].slice.call(e.querySelectorAll("button")).forEach(function(button) {
upgrade(button, "MaterialButton");
@ -3154,6 +3348,9 @@
case "colorpicker":
appendControl(new ControlColorpicker(e));
break;
case "colortemppicker":
appendControl(new ControlColortemppicker(e));
break;
case "mapview":
appendControl(new ControlMap(e));
break;
@ -3748,6 +3945,14 @@
colorpicker: ".colorpicker",
button: ".colorpicker__buttons > button"
},
colortemppicker: {
pick: ".mdl-form__colortemppicker--pick",
circle: ".mdl-form__colortemppicker--preview > svg > circle",
modalClass: "mdl-modal--colortemppicker",
slider: ".colortemppicker__input",
colortemppicker: ".colortemppicker",
button: ".colortemppicker__buttons > button"
},
image: {
legendButton: ".chart-legend-button",
periodButton: ".chart-period-button",