From f110aec55cba39d664b1cb7ef4f25f2424c0a9aa Mon Sep 17 00:00:00 2001 From: "Jan N. Klug" Date: Fri, 31 Dec 2021 11:26:05 +0100 Subject: [PATCH] [amazonechocontrol] fix color handling for smarthome devices (#272) Signed-off-by: Jan N. Klug # Conflicts: # bundles/org.smarthomej.binding.amazonechocontrol/README.md --- .../AmazonEchoControlHandlerFactory.java | 9 +- .../handler/SmartHomeDeviceHandler.java | 18 ++- .../smarthome/AbstractInterfaceHandler.java | 8 ++ .../internal/smarthome/AlexaColor.java | 111 ++++++++++++++++++ .../smarthome/HandlerColorController.java | 15 ++- .../internal/smarthome/InterfaceHandler.java | 5 + .../internal/smarthome/AlexaColorTest.java | 44 +++++++ 7 files changed, 202 insertions(+), 8 deletions(-) create mode 100644 bundles/org.smarthomej.binding.amazonechocontrol/src/main/java/org/smarthomej/binding/amazonechocontrol/internal/smarthome/AlexaColor.java create mode 100644 bundles/org.smarthomej.binding.amazonechocontrol/src/test/java/org/smarthomej/binding/amazonechocontrol/internal/smarthome/AlexaColorTest.java diff --git a/bundles/org.smarthomej.binding.amazonechocontrol/src/main/java/org/smarthomej/binding/amazonechocontrol/internal/AmazonEchoControlHandlerFactory.java b/bundles/org.smarthomej.binding.amazonechocontrol/src/main/java/org/smarthomej/binding/amazonechocontrol/internal/AmazonEchoControlHandlerFactory.java index 803d14be2d..6b359652fb 100644 --- a/bundles/org.smarthomej.binding.amazonechocontrol/src/main/java/org/smarthomej/binding/amazonechocontrol/internal/AmazonEchoControlHandlerFactory.java +++ b/bundles/org.smarthomej.binding.amazonechocontrol/src/main/java/org/smarthomej/binding/amazonechocontrol/internal/AmazonEchoControlHandlerFactory.java @@ -40,6 +40,7 @@ import org.smarthomej.binding.amazonechocontrol.internal.handler.EchoHandler; import org.smarthomej.binding.amazonechocontrol.internal.handler.FlashBriefingProfileHandler; import org.smarthomej.binding.amazonechocontrol.internal.handler.SmartHomeDeviceHandler; +import org.smarthomej.commons.SimpleDynamicCommandDescriptionProvider; import com.google.gson.Gson; @@ -60,13 +61,15 @@ public class AmazonEchoControlHandlerFactory extends BaseThingHandlerFactory { private final BindingServlet bindingServlet; private final Gson gson; private final HttpClient httpClient; + private final SimpleDynamicCommandDescriptionProvider dynamicCommandDescriptionProvider; @Activate - public AmazonEchoControlHandlerFactory(@Reference HttpService httpService, @Reference StorageService storageService) - throws Exception { + public AmazonEchoControlHandlerFactory(@Reference HttpService httpService, @Reference StorageService storageService, + @Reference SimpleDynamicCommandDescriptionProvider dynamicCommandDescriptionProvider) throws Exception { this.storageService = storageService; this.httpService = httpService; this.gson = new Gson(); + this.dynamicCommandDescriptionProvider = dynamicCommandDescriptionProvider; this.httpClient = new HttpClient(new SslContextFactory.Client()); this.bindingServlet = new BindingServlet(httpService); @@ -109,7 +112,7 @@ protected void deactivate(ComponentContext componentContext) { } else if (SUPPORTED_ECHO_THING_TYPES_UIDS.contains(thingTypeUID)) { return new EchoHandler(thing, gson); } else if (SUPPORTED_SMART_HOME_THING_TYPES_UIDS.contains(thingTypeUID)) { - return new SmartHomeDeviceHandler(thing, gson); + return new SmartHomeDeviceHandler(thing, gson, dynamicCommandDescriptionProvider); } return null; } diff --git a/bundles/org.smarthomej.binding.amazonechocontrol/src/main/java/org/smarthomej/binding/amazonechocontrol/internal/handler/SmartHomeDeviceHandler.java b/bundles/org.smarthomej.binding.amazonechocontrol/src/main/java/org/smarthomej/binding/amazonechocontrol/internal/handler/SmartHomeDeviceHandler.java index 9918033ec6..87b34d3028 100644 --- a/bundles/org.smarthomej.binding.amazonechocontrol/src/main/java/org/smarthomej/binding/amazonechocontrol/internal/handler/SmartHomeDeviceHandler.java +++ b/bundles/org.smarthomej.binding.amazonechocontrol/src/main/java/org/smarthomej/binding/amazonechocontrol/internal/handler/SmartHomeDeviceHandler.java @@ -40,6 +40,7 @@ import org.openhab.core.thing.binding.builder.ChannelBuilder; import org.openhab.core.thing.binding.builder.ThingBuilder; import org.openhab.core.types.Command; +import org.openhab.core.types.CommandOption; import org.openhab.core.types.RefreshType; import org.openhab.core.types.State; import org.slf4j.Logger; @@ -55,6 +56,7 @@ import org.smarthomej.binding.amazonechocontrol.internal.smarthome.ChannelInfo; import org.smarthomej.binding.amazonechocontrol.internal.smarthome.Constants; import org.smarthomej.binding.amazonechocontrol.internal.smarthome.InterfaceHandler; +import org.smarthomej.commons.SimpleDynamicCommandDescriptionProvider; import com.google.gson.Gson; import com.google.gson.JsonArray; @@ -67,16 +69,20 @@ @NonNullByDefault public class SmartHomeDeviceHandler extends BaseThingHandler { private final Logger logger = LoggerFactory.getLogger(SmartHomeDeviceHandler.class); + private final SimpleDynamicCommandDescriptionProvider dynamicCommandDescriptionProvider; - private @Nullable SmartHomeBaseDevice smartHomeBaseDevice; private final Gson gson; private final Map interfaceHandlers = new HashMap<>(); private final Map lastStates = new HashMap<>(); + + private @Nullable SmartHomeBaseDevice smartHomeBaseDevice; private String deviceId = ""; - public SmartHomeDeviceHandler(Thing thing, Gson gson) { + public SmartHomeDeviceHandler(Thing thing, Gson gson, + SimpleDynamicCommandDescriptionProvider dynamicCommandDescriptionProvider) { super(thing); this.gson = gson; + this.dynamicCommandDescriptionProvider = dynamicCommandDescriptionProvider; } public synchronized void setDeviceAndUpdateThingState(AccountHandler accountHandler, @@ -123,6 +129,12 @@ public synchronized void setDeviceAndUpdateThingState(AccountHandler accountHand if (addChannelToDevice(thingBuilder, callback, channelInfo)) { changed = true; } + + List commandOptions = handler.getCommandDescription(channelInfo); + if (commandOptions != null) { + dynamicCommandDescriptionProvider.setCommandOptions( + new ChannelUID(thing.getUID(), channelInfo.channelId), commandOptions); + } } } } @@ -168,6 +180,8 @@ public void dispose() { if (accountHandler != null) { accountHandler.removeSmartHomeDeviceHandler(this); } + + dynamicCommandDescriptionProvider.removeCommandDescriptionForThing(thing.getUID()); } public String getId() { diff --git a/bundles/org.smarthomej.binding.amazonechocontrol/src/main/java/org/smarthomej/binding/amazonechocontrol/internal/smarthome/AbstractInterfaceHandler.java b/bundles/org.smarthomej.binding.amazonechocontrol/src/main/java/org/smarthomej/binding/amazonechocontrol/internal/smarthome/AbstractInterfaceHandler.java index 5eaa8ed86c..b1154e9c2d 100644 --- a/bundles/org.smarthomej.binding.amazonechocontrol/src/main/java/org/smarthomej/binding/amazonechocontrol/internal/smarthome/AbstractInterfaceHandler.java +++ b/bundles/org.smarthomej.binding.amazonechocontrol/src/main/java/org/smarthomej/binding/amazonechocontrol/internal/smarthome/AbstractInterfaceHandler.java @@ -22,7 +22,9 @@ import java.util.Set; import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; import org.openhab.core.types.Command; +import org.openhab.core.types.CommandOption; import org.openhab.core.types.State; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -51,6 +53,12 @@ public AbstractInterfaceHandler(SmartHomeDeviceHandler smartHomeDeviceHandler, L this.interfaces = interfaces; } + @Override + public @Nullable List getCommandDescription(ChannelInfo channelInfo) { + // return null if not used + return null; + } + protected abstract Set findChannelInfos(JsonSmartHomeCapability capability, String property); public abstract void updateChannels(String interfaceName, List stateList, UpdateChannelResult result); diff --git a/bundles/org.smarthomej.binding.amazonechocontrol/src/main/java/org/smarthomej/binding/amazonechocontrol/internal/smarthome/AlexaColor.java b/bundles/org.smarthomej.binding.amazonechocontrol/src/main/java/org/smarthomej/binding/amazonechocontrol/internal/smarthome/AlexaColor.java new file mode 100644 index 0000000000..de3d057624 --- /dev/null +++ b/bundles/org.smarthomej.binding.amazonechocontrol/src/main/java/org/smarthomej/binding/amazonechocontrol/internal/smarthome/AlexaColor.java @@ -0,0 +1,111 @@ +/** + * Copyright (c) 2021 Contributors to the SmartHome/J 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.smarthomej.binding.amazonechocontrol.internal.smarthome; + +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.library.types.HSBType; + +/** + * The {@link AlexaColor} defines the Alexa color names + * + * @author Jan N. Klug - Initial contribution + */ +@NonNullByDefault +public class AlexaColor { + public static final List ALEXA_COLORS = List.of( // + new AlexaColor("white", new HSBType("0,0,100")), // + new AlexaColor("red", new HSBType("0,100,100")), // + new AlexaColor("crimson", new HSBType("348,90,100")), // + new AlexaColor("salmon", new HSBType("16,52,100")), // + new AlexaColor("orange", new HSBType("38,100,100")), // + new AlexaColor("gold", new HSBType("49,100,100")), // + new AlexaColor("yellow", new HSBType("60,100,100")), // + new AlexaColor("green", new HSBType("120,100,100")), // + new AlexaColor("turquoise", new HSBType("173,72,100")), // + new AlexaColor("cyan", new HSBType("180,100,100")), // + new AlexaColor("sky_blue", new HSBType("197,42,100")), // + new AlexaColor("blue", new HSBType("240,100,100")), // + new AlexaColor("purple", new HSBType("276,86,100")), // + new AlexaColor("magenta", new HSBType("300,100,100")), // + new AlexaColor("pink", new HSBType("348,25,100")), // + new AlexaColor("lavender", new HSBType("255,50,100"))); + + public final String colorName; + final HSBType value; + private final double[] lab; + + public AlexaColor(String colorName, HSBType value) { + this.colorName = colorName; + this.value = value; + this.lab = getLabFromHSB(value); + } + + /** + * get the closest Alexa color + * + * @param value a given HSB color + * @return the name of the closest pre-defined Alexa color + */ + public static String getClosestColorName(HSBType value) { + double[] lab = getLabFromHSB(value); + String colorName = ""; + double smallestDistance = Double.MAX_VALUE; + for (AlexaColor color : ALEXA_COLORS) { + double distance = color.getEuclideanDistance(lab); + if (distance < smallestDistance) { + colorName = color.colorName; + smallestDistance = distance; + } + } + return colorName; + } + + private double getEuclideanDistance(double[] value) { + double deltaL = value[0] - lab[0]; + double deltaA = value[1] - lab[1]; + double deltaB = value[2] - lab[2]; + + return Math.sqrt(deltaL * deltaL + deltaA * deltaA + deltaB * deltaB); + } + + private static double[] getLabFromHSB(HSBType value) { + double r = value.getRed().doubleValue() / 100.0; + double g = value.getGreen().doubleValue() / 100.0; + double b = value.getBlue().doubleValue() / 100.0; + + // D65, 10 degree + double xn = 94.811; + double yn = 100.0; + double zn = 107.304; + + double x = 0.4124564 * r + 0.3575761 * g + 0.1804375 * b; + double y = 0.2126729 * r + 0.7151522 * g + 0.0721750 * b; + double z = 0.0193339 * r + 0.1191920 * g + 0.9503041 * b; + + double ls = 116.0 * labRoot(y / yn) - 16.0; + double as = 500.0 * (labRoot(x / xn) - labRoot(y / yn)); + double bs = 200.0 * (labRoot(y / yn) - labRoot(z / zn)); + + return new double[] { ls, as, bs }; + } + + private static double labRoot(double value) { + if (value < 216.0 / 24389.0) { + return (1.0 / 116.0) * ((24389.0 / 27.0) * value + 16.0); + } else { + return Math.pow(value, 1.0 / 3.0); + } + } +} diff --git a/bundles/org.smarthomej.binding.amazonechocontrol/src/main/java/org/smarthomej/binding/amazonechocontrol/internal/smarthome/HandlerColorController.java b/bundles/org.smarthomej.binding.amazonechocontrol/src/main/java/org/smarthomej/binding/amazonechocontrol/internal/smarthome/HandlerColorController.java index d144acfd91..41954af958 100644 --- a/bundles/org.smarthomej.binding.amazonechocontrol/src/main/java/org/smarthomej/binding/amazonechocontrol/internal/smarthome/HandlerColorController.java +++ b/bundles/org.smarthomej.binding.amazonechocontrol/src/main/java/org/smarthomej/binding/amazonechocontrol/internal/smarthome/HandlerColorController.java @@ -14,9 +14,11 @@ package org.smarthomej.binding.amazonechocontrol.internal.smarthome; import java.io.IOException; +import java.math.BigDecimal; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.stream.Collectors; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; @@ -25,6 +27,7 @@ import org.openhab.core.library.types.PercentType; import org.openhab.core.library.types.StringType; import org.openhab.core.types.Command; +import org.openhab.core.types.CommandOption; import org.openhab.core.types.UnDefType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -75,9 +78,9 @@ public void updateChannels(String interfaceName, List stateList, Upd JsonObject value = state.get("value").getAsJsonObject(); // For groups take the maximum if (colorValue == null) { - colorValue = new HSBType(new DecimalType(value.get("hue").getAsInt()), - new PercentType(value.get("saturation").getAsInt() * 100), - new PercentType(value.get("brightness").getAsInt() * 100)); + colorValue = new HSBType(new DecimalType(value.get("hue").getAsDouble()), + new PercentType(BigDecimal.valueOf(value.get("saturation").getAsDouble() * 100.0)), + new PercentType(BigDecimal.valueOf(value.get("brightness").getAsDouble() * 100.0))); } } } @@ -129,4 +132,10 @@ public boolean handleCommand(Connection connection, JsonSmartHomeDevice shd, Str } return false; } + + @Override + public List getCommandDescription(ChannelInfo channelInfo) { + return AlexaColor.ALEXA_COLORS.stream().map(color -> new CommandOption(color.colorName, color.colorName)) + .collect(Collectors.toList()); + } } diff --git a/bundles/org.smarthomej.binding.amazonechocontrol/src/main/java/org/smarthomej/binding/amazonechocontrol/internal/smarthome/InterfaceHandler.java b/bundles/org.smarthomej.binding.amazonechocontrol/src/main/java/org/smarthomej/binding/amazonechocontrol/internal/smarthome/InterfaceHandler.java index fca58fb42d..425883b02b 100644 --- a/bundles/org.smarthomej.binding.amazonechocontrol/src/main/java/org/smarthomej/binding/amazonechocontrol/internal/smarthome/InterfaceHandler.java +++ b/bundles/org.smarthomej.binding.amazonechocontrol/src/main/java/org/smarthomej/binding/amazonechocontrol/internal/smarthome/InterfaceHandler.java @@ -17,7 +17,9 @@ import java.util.List; import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; import org.openhab.core.types.Command; +import org.openhab.core.types.CommandOption; import org.smarthomej.binding.amazonechocontrol.internal.connection.Connection; import org.smarthomej.binding.amazonechocontrol.internal.jsons.JsonSmartHomeCapability; import org.smarthomej.binding.amazonechocontrol.internal.jsons.JsonSmartHomeDevice; @@ -43,6 +45,9 @@ boolean handleCommand(Connection connection, JsonSmartHomeDevice shd, String ent List capabilities, String channelId, Command command) throws IOException, InterruptedException; + @Nullable + List getCommandDescription(ChannelInfo channelInfo); + class UpdateChannelResult { public boolean needSingleUpdate; } diff --git a/bundles/org.smarthomej.binding.amazonechocontrol/src/test/java/org/smarthomej/binding/amazonechocontrol/internal/smarthome/AlexaColorTest.java b/bundles/org.smarthomej.binding.amazonechocontrol/src/test/java/org/smarthomej/binding/amazonechocontrol/internal/smarthome/AlexaColorTest.java new file mode 100644 index 0000000000..589bd840fb --- /dev/null +++ b/bundles/org.smarthomej.binding.amazonechocontrol/src/test/java/org/smarthomej/binding/amazonechocontrol/internal/smarthome/AlexaColorTest.java @@ -0,0 +1,44 @@ +/** + * Copyright (c) 2021 Contributors to the SmartHome/J 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.smarthomej.binding.amazonechocontrol.internal.smarthome; + +import java.util.stream.Stream; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.junit.jupiter.MockitoExtension; +import org.openhab.core.test.java.JavaTest; + +/** + * The {@link AlexaColorTest} is a + * + * @author Jan N. Klug - Initial contribution + */ +@NonNullByDefault +@ExtendWith(MockitoExtension.class) +public class AlexaColorTest extends JavaTest { + + @ParameterizedTest + @MethodSource("getColors") + public void distanceTest(AlexaColor color) { + Assertions.assertEquals(color.colorName, AlexaColor.getClosestColorName(color.value)); + } + + @SuppressWarnings("unused") + private static Stream getColors() { + return AlexaColor.ALEXA_COLORS.stream(); + } +}