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

[BasicUI] Implement new sitemap element Colortemperaturepicker #2851

Merged
merged 1 commit into from
Nov 9, 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
37 changes: 37 additions & 0 deletions bundles/org.openhab.ui.basic/snippets-src/colortemppicker.html
Original file line number Diff line number Diff line change
@@ -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>
10 changes: 10 additions & 0 deletions bundles/org.openhab.ui.basic/snippets-src/main.html
Original file line number Diff line number Diff line change
Expand Up @@ -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">
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ protected void service(@NonNullByDefault({}) HttpServletRequest req, @NonNullByD
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)) {
Expand Down
Loading