Skip to content

Commit

Permalink
[BasicUI] Implement new sitemap element colortemperaturepicker
Browse files Browse the repository at this point in the history
Related to #3891

Signed-off-by: Laurent Garnier <lg.hc@free.fr>
  • Loading branch information
lolodomo committed Nov 1, 2024
1 parent 0a58424 commit 2450372
Show file tree
Hide file tree
Showing 6 changed files with 526 additions and 2 deletions.
31 changes: 31 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,31 @@
<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-state="%state%"
data-unit="%unit%"
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>
</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,194 @@
/**
* 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 BigDecimal MIN_TEMPERATURE_MIRED = BigDecimal.valueOf(100);
private static final BigDecimal MAX_TEMPERATURE_MIRED = BigDecimal.valueOf(1000);

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;

boolean validItemType = false;
boolean validUnit = false;
BigDecimal current = null;
Unit<?> unit = null;
BigDecimal min = ctp.getMinValue();
BigDecimal max = ctp.getMaxValue();
String gradientColors = null;

String itemType = null;
String itemName = w.getItem();
if (itemName != null) {
try {
Item item = itemUIRegistry.getItem(itemName);
itemType = item.getType();
validItemType = (CoreItemFactory.NUMBER + ":Temperature").equals(itemType);

if (validItemType) {
State state = itemUIRegistry.getState(w);
if (state instanceof QuantityType<?> quantityTypeState) {
current = quantityTypeState.toBigDecimal();
}
unit = UnitUtils.parseUnit(getUnitForWidget(w));
if (unit == null && state instanceof QuantityType<?> quantityTypeState) {
unit = quantityTypeState.getUnit();
}
validUnit = unit == Units.KELVIN || unit == Units.MIRED;
if (validUnit) {
// Search the min and max in the item state description if not defined in the widget
if (min == null) {
min = getMinimumFromStateDescription(item.getStateDescription(), unit == Units.KELVIN,
unit == Units.KELVIN ? MIN_TEMPERATURE_KELVIN : MIN_TEMPERATURE_MIRED);
}
if (max == null) {
max = getMaximumFromStateDescription(item.getStateDescription(), unit == Units.KELVIN,
unit == Units.KELVIN ? MAX_TEMPERATURE_KELVIN : MAX_TEMPERATURE_MIRED);
}

// Get RGB values to create a gradient in mirek
double minMirek = unit == Units.KELVIN ? 1000000.0 / max.doubleValue() : min.doubleValue();
double maxMirek = unit == Units.KELVIN ? 1000000.0 / min.doubleValue() : max.doubleValue();
logger.debug("ColortemppickerRenderer current={} unit={} min={} max={} minMirek={} maxMirek={}",
current, unit, min, max, minMirek, maxMirek);
StringBuilder gradientBuilder = new StringBuilder();
for (int percent = 0; percent <= 100; percent += 5) {
double valueMirek = maxMirek - (maxMirek - minMirek) * percent / 100.0;
double valueKelvin = 1000000.0 / valueMirek;
try {
int[] rgb = ColorUtil.hsbToRgb(ColorUtil.xyToHsb(ColorUtil.kelvinToXY(valueKelvin)));
logger.debug("Gradient {}%: {} mirek => {} K => RGB {} {} {}", percent, valueMirek,
valueKelvin, rgb[0], rgb[1], rgb[2]);
gradientBuilder.append("(%d,%d,%d,1) %d%%,".formatted(rgb[0], rgb[1], rgb[2], percent));
} catch (IllegalArgumentException | IndexOutOfBoundsException e) {
logger.debug("Can't get RGB for {} Kelvin, ignoring it", valueKelvin);
}
}
if (gradientBuilder.length() > 1) {
gradientColors = gradientBuilder.substring(0, gradientBuilder.length() - 1);
}
} 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(validItemType && validUnit ? "colortemppicker" : "text");

// Should be called before preprocessSnippet
snippet = snippet.replace("%state%", current == null ? "UNDEF" : String.valueOf(current.doubleValue()));

snippet = preprocessSnippet(snippet, w, true);
snippet = snippet.replace("%unit%", unit == null ? "" : unit.toString());
snippet = snippet.replace("%minValue%", min == null ? "" : String.valueOf(min.doubleValue()));
snippet = snippet.replace("%maxValue%", max == null ? "" : String.valueOf(max.doubleValue()));
snippet = snippet.replace("%gradientColors%", gradientColors == null ? "" : gradientColors);

// Process the color tags
snippet = processColor(w, snippet);

sb.append(snippet);
return ECollections.emptyEList();
}

private BigDecimal getMinimumFromStateDescription(@Nullable StateDescription stateDescription,
boolean targetUnitInKelvin, BigDecimal defaultValue) {
if (stateDescription == null) {
return defaultValue;
} else {
BigDecimal value = stateDescription.getMinimum();
return value == null ? defaultValue
: (isUnitinKelvin(stateDescription) == targetUnitInKelvin ? value
: BigDecimal.valueOf(1000000.0 / value.doubleValue()));
}
}

private BigDecimal getMaximumFromStateDescription(@Nullable StateDescription stateDescription,
boolean targetUnitInKelvin, BigDecimal defaultValue) {
if (stateDescription == null) {
return defaultValue;
} else {
BigDecimal value = stateDescription.getMaximum();
return value == null ? defaultValue
: (isUnitinKelvin(stateDescription) == targetUnitInKelvin ? value
: BigDecimal.valueOf(1000000.0 / value.doubleValue()));
}
}

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
88 changes: 87 additions & 1 deletion bundles/org.openhab.ui.basic/web-src/_layout.scss
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,7 @@
}
&__setpoint,
&__colorpicker,
&__colortemppicker,
&__rollerblind {
.mdl-button {
min-width: 0;
Expand Down Expand Up @@ -458,7 +459,8 @@
text-transform: uppercase;
}
&--setpoint,
&--rollerblind {
&--rollerblind,
&--colortemppicker {
padding: 0 10px 0 5px;
}
&--group {
Expand Down Expand Up @@ -755,6 +757,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;
Expand Down Expand Up @@ -805,6 +885,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 {
Expand Down
Loading

0 comments on commit 2450372

Please sign in to comment.