diff --git a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/LifxLightContext.java b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/LifxLightContext.java index 633960006b2..5a65ff3fa41 100644 --- a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/LifxLightContext.java +++ b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/LifxLightContext.java @@ -15,6 +15,7 @@ package org.openhab.binding.lifx.internal; import java.util.concurrent.ScheduledExecutorService; import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.lifx.internal.LifxProduct.Features; import org.openhab.binding.lifx.internal.handler.LifxLightHandler; import org.openhab.binding.lifx.internal.handler.LifxLightHandler.CurrentLightState; @@ -30,14 +31,14 @@ public class LifxLightContext { private final LifxLightConfig configuration; private final CurrentLightState currentLightState; private final LifxLightState pendingLightState; - private final LifxProduct product; + private final Features features; private final ScheduledExecutorService scheduler; - public LifxLightContext(String logId, LifxProduct product, LifxLightConfig configuration, + public LifxLightContext(String logId, Features features, LifxLightConfig configuration, CurrentLightState currentLightState, LifxLightState pendingLightState, ScheduledExecutorService scheduler) { this.logId = logId; this.configuration = configuration; - this.product = product; + this.features = features; this.currentLightState = currentLightState; this.pendingLightState = pendingLightState; this.scheduler = scheduler; @@ -51,8 +52,8 @@ public class LifxLightContext { return configuration; } - public LifxProduct getProduct() { - return product; + public Features getFeatures() { + return features; } public CurrentLightState getCurrentLightState() { diff --git a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/LifxLightCurrentStateUpdater.java b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/LifxLightCurrentStateUpdater.java index 910c8b77878..ef6ad12b691 100644 --- a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/LifxLightCurrentStateUpdater.java +++ b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/LifxLightCurrentStateUpdater.java @@ -23,6 +23,7 @@ import java.util.concurrent.locks.ReentrantLock; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.lifx.internal.LifxProduct.Features; import org.openhab.binding.lifx.internal.dto.GetColorZonesRequest; import org.openhab.binding.lifx.internal.dto.GetLightInfraredRequest; import org.openhab.binding.lifx.internal.dto.GetRequest; @@ -56,7 +57,7 @@ public class LifxLightCurrentStateUpdater { private final Logger logger = LoggerFactory.getLogger(LifxLightCurrentStateUpdater.class); private final String logId; - private final LifxProduct product; + private final Features features; private final CurrentLightState currentLightState; private final ScheduledExecutorService scheduler; private final LifxLightCommunicationHandler communicationHandler; @@ -70,7 +71,7 @@ public class LifxLightCurrentStateUpdater { public LifxLightCurrentStateUpdater(LifxLightContext context, LifxLightCommunicationHandler communicationHandler) { this.logId = context.getLogId(); - this.product = context.getProduct(); + this.features = context.getFeatures(); this.currentLightState = context.getCurrentLightState(); this.scheduler = context.getScheduler(); this.communicationHandler = communicationHandler; @@ -132,13 +133,13 @@ public class LifxLightCurrentStateUpdater { private void sendLightStateRequests() { communicationHandler.sendPacket(new GetRequest()); - if (product.hasFeature(INFRARED)) { + if (features.hasFeature(INFRARED)) { communicationHandler.sendPacket(new GetLightInfraredRequest()); } - if (product.hasFeature(MULTIZONE)) { + if (features.hasFeature(MULTIZONE)) { communicationHandler.sendPacket(new GetColorZonesRequest()); } - if (product.hasFeature(TILE_EFFECT)) { + if (features.hasFeature(TILE_EFFECT)) { communicationHandler.sendPacket(new GetTileEffectRequest()); } if (updateSignalStrength) { diff --git a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/LifxLightDiscovery.java b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/LifxLightDiscovery.java index cfadb13a671..5799af1994a 100644 --- a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/LifxLightDiscovery.java +++ b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/LifxLightDiscovery.java @@ -298,9 +298,11 @@ public class LifxLightDiscovery extends AbstractDiscoveryService { light.label = ((StateLabelResponse) packet).getLabel().trim(); } else if (packet instanceof StateVersionResponse) { try { - light.product = LifxProduct + LifxProduct product = LifxProduct .getProductFromProductID(((StateVersionResponse) packet).getProduct()); + light.product = product; light.productVersion = ((StateVersionResponse) packet).getVersion(); + light.supportedProduct = product.isLight(); } catch (IllegalArgumentException e) { logger.debug("Discovered an unsupported light ({}): {}", light.macAddress.getAsLabel(), e.getMessage()); @@ -309,7 +311,7 @@ public class LifxLightDiscovery extends AbstractDiscoveryService { } } - if (light != null && light.isDataComplete()) { + if (light != null && light.supportedProduct && light.isDataComplete()) { try { thingDiscovered(createDiscoveryResult(light)); } catch (IllegalArgumentException e) { diff --git a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/LifxLightStateChanger.java b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/LifxLightStateChanger.java index c2ee8d1c9ba..dbccb2113b5 100644 --- a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/LifxLightStateChanger.java +++ b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/LifxLightStateChanger.java @@ -29,6 +29,7 @@ import java.util.concurrent.locks.ReentrantLock; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.lifx.internal.LifxProduct.Features; import org.openhab.binding.lifx.internal.dto.AcknowledgementResponse; import org.openhab.binding.lifx.internal.dto.ApplicationRequest; import org.openhab.binding.lifx.internal.dto.Effect; @@ -74,7 +75,7 @@ public class LifxLightStateChanger implements LifxLightStateListener { private final Logger logger = LoggerFactory.getLogger(LifxLightStateChanger.class); private final String logId; - private final LifxProduct product; + private final Features features; private final Duration fadeTime; private final LifxLightState pendingLightState; private final ScheduledExecutorService scheduler; @@ -104,7 +105,7 @@ public class LifxLightStateChanger implements LifxLightStateListener { public LifxLightStateChanger(LifxLightContext context, LifxLightCommunicationHandler communicationHandler) { this.logId = context.getLogId(); - this.product = context.getProduct(); + this.features = context.getFeatures(); this.fadeTime = context.getConfiguration().getFadeTime(); this.pendingLightState = context.getPendingLightState(); this.scheduler = context.getScheduler(); @@ -371,7 +372,7 @@ public class LifxLightStateChanger implements LifxLightStateListener { } private void getZonesIfZonesAreSet() { - if (product.hasFeature(MULTIZONE)) { + if (features.hasFeature(MULTIZONE)) { List pending = pendingPacketsMap.get(SetColorZonesRequest.TYPE); if (pending == null || pending.isEmpty()) { GetColorZonesRequest zoneColorPacket = new GetColorZonesRequest(); diff --git a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/LifxProduct.java b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/LifxProduct.java index 0416d378e23..3f561393514 100644 --- a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/LifxProduct.java +++ b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/LifxProduct.java @@ -16,8 +16,9 @@ import static org.openhab.binding.lifx.internal.LifxProduct.Feature.*; import static org.openhab.binding.lifx.internal.LifxProduct.TemperatureRange.*; import static org.openhab.binding.lifx.internal.LifxProduct.Vendor.LIFX; -import java.util.Arrays; -import java.util.EnumSet; +import java.util.HashSet; +import java.util.List; +import java.util.Set; import org.eclipse.jdt.annotation.NonNullByDefault; import org.openhab.core.thing.ThingTypeUID; @@ -33,75 +34,99 @@ import org.openhab.core.thing.ThingTypeUID; @NonNullByDefault public enum LifxProduct { - PRODUCT_1(1, "LIFX Original 1000", TR_2500_9000, COLOR), - PRODUCT_3(3, "LIFX Color 650", TR_2500_9000, COLOR), - PRODUCT_10(10, "LIFX White 800 (Low Voltage)", TR_2700_6500), - PRODUCT_11(11, "LIFX White 800 (High Voltage)", TR_2700_6500), - PRODUCT_15(15, "LIFX Color 1000", TR_2500_9000, COLOR), - PRODUCT_18(18, "LIFX White 900 BR30 (Low Voltage)", TR_2700_6500), - PRODUCT_19(19, "LIFX White 900 BR30 (High Voltage)", TR_2700_6500), - PRODUCT_20(20, "LIFX Color 1000 BR30", TR_2500_9000, COLOR), - PRODUCT_22(22, "LIFX Color 1000", TR_2500_9000, COLOR), - PRODUCT_27(27, "LIFX A19", TR_2500_9000, COLOR), - PRODUCT_28(28, "LIFX BR30", TR_2500_9000, COLOR), - PRODUCT_29(29, "LIFX A19 Night Vision", TR_2500_9000, COLOR, INFRARED), - PRODUCT_30(30, "LIFX BR30 Night Vision", TR_2500_9000, COLOR, INFRARED), - PRODUCT_31(31, "LIFX Z", TR_2500_9000, COLOR, MULTIZONE), - PRODUCT_32(32, "LIFX Z", TR_2500_9000, COLOR, MULTIZONE), - PRODUCT_36(36, "LIFX Downlight", TR_2500_9000, COLOR), - PRODUCT_37(37, "LIFX Downlight", TR_2500_9000, COLOR), - PRODUCT_38(38, "LIFX Beam", TR_2500_9000, COLOR, MULTIZONE), - PRODUCT_39(39, "LIFX Downlight White to Warm", TR_1500_9000), - PRODUCT_40(40, "LIFX Downlight", TR_2500_9000, COLOR), - PRODUCT_43(43, "LIFX A19", TR_2500_9000, COLOR), - PRODUCT_44(44, "LIFX BR30", TR_2500_9000, COLOR), - PRODUCT_45(45, "LIFX A19 Night Vision", TR_2500_9000, COLOR, INFRARED), - PRODUCT_46(46, "LIFX BR30 Night Vision", TR_2500_9000, COLOR, INFRARED), - PRODUCT_49(49, "LIFX Mini Color", TR_2500_9000, COLOR), - PRODUCT_50(50, "LIFX Mini White to Warm", TR_1500_4000), - PRODUCT_51(51, "LIFX Mini White", TR_2700_2700), - PRODUCT_52(52, "LIFX GU10", TR_2500_9000, COLOR), - PRODUCT_53(53, "LIFX GU10", TR_2500_9000, COLOR), - PRODUCT_55(55, "LIFX Tile", TR_2500_9000, CHAIN, COLOR, MATRIX, TILE_EFFECT), - PRODUCT_57(57, "LIFX Candle", TR_1500_9000, COLOR, MATRIX), - PRODUCT_59(59, "LIFX Mini Color", TR_2500_9000, COLOR), - PRODUCT_60(60, "LIFX Mini White to Warm", TR_1500_4000), - PRODUCT_61(61, "LIFX Mini White", TR_2700_2700), - PRODUCT_62(62, "LIFX A19", TR_2500_9000, COLOR), - PRODUCT_63(63, "LIFX BR30", TR_2500_9000, COLOR), - PRODUCT_64(64, "LIFX A19 Night Vision", TR_2500_9000, COLOR, INFRARED), - PRODUCT_65(65, "LIFX BR30 Night Vision", TR_2500_9000, COLOR, INFRARED), - PRODUCT_66(66, "LIFX Mini White", TR_2700_2700), - PRODUCT_68(68, "LIFX Candle", TR_1500_9000, MATRIX), - PRODUCT_81(81, "LIFX Candle White to Warm", TR_2200_6500), - PRODUCT_82(82, "LIFX Filament Clear", TR_2100_2100), - PRODUCT_85(85, "LIFX Filament Amber", TR_2000_2000), - PRODUCT_87(87, "LIFX Mini White", TR_2700_2700), - PRODUCT_88(88, "LIFX Mini White", TR_2700_2700), - PRODUCT_90(90, "LIFX Clean", TR_2500_9000, HEV), - PRODUCT_91(91, "LIFX Color", TR_2500_9000, COLOR), - PRODUCT_92(92, "LIFX Color", TR_2500_9000, COLOR), - PRODUCT_94(94, "LIFX BR30", TR_2500_9000, COLOR), - PRODUCT_96(96, "LIFX Candle White to Warm", TR_2200_6500), - PRODUCT_97(97, "LIFX A19", TR_2500_9000, COLOR), - PRODUCT_98(98, "LIFX BR30", TR_2500_9000, COLOR), - PRODUCT_99(99, "LIFX Clean", TR_2500_9000, HEV), - PRODUCT_100(100, "LIFX Filament Clear", TR_2100_2100), - PRODUCT_101(101, "LIFX Filament Amber", TR_2000_2000), - PRODUCT_109(109, "LIFX A19 Night Vision", TR_2500_9000, COLOR, INFRARED), - PRODUCT_110(110, "LIFX BR30 Night Vision", TR_2500_9000, COLOR, INFRARED), - PRODUCT_111(111, "LIFX A19 Night Vision", TR_2500_9000, COLOR, INFRARED); + PRODUCT_1(1, "LIFX Original 1000", new Features(TR_2500_9000, COLOR)), + PRODUCT_3(3, "LIFX Color 650", new Features(TR_2500_9000, COLOR)), + PRODUCT_10(10, "LIFX White 800 (Low Voltage)", new Features(TR_2700_6500)), + PRODUCT_11(11, "LIFX White 800 (High Voltage)", new Features(TR_2700_6500)), + PRODUCT_15(15, "LIFX Color 1000", new Features(TR_2500_9000, COLOR)), + PRODUCT_18(18, "LIFX White 900 BR30 (Low Voltage)", new Features(TR_2700_6500)), + PRODUCT_19(19, "LIFX White 900 BR30 (High Voltage)", new Features(TR_2700_6500)), + PRODUCT_20(20, "LIFX Color 1000 BR30", new Features(TR_2500_9000, COLOR)), + PRODUCT_22(22, "LIFX Color 1000", new Features(TR_2500_9000, COLOR)), + PRODUCT_27(27, "LIFX A19", new Features(TR_2500_9000, COLOR), // + new Upgrade(2, 80, new Features(TR_1500_9000))), + PRODUCT_28(28, "LIFX BR30", new Features(TR_2500_9000, COLOR), // + new Upgrade(2, 80, new Features(TR_1500_9000))), + PRODUCT_29(29, "LIFX A19 Night Vision", new Features(TR_2500_9000, COLOR, INFRARED), // + new Upgrade(2, 80, new Features(TR_1500_9000))), + PRODUCT_30(30, "LIFX BR30 Night Vision", new Features(TR_2500_9000, COLOR, INFRARED), // + new Upgrade(2, 80, new Features(TR_1500_9000))), + PRODUCT_31(31, "LIFX Z", new Features(TR_2500_9000, COLOR, MULTIZONE)), + PRODUCT_32(32, "LIFX Z", new Features(TR_2500_9000, COLOR, MULTIZONE), // + new Upgrade(2, 77, new Features(EXTENDED_MULTIZONE)), // + new Upgrade(2, 80, new Features(TR_1500_9000))), + PRODUCT_36(36, "LIFX Downlight", new Features(TR_2500_9000, COLOR), // + new Upgrade(2, 80, new Features(TR_1500_9000))), + PRODUCT_37(37, "LIFX Downlight", new Features(TR_2500_9000, COLOR), // + new Upgrade(2, 80, new Features(TR_1500_9000))), + PRODUCT_38(38, "LIFX Beam", new Features(TR_2500_9000, COLOR, MULTIZONE), // + new Upgrade(2, 77, new Features(EXTENDED_MULTIZONE)), // + new Upgrade(2, 80, new Features(TR_1500_9000))), + PRODUCT_39(39, "LIFX Downlight White to Warm", new Features(TR_2500_9000), // + new Upgrade(2, 80, new Features(TR_1500_9000))), + PRODUCT_40(40, "LIFX Downlight", new Features(TR_2500_9000, COLOR), // + new Upgrade(2, 80, new Features(TR_1500_9000))), + PRODUCT_43(43, "LIFX A19", new Features(TR_2500_9000, COLOR), // + new Upgrade(2, 80, new Features(TR_1500_9000))), + PRODUCT_44(44, "LIFX BR30", new Features(TR_2500_9000, COLOR), // + new Upgrade(2, 80, new Features(TR_1500_9000))), + PRODUCT_45(45, "LIFX A19 Night Vision", new Features(TR_2500_9000, COLOR, INFRARED), // + new Upgrade(2, 80, new Features(TR_1500_9000))), + PRODUCT_46(46, "LIFX BR30 Night Vision", new Features(TR_2500_9000, COLOR, INFRARED), // + new Upgrade(2, 80, new Features(TR_1500_9000))), + PRODUCT_49(49, "LIFX Mini Color", new Features(TR_1500_9000, COLOR)), + PRODUCT_50(50, "LIFX Mini White to Warm", new Features(TR_1500_6500), // + new Upgrade(3, 70, new Features(TR_1500_9000))), + PRODUCT_51(51, "LIFX Mini White", new Features(TR_2700_2700)), + PRODUCT_52(52, "LIFX GU10", new Features(TR_1500_9000, COLOR)), + PRODUCT_53(53, "LIFX GU10", new Features(TR_1500_9000, COLOR)), + PRODUCT_55(55, "LIFX Tile", new Features(TR_2500_9000, CHAIN, COLOR, MATRIX, TILE_EFFECT)), + PRODUCT_57(57, "LIFX Candle", new Features(TR_1500_9000, COLOR, MATRIX)), + PRODUCT_59(59, "LIFX Mini Color", new Features(TR_1500_9000, COLOR)), + PRODUCT_60(60, "LIFX Mini White to Warm", new Features(TR_1500_6500), // + new Upgrade(3, 70, new Features(TR_1500_9000))), + PRODUCT_61(61, "LIFX Mini White", new Features(TR_2700_2700)), + PRODUCT_62(62, "LIFX A19", new Features(TR_1500_9000, COLOR)), + PRODUCT_63(63, "LIFX BR30", new Features(TR_1500_9000, COLOR)), + PRODUCT_64(64, "LIFX A19 Night Vision", new Features(TR_1500_9000, COLOR, INFRARED)), + PRODUCT_65(65, "LIFX BR30 Night Vision", new Features(TR_1500_9000, COLOR, INFRARED)), + PRODUCT_66(66, "LIFX Mini White", new Features(TR_2700_2700)), + PRODUCT_68(68, "LIFX Candle", new Features(TR_1500_9000, MATRIX)), + PRODUCT_70(70, "LIFX Switch", new Features(BUTTONS, RELAYS)), + PRODUCT_71(71, "LIFX Switch", new Features(BUTTONS, RELAYS)), + PRODUCT_81(81, "LIFX Candle White to Warm", new Features(TR_2200_6500)), + PRODUCT_82(82, "LIFX Filament Clear", new Features(TR_2100_2100)), + PRODUCT_85(85, "LIFX Filament Amber", new Features(TR_2000_2000)), + PRODUCT_87(87, "LIFX Mini White", new Features(TR_2700_2700)), + PRODUCT_88(88, "LIFX Mini White", new Features(TR_2700_2700)), + PRODUCT_89(89, "LIFX Switch", new Features(BUTTONS, RELAYS)), + PRODUCT_90(90, "LIFX Clean", new Features(TR_1500_9000, HEV)), + PRODUCT_91(91, "LIFX Color", new Features(TR_1500_9000, COLOR)), + PRODUCT_92(92, "LIFX Color", new Features(TR_1500_9000, COLOR)), + PRODUCT_94(94, "LIFX BR30", new Features(TR_1500_9000, COLOR)), + PRODUCT_96(96, "LIFX Candle White to Warm", new Features(TR_2200_6500)), + PRODUCT_97(97, "LIFX A19", new Features(TR_1500_9000, COLOR)), + PRODUCT_98(98, "LIFX BR30", new Features(TR_1500_9000, COLOR)), + PRODUCT_99(99, "LIFX Clean", new Features(TR_1500_9000, HEV)), + PRODUCT_100(100, "LIFX Filament Clear", new Features(TR_2100_2100)), + PRODUCT_101(101, "LIFX Filament Amber", new Features(TR_2000_2000)), + PRODUCT_109(109, "LIFX A19 Night Vision", new Features(TR_1500_9000, COLOR, INFRARED)), + PRODUCT_110(110, "LIFX BR30 Night Vision", new Features(TR_1500_9000, COLOR, INFRARED)), + PRODUCT_111(111, "LIFX A19 Night Vision", new Features(TR_1500_9000, COLOR, INFRARED)); /** * Enumerates the product features. */ public enum Feature { + BUTTONS, CHAIN, COLOR, + EXTENDED_MULTIZONE, HEV, INFRARED, MATRIX, MULTIZONE, + RELAYS, TILE_EFFECT } @@ -132,11 +157,22 @@ public enum LifxProduct { * Enumerates the color temperature ranges of lights. */ public enum TemperatureRange { + /** + * When the temperature range is not defined for {@link Upgrade}s it is inherited from the most + * recent firmware version. + */ + NONE(0, 0), + /** * 1500-4000K */ TR_1500_4000(1500, 4000), + /** + * 1500-6500K + */ + TR_1500_6500(1500, 6500), + /** * 1500-9000K */ @@ -208,30 +244,70 @@ public enum LifxProduct { } } + public static class Features { + private TemperatureRange temperatureRange; + private Set features; + + private Features(Feature... features) { + this(NONE, features); + } + + private Features(Features other) { + this(other.temperatureRange, other.features); + } + + private Features(TemperatureRange temperatureRange, Feature... features) { + this.temperatureRange = temperatureRange; + this.features = Set.of(features); + } + + private Features(TemperatureRange temperatureRange, Set features) { + this.temperatureRange = temperatureRange; + this.features = Set.copyOf(features); + } + + public TemperatureRange getTemperatureRange() { + return temperatureRange; + } + + public boolean hasFeature(Feature feature) { + return features.contains(feature); + } + + public void update(Features other) { + temperatureRange = other.temperatureRange; + features = other.features; + } + } + + static class Upgrade { + final long major; + final long minor; + final Features features; + + private Upgrade(long major, long minor, Features features) { + this.major = major; + this.minor = minor; + this.features = features; + } + } + private final Vendor vendor; private final long id; private final String name; - private final TemperatureRange temperatureRange; - private final EnumSet features = EnumSet.noneOf(Feature.class); + private final Features features; + private final List upgrades; - private LifxProduct(long id, String name, TemperatureRange temperatureRange) { - this(LIFX, id, name, temperatureRange); + private LifxProduct(long id, String name, Features features, Upgrade... upgrades) { + this(LIFX, id, name, features, upgrades); } - private LifxProduct(long id, String name, TemperatureRange temperatureRange, Feature... features) { - this(LIFX, id, name, temperatureRange, features); - } - - private LifxProduct(Vendor vendor, long id, String name, TemperatureRange temperatureRange) { - this(vendor, id, name, temperatureRange, new Feature[0]); - } - - private LifxProduct(Vendor vendor, long id, String name, TemperatureRange temperatureRange, Feature... features) { + private LifxProduct(Vendor vendor, long id, String name, Features features, Upgrade... upgrades) { this.vendor = vendor; this.id = id; this.name = name; - this.temperatureRange = temperatureRange; - this.features.addAll(Arrays.asList(features)); + this.features = features; + this.upgrades = List.of(upgrades); } @Override @@ -251,8 +327,46 @@ public enum LifxProduct { return name; } - public TemperatureRange getTemperatureRange() { - return temperatureRange; + /** + * Returns the features of the initial product firmware version. + * + * @return the initial features + */ + public Features getFeatures() { + return new Features(features); + } + + /** + * Returns the features for a specific product firmware version. + * + * @param version the firmware version + * @return the composite features of all firmware upgrades for the given major and minor firmware version + */ + public Features getFeatures(String version) { + if (upgrades.isEmpty() || !version.contains(".")) { + return new Features(features); + } + + String[] majorMinorVersion = version.split("\\."); + long major = Long.valueOf(majorMinorVersion[0]); + long minor = Long.valueOf(majorMinorVersion[1]); + + TemperatureRange temperatureRange = features.temperatureRange; + Set features = new HashSet<>(this.features.features); + + for (Upgrade upgrade : upgrades) { + if (upgrade.major < major || (upgrade.major == major && upgrade.minor <= minor)) { + Features upgradeFeatures = upgrade.features; + if (upgradeFeatures.temperatureRange != NONE) { + temperatureRange = upgradeFeatures.temperatureRange; + } + features.addAll(upgradeFeatures.features); + } else { + break; + } + } + + return new Features(temperatureRange, features); } public ThingTypeUID getThingTypeUID() { @@ -271,8 +385,8 @@ public enum LifxProduct { } } - public boolean hasFeature(Feature feature) { - return features.contains(feature); + private boolean hasFeature(Feature feature) { + return features.hasFeature(feature); } /** @@ -308,4 +422,12 @@ public enum LifxProduct { throw new IllegalArgumentException(id + " is not a valid product ID"); } + + List getUpgrades() { + return upgrades; + } + + public boolean isLight() { + return !features.hasFeature(Feature.BUTTONS) && !features.hasFeature(Feature.RELAYS); + } } diff --git a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/handler/LifxLightHandler.java b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/handler/LifxLightHandler.java index d1927d1c910..fd068429800 100644 --- a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/handler/LifxLightHandler.java +++ b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/handler/LifxLightHandler.java @@ -24,6 +24,7 @@ import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.concurrent.locks.ReentrantLock; import org.eclipse.jdt.annotation.NonNullByDefault; @@ -39,6 +40,7 @@ import org.openhab.binding.lifx.internal.LifxLightPropertiesUpdater; import org.openhab.binding.lifx.internal.LifxLightState; import org.openhab.binding.lifx.internal.LifxLightStateChanger; import org.openhab.binding.lifx.internal.LifxProduct; +import org.openhab.binding.lifx.internal.LifxProduct.Features; import org.openhab.binding.lifx.internal.dto.Effect; import org.openhab.binding.lifx.internal.dto.GetLightInfraredRequest; import org.openhab.binding.lifx.internal.dto.GetLightPowerRequest; @@ -90,7 +92,7 @@ public class LifxLightHandler extends BaseThingHandler { private static final Duration MAX_STATE_CHANGE_DURATION = Duration.ofSeconds(4); private final LifxChannelFactory channelFactory; - private @NonNullByDefault({}) LifxProduct product; + private @NonNullByDefault({}) Features features; private @Nullable PercentType powerOnBrightness; private @Nullable HSBType powerOnColor; @@ -175,7 +177,7 @@ public class LifxLightHandler extends BaseThingHandler { updateStateIfChanged(CHANNEL_COLOR, hsb); updateStateIfChanged(CHANNEL_BRIGHTNESS, hsb.getBrightness()); updateStateIfChanged(CHANNEL_TEMPERATURE, - kelvinToPercentType(updateColor.getKelvin(), product.getTemperatureRange())); + kelvinToPercentType(updateColor.getKelvin(), features.getTemperatureRange())); updateZoneChannels(powerState, colors); } @@ -210,7 +212,7 @@ public class LifxLightHandler extends BaseThingHandler { } private void updateZoneChannels(@Nullable PowerState powerState, HSBK[] colors) { - if (!product.hasFeature(MULTIZONE) || colors.length == 0) { + if (!features.hasFeature(MULTIZONE) || colors.length == 0) { return; } @@ -225,7 +227,7 @@ public class LifxLightHandler extends BaseThingHandler { HSBK updateColor = nullSafeUpdateColor(powerState, color); updateStateIfChanged(CHANNEL_COLOR_ZONE + i, updateColor.getHSB()); updateStateIfChanged(CHANNEL_TEMPERATURE_ZONE + i, - kelvinToPercentType(updateColor.getKelvin(), product.getTemperatureRange())); + kelvinToPercentType(updateColor.getKelvin(), features.getTemperatureRange())); } } } @@ -243,9 +245,12 @@ public class LifxLightHandler extends BaseThingHandler { LifxLightConfig configuration = getConfigAs(LifxLightConfig.class); logId = getLogId(configuration.getMACAddress(), configuration.getHost()); - product = getProduct(); - logger.debug("{} : Initializing handler for product {}", logId, product.getName()); + if (logger.isDebugEnabled()) { + logger.debug("{} : Initializing handler for product {}", logId, getProduct().getName()); + } + + features = getFeatures(); powerOnBrightness = getPowerOnBrightness(); powerOnColor = getPowerOnColor(); @@ -262,7 +267,7 @@ public class LifxLightHandler extends BaseThingHandler { currentLightState = new CurrentLightState(); pendingLightState = new LifxLightState(); - LifxLightContext context = new LifxLightContext(logId, product, configuration, currentLightState, + LifxLightContext context = new LifxLightContext(logId, features, configuration, currentLightState, pendingLightState, scheduler); communicationHandler = new LifxLightCommunicationHandler(context); @@ -338,7 +343,7 @@ public class LifxLightHandler extends BaseThingHandler { private @Nullable PercentType getPowerOnBrightness() { Channel channel = null; - if (product.hasFeature(COLOR)) { + if (features.hasFeature(COLOR)) { ChannelUID channelUID = new ChannelUID(getThing().getUID(), LifxBindingConstants.CHANNEL_COLOR); channel = getThing().getChannel(channelUID.getId()); } else { @@ -358,7 +363,7 @@ public class LifxLightHandler extends BaseThingHandler { private @Nullable HSBType getPowerOnColor() { Channel channel = null; - if (product.hasFeature(COLOR)) { + if (features.hasFeature(COLOR)) { ChannelUID channelUID = new ChannelUID(getThing().getUID(), LifxBindingConstants.CHANNEL_COLOR); channel = getThing().getChannel(channelUID.getId()); } @@ -391,7 +396,7 @@ public class LifxLightHandler extends BaseThingHandler { private @Nullable Double getEffectSpeed(String parameter) { Channel channel = null; - if (product.hasFeature(TILE_EFFECT)) { + if (features.hasFeature(TILE_EFFECT)) { ChannelUID channelUID = new ChannelUID(getThing().getUID(), LifxBindingConstants.CHANNEL_EFFECT); channel = getThing().getChannel(channelUID.getId()); } @@ -405,8 +410,21 @@ public class LifxLightHandler extends BaseThingHandler { return speed == null ? null : Double.valueOf(speed.toString()); } + private Features getFeatures() { + LifxProduct product = getProduct(); + + String propertyValue = getThing().getProperties().get(LifxBindingConstants.PROPERTY_HOST_VERSION); + if (propertyValue == null) { + logger.debug("{} : Using features of initial firmware version", logId); + return product.getFeatures(); + } + + logger.debug("{} : Using features of firmware version {}", logId, propertyValue); + return product.getFeatures(propertyValue); + } + private LifxProduct getProduct() { - Object propertyValue = getThing().getProperties().get(LifxBindingConstants.PROPERTY_PRODUCT_ID); + String propertyValue = getThing().getProperties().get(LifxBindingConstants.PROPERTY_PRODUCT_ID); if (propertyValue == null) { return LifxProduct.getLikelyProduct(getThing().getThingTypeUID()); } @@ -414,7 +432,7 @@ public class LifxLightHandler extends BaseThingHandler { // Without first conversion to double, on a very first thing creation from discovery inbox, // the product type is incorrectly parsed, as framework passed it as a floating point number // (e.g. 50.0 instead of 50) - Double d = Double.valueOf((String) propertyValue); + Double d = Double.valueOf(propertyValue); long productID = d.longValue(); return LifxProduct.getProductFromProductID(productID); } catch (IllegalArgumentException e) { @@ -485,7 +503,7 @@ public class LifxLightHandler extends BaseThingHandler { sendPacket(new GetWifiInfoRequest()); break; case CHANNEL_EFFECT: - if (product.hasFeature(TILE_EFFECT)) { + if (features.hasFeature(TILE_EFFECT)) { sendPacket(new GetTileEffectRequest()); } break; @@ -538,7 +556,7 @@ public class LifxLightHandler extends BaseThingHandler { } break; case CHANNEL_EFFECT: - if (command instanceof StringType && product.hasFeature(TILE_EFFECT)) { + if (command instanceof StringType && features.hasFeature(TILE_EFFECT)) { handleTileEffectCommand((StringType) command); } else { supportedCommand = false; @@ -597,14 +615,14 @@ public class LifxLightHandler extends BaseThingHandler { private void handleTemperatureCommand(PercentType temperature) { HSBK newColor = getLightStateForCommand().getColor(); newColor.setSaturation(PercentType.ZERO); - newColor.setKelvin(percentTypeToKelvin(temperature, product.getTemperatureRange())); + newColor.setKelvin(percentTypeToKelvin(temperature, features.getTemperatureRange())); getLightStateForCommand().setColor(newColor); } private void handleTemperatureCommand(PercentType temperature, int zoneIndex) { HSBK newColor = getLightStateForCommand().getColor(zoneIndex); newColor.setSaturation(PercentType.ZERO); - newColor.setKelvin(percentTypeToKelvin(temperature, product.getTemperatureRange())); + newColor.setKelvin(percentTypeToKelvin(temperature, features.getTemperatureRange())); getLightStateForCommand().setColor(newColor, zoneIndex); } @@ -633,7 +651,7 @@ public class LifxLightHandler extends BaseThingHandler { PercentType localPowerOnTemperature = powerOnTemperature; if (localPowerOnTemperature != null && onOff == OnOffType.ON) { getLightStateForCommand() - .setTemperature(percentTypeToKelvin(localPowerOnTemperature, product.getTemperatureRange())); + .setTemperature(percentTypeToKelvin(localPowerOnTemperature, features.getTemperatureRange())); } PercentType powerOnBrightness = this.powerOnBrightness; @@ -658,14 +676,14 @@ public class LifxLightHandler extends BaseThingHandler { private void handleIncreaseDecreaseTemperatureCommand(IncreaseDecreaseType increaseDecrease) { PercentType baseTemperature = kelvinToPercentType(getLightStateForCommand().getColor().getKelvin(), - product.getTemperatureRange()); + features.getTemperatureRange()); PercentType newTemperature = increaseDecreasePercentType(increaseDecrease, baseTemperature); handleTemperatureCommand(newTemperature); } private void handleIncreaseDecreaseTemperatureCommand(IncreaseDecreaseType increaseDecrease, int zoneIndex) { PercentType baseTemperature = kelvinToPercentType(getLightStateForCommand().getColor(zoneIndex).getKelvin(), - product.getTemperatureRange()); + features.getTemperatureRange()); PercentType newTemperature = increaseDecreasePercentType(increaseDecrease, baseTemperature); handleTemperatureCommand(newTemperature, zoneIndex); } @@ -691,7 +709,18 @@ public class LifxLightHandler extends BaseThingHandler { flameSpeedInMSecs.longValue()); getLightStateForCommand().setTileEffect(effect); } catch (IllegalArgumentException e) { - logger.debug("Wrong effect type received as command: {}", type); + logger.debug("{} : Wrong effect type received as command: {}", logId, type); + } + } + + @Override + protected void updateProperties(Map properties) { + String oldHostVersion = getThing().getProperties().get(LifxBindingConstants.PROPERTY_HOST_VERSION); + super.updateProperties(properties); + String newHostVersion = getThing().getProperties().get(LifxBindingConstants.PROPERTY_HOST_VERSION); + + if (!Objects.equals(oldHostVersion, newHostVersion)) { + features.update(getFeatures()); } } diff --git a/bundles/org.openhab.binding.lifx/src/test/java/org/openhab/binding/lifx/internal/LifxProductTest.java b/bundles/org.openhab.binding.lifx/src/test/java/org/openhab/binding/lifx/internal/LifxProductTest.java new file mode 100644 index 00000000000..d0547ed6709 --- /dev/null +++ b/bundles/org.openhab.binding.lifx/src/test/java/org/openhab/binding/lifx/internal/LifxProductTest.java @@ -0,0 +1,140 @@ +/** + * Copyright (c) 2010-2021 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.binding.lifx.internal; + +import static org.hamcrest.CoreMatchers.*; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.hasSize; +import static org.openhab.binding.lifx.internal.LifxProduct.Feature.*; +import static org.openhab.binding.lifx.internal.LifxProduct.TemperatureRange.*; + +import java.util.HashSet; +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; +import org.openhab.binding.lifx.internal.LifxProduct.Features; +import org.openhab.binding.lifx.internal.LifxProduct.Upgrade; + +/** + * Tests {@link LifxProduct}. + * + * @author Wouter Born - Initial contribution + */ +@NonNullByDefault +public class LifxProductTest { + + @Test + public void productIDsAreUnique() { + Set productIDs = new HashSet<>(); + for (LifxProduct product : LifxProduct.values()) { + assertThat(productIDs, not(hasItem(product.getID()))); + productIDs.add(product.getID()); + } + } + + @Test + public void productNamesMatchProductIDs() { + for (LifxProduct product : LifxProduct.values()) { + assertThat(product.name(), is("PRODUCT_" + product.getID())); + } + } + + @Test + public void lightsHaveDefinedTemperatureRange() { + for (LifxProduct product : LifxProduct.values()) { + if (product.isLight()) { + String reason = String.format("The %s light does not define a temperature range", product.name()); + assertThat(reason, product.getFeatures().getTemperatureRange(), is(not(NONE))); + } + } + } + + @Test + public void upgradesSortedByMajorMinor() { + for (LifxProduct product : LifxProduct.values()) { + long major = 0; + long minor = 0; + for (Upgrade upgrade : product.getUpgrades()) { + String reason = String.format("Upgrades for %s are not sorted by major minor (%s.%s >= %s.%s)", + product.name(), major, minor, upgrade.major, upgrade.minor); + assertThat(reason, major < upgrade.major || (major == upgrade.major && minor < upgrade.minor), + is(true)); + major = upgrade.major; + minor = upgrade.minor; + } + } + } + + @Test + public void getFeaturesForProductWithoutUpgrades() { + LifxProduct product = LifxProduct.PRODUCT_1; + assertThat(product.getUpgrades(), hasSize(0)); + + Features features = product.getFeatures(); + assertThat(features.getTemperatureRange(), is(TR_2500_9000)); + assertThat(features.hasFeature(COLOR), is(true)); + + features = product.getFeatures("1.23"); + assertThat(features.getTemperatureRange(), is(TR_2500_9000)); + assertThat(features.hasFeature(COLOR), is(true)); + } + + @Test + public void getFeaturesForProductWithUpgrades() { + LifxProduct product = LifxProduct.PRODUCT_32; + assertThat(product.getUpgrades(), hasSize(2)); + + Features features = product.getFeatures(); + assertThat(features.getTemperatureRange(), is(TR_2500_9000)); + assertThat(features.hasFeature(COLOR), is(true)); + assertThat(features.hasFeature(EXTENDED_MULTIZONE), is(false)); + assertThat(features.hasFeature(INFRARED), is(false)); + assertThat(features.hasFeature(MULTIZONE), is(true)); + + features = product.getFeatures("2.70"); + assertThat(features.getTemperatureRange(), is(TR_2500_9000)); + assertThat(features.hasFeature(COLOR), is(true)); + assertThat(features.hasFeature(EXTENDED_MULTIZONE), is(false)); + assertThat(features.hasFeature(INFRARED), is(false)); + assertThat(features.hasFeature(MULTIZONE), is(true)); + + features = product.getFeatures("2.77"); + assertThat(features.getTemperatureRange(), is(TR_2500_9000)); + assertThat(features.hasFeature(COLOR), is(true)); + assertThat(features.hasFeature(EXTENDED_MULTIZONE), is(true)); + assertThat(features.hasFeature(INFRARED), is(false)); + assertThat(features.hasFeature(MULTIZONE), is(true)); + + features = product.getFeatures("2.79"); + assertThat(features.getTemperatureRange(), is(TR_2500_9000)); + assertThat(features.hasFeature(COLOR), is(true)); + assertThat(features.hasFeature(EXTENDED_MULTIZONE), is(true)); + assertThat(features.hasFeature(INFRARED), is(false)); + assertThat(features.hasFeature(MULTIZONE), is(true)); + + features = product.getFeatures("2.80"); + assertThat(features.getTemperatureRange(), is(TR_1500_9000)); + assertThat(features.hasFeature(COLOR), is(true)); + assertThat(features.hasFeature(EXTENDED_MULTIZONE), is(true)); + assertThat(features.hasFeature(INFRARED), is(false)); + assertThat(features.hasFeature(MULTIZONE), is(true)); + + features = product.getFeatures("2.81"); + assertThat(features.getTemperatureRange(), is(TR_1500_9000)); + assertThat(features.hasFeature(COLOR), is(true)); + assertThat(features.hasFeature(EXTENDED_MULTIZONE), is(true)); + assertThat(features.hasFeature(INFRARED), is(false)); + assertThat(features.hasFeature(MULTIZONE), is(true)); + } +}