Fix precision-related issues (#20917)

Resolves #20912

Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk>
This commit is contained in:
Jacob Laursen
2026-06-10 19:09:46 +02:00
committed by GitHub
parent 8212a6fbc6
commit 67394e2994
8 changed files with 147 additions and 14 deletions
@@ -12,6 +12,8 @@
*/
package org.openhab.io.hueemulation.internal;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@@ -48,6 +50,9 @@ import org.openhab.io.hueemulation.internal.dto.response.HueSuccessResponseState
@NonNullByDefault
public class StateUtils {
private static final BigDecimal BIG_DECIMAL_HUNDRED = BigDecimal.valueOf(100);
private static final BigDecimal BIG_DECIMAL_HUE_BRI_MAX = BigDecimal.valueOf(HueStateBulb.MAX_BRI);
/**
* Compute the hue state from a given item state and a device type.
*
@@ -65,7 +70,8 @@ public class StateUtils {
if (itemState instanceof HSBType hsbState) {
state = new HueStateColorBulb(hsbState);
} else if (itemState instanceof PercentType percentState) {
state = new HueStateColorBulb(percentState, percentState.intValue() > 0);
state = new HueStateColorBulb(percentState,
percentState.toBigDecimal().compareTo(BigDecimal.ZERO) > 0);
} else if (itemState instanceof OnOffType onOffState) {
state = new HueStateColorBulb(onOffState == OnOffType.ON);
} else {
@@ -76,9 +82,9 @@ public class StateUtils {
case WhiteTemperatureType:
if (itemState instanceof HSBType hsbState) {
PercentType brightness = hsbState.getBrightness();
state = new HueStateBulb(brightness, brightness.intValue() > 0);
state = new HueStateBulb(brightness, brightness.toBigDecimal().compareTo(BigDecimal.ZERO) > 0);
} else if (itemState instanceof PercentType percentState) {
state = new HueStateBulb(percentState, percentState.intValue() > 0);
state = new HueStateBulb(percentState, percentState.toBigDecimal().compareTo(BigDecimal.ZERO) > 0);
} else if (itemState instanceof OnOffType onOffState) {
state = new HueStateBulb(onOffState == OnOffType.ON);
} else {
@@ -170,7 +176,7 @@ public class StateUtils {
if (newState.bri != null) {
try {
state.as(HueStateBulb.class).bri = newState.bri;
command = new PercentType((int) (newState.bri * 100.0 / HueStateBulb.MAX_BRI + 0.5));
command = percentTypeFromHueBrightness(newState.bri);
successApplied.put("bri", newState.bri);
} catch (ClassCastException e) {
errorApplied.add("bri");
@@ -183,7 +189,7 @@ public class StateUtils {
if (newBri < 0 || newBri > HueStateBulb.MAX_BRI) {
throw new IllegalArgumentException();
}
command = new PercentType((int) (newBri * 100.0 / HueStateBulb.MAX_BRI + 0.5));
command = percentTypeFromHueBrightness(newBri);
successApplied.put("bri", newState.bri);
} catch (ClassCastException e) {
errorApplied.add("bri_inc");
@@ -349,6 +355,28 @@ public class StateUtils {
return command;
}
/**
* Converts a Hue brightness value (0-254) to a {@link PercentType} with two decimal places.
*
* @param brightness the Hue brightness value
* @return the corresponding PercentType
*/
public static PercentType percentTypeFromHueBrightness(int brightness) {
return new PercentType(BigDecimal.valueOf(brightness).multiply(BIG_DECIMAL_HUNDRED)
.divide(BIG_DECIMAL_HUE_BRI_MAX, 2, RoundingMode.HALF_UP));
}
/**
* Converts a {@link PercentType} to a Hue brightness value (1-254).
*
* @param percentValue the PercentType value
* @return the corresponding Hue brightness value
*/
public static int hueBrightnessFromPercentType(PercentType percentValue) {
return Math.max(1, percentValue.toBigDecimal().multiply(BIG_DECIMAL_HUE_BRI_MAX)
.divide(BIG_DECIMAL_HUNDRED, 0, RoundingMode.HALF_UP).intValue());
}
public static @Nullable DeviceType determineTargetType(ConfigStore cs, Item element) {
String category = element.getCategory();
String type = element.getType();
@@ -12,7 +12,9 @@
*/
package org.openhab.io.hueemulation.internal.dto;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.library.types.PercentType;
import org.openhab.io.hueemulation.internal.StateUtils;
/**
* Hue API state object
@@ -21,9 +23,9 @@ import org.openhab.core.library.types.PercentType;
* @author David Graeff - "extended color light bulbs" support
*
*/
@NonNullByDefault
public class HueStateBulb extends HueStatePlug {
// https://github.com/openhab/openhab-addons/issues/2881
// Apparently the maximum brightness is 254
public static final int MAX_BRI = 254;
public int bri = 0;
@@ -47,16 +49,15 @@ public class HueStateBulb extends HueStatePlug {
*/
public HueStateBulb(PercentType brightness, boolean on) {
super(on);
this.bri = Math.max(1, (int) (brightness.intValue() * MAX_BRI / 100.0 + 0.5));
this.bri = StateUtils.hueBrightnessFromPercentType(brightness);
}
public PercentType toBrightnessType() {
int bri = this.bri * 100 / MAX_BRI;
if (!this.on) {
bri = 0;
if (this.on) {
return StateUtils.percentTypeFromHueBrightness(this.bri);
} else {
return PercentType.ZERO;
}
return new PercentType(bri);
}
@Override
@@ -12,6 +12,7 @@
*/
package org.openhab.io.hueemulation.internal.dto;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.HSBType;
import org.openhab.core.library.types.PercentType;
@@ -24,6 +25,7 @@ import org.openhab.core.library.types.PercentType;
* @author Florian Lentz - added xy support
*
*/
@NonNullByDefault
public class HueStateColorBulb extends HueStateBulb {
public static final int MAX_HUE = 65535; // For extended color light bulbs
public int hue = 0;
@@ -71,7 +73,7 @@ public class HueStateColorBulb extends HueStateBulb {
* @param hsb Color information. Sets the hue state to "on" if brightness is > 0.
*/
public HueStateColorBulb(HSBType hsb) {
super(hsb.getBrightness(), hsb.getBrightness().intValue() > 0);
super(hsb.getBrightness(), hsb.getBrightness().compareTo(DecimalType.ZERO) > 0);
this.hue = (int) (hsb.getHue().intValue() * MAX_HUE / 360.0 + 0.5);
this.sat = (int) (hsb.getSaturation().intValue() * MAX_SAT / 100.0 + 0.5);
colormode = this.sat > 0 ? ColorMode.hs : ColorMode.ct;
@@ -12,6 +12,7 @@
*/
package org.openhab.io.hueemulation.internal.dto;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.library.types.OnOffType;
/**
@@ -20,6 +21,7 @@ import org.openhab.core.library.types.OnOffType;
* @author David Graeff - Initial contribution
*
*/
@NonNullByDefault
public class HueStatePlug extends AbstractHueState {
public boolean on;
@@ -35,4 +35,11 @@ public class HueStateChange {
public Integer sat_inc;
public List<Double> xy_inc;
public Integer ct_inc;
@Override
public String toString() {
return "on: " + on + ", bri: " + bri + ", hue: " + hue + ", sat: " + sat + ", effect: " + effect + ", ct: " + ct
+ ", alert: " + alert + ", xy: " + xy + ", transitiontime: " + transitiontime + ", bri_inc: " + bri_inc
+ ", hue_inc: " + hue_inc + ", sat_inc: " + sat_inc + ", xy_inc: " + xy_inc + ", ct_inc: " + ct_inc;
}
}
@@ -447,10 +447,16 @@ public class LightsAndGroups implements ConfigurationListener, RegistryChangeLis
"Invalid request: No state change data received!");
}
logger.debug("Received state change for light {}: {}", id, newState);
hueDevice.state = StateUtils.colorStateFromItemState(hueDevice.item.getState(), hueDevice.deviceType);
String itemUID = hueDevice.item.getUID();
List<HueResponse> responses = new ArrayList<>();
if (newState.on == Boolean.TRUE && newState.bri != null && newState.bri == 0) {
// The bulb should be on, but the brightness is 0. Skip the brightness change.
newState.bri = null;
}
Command command = StateUtils.computeCommandByState(responses, "/lights/" + id + "/state", hueDevice.state,
newState);
@@ -332,6 +332,25 @@ public class LightsAndGroupsTests {
assertThat(stateColorBulb.bri, is(200));
}
@Test
public void changeOnAndInvalidBriValues() throws Exception {
HueLightEntry hueLightEntry = cs.ds.lights.get("2");
assertNotNull(hueLightEntry);
HueStateColorBulb stateColorBulb = assertInstanceOf(HueStateColorBulb.class, hueLightEntry.state);
assertThat(stateColorBulb.on, is(false));
assertThat(stateColorBulb.bri, is(1));
String body = "{'on':true,'bri':0}";
ContentResponse response = commonSetup.sendPut("/testuser/lights/2/state", body);
assertThat(response.getStatus(), is(200));
assertThat(response.getContentAsString(), containsString("success"));
hueLightEntry = cs.ds.lights.get("2");
assertNotNull(hueLightEntry);
stateColorBulb = assertInstanceOf(HueStateColorBulb.class, hueLightEntry.state);
assertThat(stateColorBulb.on, is(true));
assertThat(stateColorBulb.bri, is(1));
}
@Test
public void changeHueSatValues() throws Exception {
HueLightEntry hueLightEntry = cs.ds.lights.get("2");
@@ -0,0 +1,68 @@
/*
* Copyright (c) 2010-2026 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.io.hueemulation.internal.rest;
import static org.hamcrest.CoreMatchers.*;
import static org.hamcrest.MatcherAssert.assertThat;
import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.openhab.core.library.types.PercentType;
import org.openhab.io.hueemulation.internal.StateUtils;
/**
* Tests for {@link StateUtils}.
*
* @author Jacob Laursen - Initial contribution
*/
@NonNullByDefault
public class StateUtilsTests {
@ParameterizedTest
@MethodSource("provideTestCasesForPercentTypeFromHueBrightness")
void percentTypeFromHueBrightness(int bri, PercentType expectedPercent) {
assertThat(StateUtils.percentTypeFromHueBrightness(bri), is(equalTo(expectedPercent)));
}
@ParameterizedTest
@MethodSource("provideTestCasesForHueBrightnessFromPercentType")
void hueBrightnessFromPercentType(PercentType percentValue, int expectedBri) {
assertThat(StateUtils.hueBrightnessFromPercentType(percentValue), is(equalTo(expectedBri)));
}
private static Stream<Arguments> provideTestCasesForPercentTypeFromHueBrightness() {
return Stream.of( //
Arguments.of(0, PercentType.ZERO), //
Arguments.of(1, PercentType.valueOf("0.39")), //
Arguments.of(2, PercentType.valueOf("0.79")), //
Arguments.of(3, PercentType.valueOf("1.18")), //
Arguments.of(127, new PercentType(50)), //
Arguments.of(253, PercentType.valueOf("99.61")), //
Arguments.of(254, PercentType.HUNDRED) //
);
}
private static Stream<Arguments> provideTestCasesForHueBrightnessFromPercentType() {
return Stream.of( //
Arguments.of(PercentType.ZERO, 1), //
Arguments.of(PercentType.valueOf("0.5"), 1), //
Arguments.of(new PercentType(1), 3), //
Arguments.of(new PercentType(50), 127), //
Arguments.of(PercentType.HUNDRED, 254) //
);
}
}