From a0027e24fd4507e241f705838c97edff0b0d21d8 Mon Sep 17 00:00:00 2001 From: lolodomo Date: Sat, 2 May 2020 18:06:58 +0200 Subject: [PATCH] [hue] Add support for groups/rooms Fix #7419 Signed-off-by: Laurent Garnier --- bundles/org.openhab.binding.hue/README.md | 57 ++- .../binding/hue/internal/FullGroup.java | 22 +- .../openhab/binding/hue/internal/Group.java | 24 +- .../hue/internal/HueBindingConstants.java | 5 + .../binding/hue/internal/HueBridge.java | 14 +- .../hue/internal/HueThingHandlerFactory.java | 24 +- .../openhab/binding/hue/internal/State.java | 29 ++ .../discovery/HueLightDiscoveryService.java | 64 ++- .../internal/handler/GroupStatusListener.java | 60 +++ .../internal/handler/HueBridgeHandler.java | 196 ++++++++- .../hue/internal/handler/HueClient.java | 35 ++ .../hue/internal/handler/HueGroupHandler.java | 373 ++++++++++++++++++ .../resources/ESH-INF/i18n/hue.properties | 3 + .../resources/ESH-INF/i18n/hue_fr.properties | 8 + .../main/resources/ESH-INF/thing/Group.xml | 37 ++ 15 files changed, 905 insertions(+), 46 deletions(-) create mode 100644 bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/GroupStatusListener.java create mode 100644 bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/HueGroupHandler.java create mode 100644 bundles/org.openhab.binding.hue/src/main/resources/ESH-INF/thing/Group.xml diff --git a/bundles/org.openhab.binding.hue/README.md b/bundles/org.openhab.binding.hue/README.md index bc2c6ac88a9e4..93aeb290ff9a8 100644 --- a/bundles/org.openhab.binding.hue/README.md +++ b/bundles/org.openhab.binding.hue/README.md @@ -75,11 +75,13 @@ hue:0210:00178810d0dc:1 The thing type is the second string behind the first colon and in this example it is **0210**. +Finally, the Hue binding also supports the groups of lights and rooms set up on the Hue bridge. + ## Discovery The Hue bridge is discovered through UPnP in the local network. Once it is added as a Thing, its authentication button (in the middle) needs to be pressed in order to authorize the binding to access it. -Once the binding is authorized, it automatically reads all devices that are set up on the Hue bridge and puts them into the Inbox. +Once the binding is authorized, it automatically reads all devices and groups that are set up on the Hue bridge and puts them into the Inbox. ## Thing Configuration @@ -138,16 +140,36 @@ The following device types also have an optional configuration value to specify | fadetime | Fade time in Milliseconds to a new state (min="0", step="100", default="400") | +### Groups + +The groups are identified by the number that the Hue bridge assigns to them. +Thus, all it needs for manual configuration is this single value like + +``` +group kitchen-bulbs "Kitchen Lamps" @ "Kitchen" [ groupId="1" ] +``` + +You can freely choose the thing identifier (such as kitchen-bulbs), its name (such as "Kitchen Lamps") and the location (such as "Kitchen"). +The name will then be used e.g. by Paper UI to show the item. + +The group type also have an optional configuration value to specify the fade time in milliseconds for the transition to a new state. + +| Parameter | Description | +|-----------|-------------------------------------------------------------------------------| +| groupId | Number of the group provided by the Hue bridge. **Mandatory** | +| fadetime | Fade time in Milliseconds to a new state (min="0", step="100", default="400") | + + ## Channels The devices support some of the following channels: | Channel Type ID | Item Type | Description | Thing types supporting this channel | |-------------------|--------------------|-----------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------| -| switch | Switch | This channel supports switching the device on and off. | 0000, 0010 | -| color | Color | This channel supports full color control with hue, saturation and brightness values. | 0200, 0210 | -| brightness | Dimmer | This channel supports adjusting the brightness value. Note that this is not available, if the color channel is supported. | 0100, 0110, 0220 | -| color_temperature | Dimmer | This channel supports adjusting the color temperature from cold (0%) to warm (100%). | 0210, 0220 | +| switch | Switch | This channel supports switching the device on and off. | 0000, 0010, group | +| color | Color | This channel supports full color control with hue, saturation and brightness values. | 0200, 0210, group | +| brightness | Dimmer | This channel supports adjusting the brightness value. Note that this is not available, if the color channel is supported. | 0100, 0110, 0220, group | +| color_temperature | Dimmer | This channel supports adjusting the color temperature from cold (0%) to warm (100%). | 0210, 0220, group | | alert | String | This channel supports displaying alerts by flashing the bulb either once or multiple times. Valid values are: NONE, SELECT and LSELECT. | 0000, 0100, 0200, 0210, 0220 | | effect | Switch | This channel supports color looping. | 0200, 0210, 0220 | | dimmer_switch | Number | This channel shows which button was last pressed on the dimmer switch. | 0820 | @@ -236,12 +258,13 @@ And there is one Hue Motion Sensor (represented by three devices) and a Hue Dimm ``` Bridge hue:bridge:1 "Hue Bridge" [ ipAddress="192.168.0.64" ] { - 0210 bulb1 "Lamp 1" @ "Kitchen" [ lightId="1" ] - 0220 bulb2 "Lamp 2" @ "Kitchen" [ lightId="2" ] - 0106 light-level-sensor "Light-Sensor" @ "Entrance" [ sensorId="3" ] - 0107 motion-sensor "Motion-Sensor" @ "Entrance" [ sensorId="4" ] - 0302 temperature-sensor "Temp-Sensor" @ "Entrance" [ sensorId="5" ] - 0820 dimmer-switch "Dimmer-Switch" @ "Entrance" [ sensorId="6" ] + 0210 bulb1 "Lamp 1" @ "Kitchen" [ lightId="1" ] + 0220 bulb2 "Lamp 2" @ "Kitchen" [ lightId="2" ] + group kitchen-bulbs "Kitchen Lamps" @ "Kitchen" [ groupId="1" ] + 0106 light-level-sensor "Light-Sensor" @ "Entrance" [ sensorId="3" ] + 0107 motion-sensor "Motion-Sensor" @ "Entrance" [ sensorId="4" ] + 0302 temperature-sensor "Temp-Sensor" @ "Entrance" [ sensorId="5" ] + 0820 dimmer-switch "Dimmer-Switch" @ "Entrance" [ sensorId="6" ] } ``` @@ -261,6 +284,12 @@ Switch Light2_Toggle { channel="hue:0220:1:bulb2:brightness" } Dimmer Light2_Dimmer { channel="hue:0220:1:bulb2:brightness" } Dimmer Light2_ColorTemp { channel="hue:0220:1:bulb2:color_temperature" } +// Kitchen +Switch Kitchen_Switch { channel="hue:group:1:kitchen-bulbs:switch" } +Dimmer Kitchen_Dimmer { channel="hue:group:1:kitchen-bulbs:brightness" } +Color Kitchen_Color { channel="hue:group:1:kitchen-bulbs:color" } +Dimmer Kitchen_ColorTemp { channel="hue:group:1:kitchen-bulbs:color_temperature" } + // Light Level Sensor Number:Illuminance LightLevelSensorIlluminance { channel="hue:0106:1:light-level-sensor:illuminance" } @@ -296,6 +325,12 @@ sitemap demo label="Main Menu" Slider item= Light2_Dimmer Slider item= Light2_ColorTemp + // Kitchen + Switch item= Kitchen_Switch + Slider item= Kitchen_Dimmer + Colorpicker item= Kitchen_Color + Slider item= Kitchen_ColorTemp + // Motion Sensor Switch item=MotionSensorPresence Text item=MotionSensorLastUpdate diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/FullGroup.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/FullGroup.java index 222929e27fc7e..f51b8974df21e 100644 --- a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/FullGroup.java +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/FullGroup.java @@ -12,17 +12,26 @@ */ package org.openhab.binding.hue.internal; +import java.lang.reflect.Type; import java.util.List; +import java.util.Map; + +import com.google.gson.reflect.TypeToken; /** * Detailed group information. * * @author Q42 - Initial contribution * @author Denis Dudnik - moved Jue library source code inside the smarthome Hue binding + * @author Laurent Garnier - field state added */ public class FullGroup extends Group { + public static final Type GSON_TYPE = new TypeToken>() { + }.getType(); + private State action; private List lights; + private State state; // Will not be set by hue API FullGroup() { } @@ -42,7 +51,16 @@ public State getAction() { * * @return lights in the group */ - public List getLights() { - return Util.idsToLights(lights); + public List getLights() { + return lights; + } + + /** + * Returns the current state of the group. + * + * @return current state + */ + public State getState() { + return state; } } diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/Group.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/Group.java index 1e2103e5168f7..f921af7a5bc6a 100644 --- a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/Group.java +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/Group.java @@ -12,27 +12,22 @@ */ package org.openhab.binding.hue.internal; -import java.lang.reflect.Type; -import java.util.Map; - -import com.google.gson.reflect.TypeToken; - /** * Basic group information. * * @author Q42 - Initial contribution * @author Denis Dudnik - moved Jue library source code inside the smarthome Hue binding + * @author Laurent Garnier - field type added */ public class Group { - public static final Type GSON_TYPE = new TypeToken>() { - }.getType(); - private String id; private String name; + private String type; Group() { this.id = "0"; this.name = "Lightset 0"; + this.type = "LightGroup"; } void setName(String name) { @@ -43,6 +38,10 @@ void setId(String id) { this.id = id; } + void setType(String type) { + this.type = type; + } + /** * Returns if the group can be modified. * Currently only returns false for the all lights pseudo group. @@ -70,4 +69,13 @@ public String getId() { public String getName() { return name; } + + /** + * Returns the tyoe of the group. + * + * @return type + */ + public String getType() { + return type; + } } diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/HueBindingConstants.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/HueBindingConstants.java index 7802c2e216b31..66eb7a84e89b0 100644 --- a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/HueBindingConstants.java +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/HueBindingConstants.java @@ -12,6 +12,7 @@ */ package org.openhab.binding.hue.internal; +import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.smarthome.core.thing.ThingTypeUID; /** @@ -25,6 +26,7 @@ * @author Samuel Leisering - Added support for sensor API * @author Christoph Weitkamp - Added support for sensor API */ +@NonNullByDefault public class HueBindingConstants { public static final String BINDING_ID = "hue"; @@ -51,6 +53,8 @@ public class HueBindingConstants { public static final ThingTypeUID THING_TYPE_TEMPERATURE_SENSOR = new ThingTypeUID(BINDING_ID, "0302"); public static final ThingTypeUID THING_TYPE_LIGHT_LEVEL_SENSOR = new ThingTypeUID(BINDING_ID, "0106"); + public static final ThingTypeUID THING_TYPE_GROUP = new ThingTypeUID(BINDING_ID, "group"); + // List all channels public static final String CHANNEL_COLORTEMPERATURE = "color_temperature"; public static final String CHANNEL_COLOR = "color"; @@ -88,6 +92,7 @@ public class HueBindingConstants { public static final String PRODUCT_NAME = "productName"; public static final String UNIQUE_ID = "uniqueId"; public static final String FADETIME = "fadetime"; + public static final String GROUP_ID = "groupId"; public static final String NORMALIZE_ID_REGEX = "[^a-zA-Z0-9_]"; } diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/HueBridge.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/HueBridge.java index aab93fcabb212..dffcf7223479e 100644 --- a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/HueBridge.java +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/HueBridge.java @@ -55,6 +55,7 @@ * @author Andre Fuechsel - search for lights with given serial number added * @author Denis Dudnik - moved Jue library source code inside the smarthome Hue binding, minor code cleanup * @author Samuel Leisering - added cached config and API-Version + * @author Laurent Garnier - change the return type of getGroups */ @NonNullByDefault public class HueBridge { @@ -415,20 +416,23 @@ public Group getAllGroup() { * @return list of groups * @throws UnauthorizedException thrown if the user no longer exists */ - public List getGroups() throws IOException, ApiException { + public List getGroups() throws IOException, ApiException { requireAuthentication(); Result result = http.get(getRelativeURL("groups")); handleErrors(result); - Map groupMap = safeFromJson(result.getBody(), Group.GSON_TYPE); - ArrayList groupList = new ArrayList<>(); + Map groupMap = safeFromJson(result.getBody(), FullGroup.GSON_TYPE); + ArrayList groupList = new ArrayList<>(); - groupList.add(new Group()); + if (groupMap.get("0") == null) { + // Group 0 is not returned, we create it as in fact it exists + groupList.add(getGroup(new Group())); + } for (String id : groupMap.keySet()) { - Group group = groupMap.get(id); + FullGroup group = groupMap.get(id); group.setId(id); groupList.add(group); } diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/HueThingHandlerFactory.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/HueThingHandlerFactory.java index f8d997699524b..2c0bf3df8a545 100644 --- a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/HueThingHandlerFactory.java +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/HueThingHandlerFactory.java @@ -35,6 +35,7 @@ import org.eclipse.smarthome.core.thing.binding.ThingHandlerFactory; import org.openhab.binding.hue.internal.discovery.HueLightDiscoveryService; import org.openhab.binding.hue.internal.handler.HueBridgeHandler; +import org.openhab.binding.hue.internal.handler.HueGroupHandler; import org.openhab.binding.hue.internal.handler.HueLightHandler; import org.openhab.binding.hue.internal.handler.sensors.ClipHandler; import org.openhab.binding.hue.internal.handler.sensors.DimmerSwitchHandler; @@ -53,16 +54,17 @@ * @author Andre Fuechsel - implemented to use one discovery service per bridge * @author Samuel Leisering - Added support for sensor API * @author Christoph Weitkamp - Added support for sensor API + * @author Laurent Garnier - Added support for groups */ @NonNullByDefault @Component(service = ThingHandlerFactory.class, configurationPid = "binding.hue") public class HueThingHandlerFactory extends BaseThingHandlerFactory { - public static final Set SUPPORTED_THING_TYPES = Collections.unmodifiableSet(Stream - .of(HueBridgeHandler.SUPPORTED_THING_TYPES.stream(), HueLightHandler.SUPPORTED_THING_TYPES.stream(), + public static final Set SUPPORTED_THING_TYPES = Collections.unmodifiableSet( + Stream.of(HueBridgeHandler.SUPPORTED_THING_TYPES.stream(), HueLightHandler.SUPPORTED_THING_TYPES.stream(), DimmerSwitchHandler.SUPPORTED_THING_TYPES.stream(), TapSwitchHandler.SUPPORTED_THING_TYPES.stream(), PresenceHandler.SUPPORTED_THING_TYPES.stream(), TemperatureHandler.SUPPORTED_THING_TYPES.stream(), - LightLevelHandler.SUPPORTED_THING_TYPES.stream(), ClipHandler.SUPPORTED_THING_TYPES.stream()) - .flatMap(i -> i).collect(Collectors.toSet())); + LightLevelHandler.SUPPORTED_THING_TYPES.stream(), ClipHandler.SUPPORTED_THING_TYPES.stream(), + HueGroupHandler.SUPPORTED_THING_TYPES.stream()).flatMap(i -> i).collect(Collectors.toSet())); private final Map> discoveryServiceRegs = new HashMap<>(); @@ -82,6 +84,9 @@ public class HueThingHandlerFactory extends BaseThingHandlerFactory { || ClipHandler.SUPPORTED_THING_TYPES.contains(thingTypeUID)) { ThingUID hueSensorUID = getSensorUID(thingTypeUID, thingUID, configuration, bridgeUID); return super.createThing(thingTypeUID, configuration, hueSensorUID, bridgeUID); + } else if (HueGroupHandler.SUPPORTED_THING_TYPES.contains(thingTypeUID)) { + ThingUID hueGroupUID = getGroupUID(thingTypeUID, thingUID, configuration, bridgeUID); + return super.createThing(thingTypeUID, configuration, hueGroupUID, bridgeUID); } throw new IllegalArgumentException("The thing type " + thingTypeUID + " is not supported by the hue binding."); @@ -110,6 +115,15 @@ private ThingUID getSensorUID(ThingTypeUID thingTypeUID, @Nullable ThingUID thin } } + private ThingUID getGroupUID(ThingTypeUID thingTypeUID, @Nullable ThingUID thingUID, Configuration configuration, + @Nullable ThingUID bridgeUID) { + if (thingUID != null) { + return thingUID; + } else { + return getThingUID(thingTypeUID, configuration.get(GROUP_ID).toString(), bridgeUID); + } + } + private ThingUID getThingUID(ThingTypeUID thingTypeUID, String id, @Nullable ThingUID bridgeUID) { if (bridgeUID != null) { return new ThingUID(thingTypeUID, id, bridgeUID.getId()); @@ -138,6 +152,8 @@ private ThingUID getThingUID(ThingTypeUID thingTypeUID, String id, @Nullable Thi return new LightLevelHandler(thing); } else if (ClipHandler.SUPPORTED_THING_TYPES.contains(thing.getThingTypeUID())) { return new ClipHandler(thing); + } else if (HueGroupHandler.SUPPORTED_THING_TYPES.contains(thing.getThingTypeUID())) { + return new HueGroupHandler(thing); } else { return null; } diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/State.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/State.java index dafd8c183f810..5faf630504ca5 100644 --- a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/State.java +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/State.java @@ -19,6 +19,7 @@ * * @author Q42 - Initial contribution * @author Denis Dudnik - moved Jue library source code inside the smarthome Hue binding + * @author Laurent Garnier - add few methods to update the object */ public class State { private boolean on; @@ -99,6 +100,10 @@ public boolean isOn() { return on; } + public void setOn(boolean on) { + this.on = on; + } + /** * Returns the brightness. * @@ -108,6 +113,10 @@ public int getBrightness() { return bri; } + public void setBri(int bri) { + this.bri = bri; + } + /** * Returns the hue. * @@ -117,6 +126,10 @@ public int getHue() { return hue; } + public void setHue(int hue) { + this.hue = hue; + } + /** * Returns the saturation. * @@ -126,6 +139,10 @@ public int getSaturation() { return sat; } + public void setSaturation(int sat) { + this.sat = sat; + } + /** * Returns the coordinates in CIE color space. * @@ -135,6 +152,10 @@ public float[] getXY() { return xy; } + public void setXY(float[] xy) { + this.xy = xy; + } + /** * Returns the color temperature. * @@ -144,6 +165,10 @@ public int getColorTemperature() { return ct; } + public void setColorTemperature(int ct) { + this.ct = ct; + } + /** * Returns the last alert mode set. * Future firmware updates may change this to actually report the current alert mode. @@ -169,6 +194,10 @@ public ColorMode getColorMode() { return ColorMode.valueOf(colormode.toUpperCase()); } + public void setColormode(ColorMode colormode) { + this.colormode = colormode.name(); + } + /** * Returns the current active effect. * diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/discovery/HueLightDiscoveryService.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/discovery/HueLightDiscoveryService.java index 5aef4039bf9d0..cac98bd813176 100644 --- a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/discovery/HueLightDiscoveryService.java +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/discovery/HueLightDiscoveryService.java @@ -32,11 +32,14 @@ import org.eclipse.smarthome.core.thing.Thing; import org.eclipse.smarthome.core.thing.ThingTypeUID; import org.eclipse.smarthome.core.thing.ThingUID; +import org.openhab.binding.hue.internal.FullGroup; import org.openhab.binding.hue.internal.FullHueObject; import org.openhab.binding.hue.internal.FullLight; import org.openhab.binding.hue.internal.FullSensor; import org.openhab.binding.hue.internal.HueBridge; +import org.openhab.binding.hue.internal.handler.GroupStatusListener; import org.openhab.binding.hue.internal.handler.HueBridgeHandler; +import org.openhab.binding.hue.internal.handler.HueGroupHandler; import org.openhab.binding.hue.internal.handler.HueLightHandler; import org.openhab.binding.hue.internal.handler.LightStatusListener; import org.openhab.binding.hue.internal.handler.SensorStatusListener; @@ -61,15 +64,16 @@ * @author Samuel Leisering - Added support for sensor API * @author Christoph Weitkamp - Added support for sensor API * @author Meng Yiqi - Added support for CLIP sensor + * @author Laurent Garnier - Added support for groups */ @NonNullByDefault public class HueLightDiscoveryService extends AbstractDiscoveryService - implements LightStatusListener, SensorStatusListener { + implements LightStatusListener, SensorStatusListener, GroupStatusListener { public static final Set SUPPORTED_THING_TYPES = Collections.unmodifiableSet(Stream .of(HueLightHandler.SUPPORTED_THING_TYPES.stream(), DimmerSwitchHandler.SUPPORTED_THING_TYPES.stream(), TapSwitchHandler.SUPPORTED_THING_TYPES.stream(), PresenceHandler.SUPPORTED_THING_TYPES.stream(), TemperatureHandler.SUPPORTED_THING_TYPES.stream(), LightLevelHandler.SUPPORTED_THING_TYPES.stream(), - ClipHandler.SUPPORTED_THING_TYPES.stream()) + ClipHandler.SUPPORTED_THING_TYPES.stream(), HueGroupHandler.SUPPORTED_THING_TYPES.stream()) .flatMap(i -> i).collect(Collectors.toSet())); private final Logger logger = LoggerFactory.getLogger(HueLightDiscoveryService.class); @@ -105,6 +109,7 @@ public HueLightDiscoveryService(HueBridgeHandler hueBridgeHandler) { public void activate() { hueBridgeHandler.registerLightStatusListener(this); hueBridgeHandler.registerSensorStatusListener(this); + hueBridgeHandler.registerGroupStatusListener(this); } @Override @@ -112,6 +117,7 @@ public void deactivate() { removeOlderResults(new Date().getTime(), hueBridgeHandler.getThing().getUID()); hueBridgeHandler.unregisterLightStatusListener(this); hueBridgeHandler.unregisterSensorStatusListener(this); + hueBridgeHandler.unregisterGroupStatusListener(this); } @Override @@ -129,6 +135,10 @@ public void startScan() { for (FullSensor s : sensors) { onSensorAddedInternal(s); } + List groups = hueBridgeHandler.getFullGroups(); + for (FullGroup g : groups) { + onGroupAddedInternal(g); + } // search for unpaired lights hueBridgeHandler.startSearch(); } @@ -256,7 +266,7 @@ public void onSensorRemoved(@Nullable HueBridge bridge, FullSensor sensor) { onSensorRemovedInternal(sensor); } - public void onSensorRemovedInternal(FullSensor sensor) { + private void onSensorRemovedInternal(FullSensor sensor) { ThingUID thingUID = getThingUID(sensor); if (thingUID != null) { @@ -268,4 +278,52 @@ public void onSensorRemovedInternal(FullSensor sensor) { public void onSensorStateChanged(@Nullable HueBridge bridge, FullSensor sensor) { // nothing to do } + + @Override + public void onGroupAdded(@Nullable HueBridge bridge, FullGroup group) { + onGroupAddedInternal(group); + } + + private void onGroupAddedInternal(FullGroup group) { + // Ignore the Hue Entertainment Areas + if ("Entertainment".equalsIgnoreCase(group.getType())) { + return; + } + + ThingUID bridgeUID = hueBridgeHandler.getThing().getUID(); + ThingUID thingUID = new ThingUID(THING_TYPE_GROUP, bridgeUID, group.getId()); + + Map properties = new HashMap<>(); + properties.put(GROUP_ID, group.getId()); + + String name = String.format("%s (%s)", "0".equals(group.getId()) ? "All lights" : group.getName(), + group.getType()); + DiscoveryResult discoveryResult = DiscoveryResultBuilder.create(thingUID).withThingType(THING_TYPE_GROUP) + .withProperties(properties).withBridge(bridgeUID).withRepresentationProperty(GROUP_ID).withLabel(name) + .build(); + + thingDiscovered(discoveryResult); + } + + @Override + public void onGroupGone(@Nullable HueBridge bridge, FullGroup group) { + onGroupRemovedInternal(group); + } + + @Override + public void onGroupRemoved(@Nullable HueBridge bridge, FullGroup group) { + onGroupRemovedInternal(group); + } + + private void onGroupRemovedInternal(FullGroup group) { + ThingUID bridgeUID = hueBridgeHandler.getThing().getUID(); + ThingUID thingUID = new ThingUID(THING_TYPE_GROUP, bridgeUID, group.getId()); + thingRemoved(thingUID); + } + + @Override + public void onGroupStateChanged(@Nullable HueBridge bridge, FullGroup group) { + // nothing to do + } + } diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/GroupStatusListener.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/GroupStatusListener.java new file mode 100644 index 0000000000000..cccc27840134e --- /dev/null +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/GroupStatusListener.java @@ -0,0 +1,60 @@ +/** + * Copyright (c) 2010-2020 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.hue.internal.handler; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.hue.internal.FullGroup; +import org.openhab.binding.hue.internal.HueBridge; + +/** + * The {@link GroupStatusListener} is notified when a group status has changed or a group has been removed or added. + * + * @author Laurent Garnier - Initial contribution + */ +@NonNullByDefault +public interface GroupStatusListener { + + /** + * This method is called whenever the state of the given group has changed. The new state can be obtained by + * {@link FullGroup#getState()}. + * + * @param bridge The bridge the changed group is connected to. + * @param group The group which received the state update. + */ + void onGroupStateChanged(@Nullable HueBridge bridge, FullGroup group); + + /** + * This method is called whenever a group is removed. + * + * @param bridge The bridge the removed group was connected to. + * @param group The removed group + */ + void onGroupRemoved(@Nullable HueBridge bridge, FullGroup group); + + /** + * This method is called whenever a group is reported as gone. + * + * @param bridge The bridge the reported group was connected to. + * @param group The group which is reported as gone. + */ + void onGroupGone(@Nullable HueBridge bridge, FullGroup group); + + /** + * This method is called whenever a group is added. + * + * @param bridge The bridge the added group was connected to. + * @param group The added group + */ + void onGroupAdded(@Nullable HueBridge bridge, FullGroup group); +} diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/HueBridgeHandler.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/HueBridgeHandler.java index 0102bae0ba8a9..f2a5344ec00aa 100644 --- a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/HueBridgeHandler.java +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/HueBridgeHandler.java @@ -34,6 +34,7 @@ import org.eclipse.jdt.annotation.Nullable; import org.eclipse.smarthome.config.core.Configuration; import org.eclipse.smarthome.config.core.status.ConfigStatusMessage; +import org.eclipse.smarthome.core.library.types.HSBType; import org.eclipse.smarthome.core.library.types.OnOffType; import org.eclipse.smarthome.core.thing.Bridge; import org.eclipse.smarthome.core.thing.ChannelUID; @@ -46,6 +47,7 @@ import org.openhab.binding.hue.internal.Config; import org.openhab.binding.hue.internal.ConfigUpdate; import org.openhab.binding.hue.internal.FullConfig; +import org.openhab.binding.hue.internal.FullGroup; import org.openhab.binding.hue.internal.FullLight; import org.openhab.binding.hue.internal.FullSensor; import org.openhab.binding.hue.internal.HueBridge; @@ -75,6 +77,7 @@ * @author Denis Dudnik - switched to internally integrated source of Jue library * @author Samuel Leisering - Added support for sensor API * @author Christoph Weitkamp - Added support for sensor API + * @author Laurent Garnier - Added support for groups */ @NonNullByDefault public class HueBridgeHandler extends ConfigStatusBridgeHandler implements HueClient { @@ -94,9 +97,11 @@ private static enum StatusType { private final Map lastLightStates = new ConcurrentHashMap<>(); private final Map lastSensorStates = new ConcurrentHashMap<>(); + private final Map lastGroupStates = new ConcurrentHashMap<>(); private final List lightStatusListeners = new CopyOnWriteArrayList<>(); private final List sensorStatusListeners = new CopyOnWriteArrayList<>(); + private final List groupStatusListeners = new CopyOnWriteArrayList<>(); final ReentrantLock pollingLock = new ReentrantLock(); @@ -209,16 +214,15 @@ protected void doConnectedRun() throws IOException, ApiException { for (final FullLight fullLight : lights) { final String lightId = fullLight.getId(); + lastLightStates.put(lightId, fullLight); if (lastLightStateCopy.containsKey(lightId)) { final FullLight lastFullLight = lastLightStateCopy.remove(lightId); final State lastFullLightState = lastFullLight.getState(); - lastLightStates.put(lightId, fullLight); if (!lastFullLightState.equals(fullLight.getState())) { logger.debug("Status update for Hue light '{}' detected.", lightId); notifyLightStatusListeners(fullLight, StatusType.CHANGED); } } else { - lastLightStates.put(lightId, fullLight); logger.debug("Hue light '{}' added.", lightId); notifyLightStatusListeners(fullLight, StatusType.ADDED); } @@ -230,6 +234,76 @@ protected void doConnectedRun() throws IOException, ApiException { logger.debug("Hue light '{}' removed.", fullLightEntry.getKey()); notifyLightStatusListeners(fullLightEntry.getValue(), StatusType.REMOVED); } + + Map lastGroupStateCopy = new HashMap<>(lastGroupStates); + + for (final FullGroup fullGroup : hueBridge.getGroups()) { + State groupState = fullGroup.getState(); + boolean on = false; + int sumBri = 0; + int nbBri = 0; + State colorRef = null; + HSBType firstColorHsb = null; + for (String lightId : fullGroup.getLights()) { + FullLight light = lastLightStates.get(lightId); + if (light != null) { + final State lightState = light.getState(); + logger.trace("Group {}: light {}: on {} bri {} hue {} sat {} temp {} mode {} XY {}", + fullGroup.getName(), light.getName(), lightState.isOn(), lightState.getBrightness(), + lightState.getHue(), lightState.getSaturation(), lightState.getColorTemperature(), + lightState.getColorMode(), lightState.getXY()); + if (lightState.isOn()) { + on = true; + sumBri += lightState.getBrightness(); + nbBri++; + if (lightState.getColorMode() != null) { + HSBType lightHsb = LightStateConverter.toHSBType(lightState); + if (firstColorHsb == null) { + // first color light + firstColorHsb = lightHsb; + colorRef = lightState; + } else if (!lightHsb.equals(firstColorHsb)) { + colorRef = null; + } + } + } + } + } + groupState.setOn(on); + groupState.setBri(nbBri == 0 ? 0 : sumBri / nbBri); + if (colorRef != null) { + groupState.setColormode(colorRef.getColorMode()); + groupState.setHue(colorRef.getHue()); + groupState.setSaturation(colorRef.getSaturation()); + groupState.setColorTemperature(colorRef.getColorTemperature()); + groupState.setXY(colorRef.getXY()); + } + logger.trace("Group {} ({}): on {} bri {} hue {} sat {} temp {} mode {} XY {}", fullGroup.getName(), + fullGroup.getType(), groupState.isOn(), groupState.getBrightness(), groupState.getHue(), + groupState.getSaturation(), groupState.getColorTemperature(), groupState.getColorMode(), + groupState.getXY()); + String groupId = fullGroup.getId(); + lastGroupStates.put(groupId, fullGroup); + if (lastGroupStateCopy.containsKey(groupId)) { + final FullGroup lastFullGroup = lastGroupStateCopy.remove(groupId); + final State lastFullGroupState = lastFullGroup.getState(); + if (!lastFullGroupState.equals(fullGroup.getState())) { + logger.debug("Status update for Hue group '{}' detected.", groupId); + notifyGroupStatusListeners(fullGroup, StatusType.CHANGED); + } + } else { + logger.debug("Hue group '{}' ({}) added (nb lights {}).", groupId, fullGroup.getName(), + fullGroup.getLights().size()); + notifyGroupStatusListeners(fullGroup, StatusType.ADDED); + } + } + + // Check for removed groups + for (Entry fullGroupEntry : lastGroupStateCopy.entrySet()) { + lastGroupStates.remove(fullGroupEntry.getKey()); + logger.debug("Hue group '{}' removed.", fullGroupEntry.getKey()); + notifyGroupStatusListeners(fullGroupEntry.getValue(), StatusType.REMOVED); + } } }; @@ -269,7 +343,7 @@ public void updateLightState(FullLight light, StateUpdate stateUpdate) { return null; }); } else { - logger.warn("No bridge connected or selected. Cannot set light state."); + logger.debug("No bridge connected or selected. Cannot set light state."); } } @@ -287,7 +361,7 @@ public void updateSensorState(FullSensor sensor, StateUpdate stateUpdate) { return null; }); } else { - logger.warn("No bridge connected or selected. Cannot set sensor state."); + logger.debug("No bridge connected or selected. Cannot set sensor state."); } } @@ -305,7 +379,20 @@ public void updateSensorConfig(FullSensor sensor, ConfigUpdate configUpdate) { return null; }); } else { - logger.warn("No bridge connected or selected. Cannot set sensor config."); + logger.debug("No bridge connected or selected. Cannot set sensor config."); + } + } + + @Override + public void updateGroupState(FullGroup group, StateUpdate stateUpdate) { + if (hueBridge != null) { + try { + hueBridge.setGroupState(group, stateUpdate); + } catch (IOException | ApiException e) { + handleStateUpdateException(group, stateUpdate, e); + } + } else { + logger.debug("No bridge connected or selected. Cannot set group state."); } } @@ -346,6 +433,20 @@ private void handleStateUpdateException(FullSensor sensor, StateUpdate stateUpda } } + private void handleStateUpdateException(FullGroup group, StateUpdate stateUpdate, Throwable e) { + if (e instanceof IOException) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage()); + } else if (e instanceof EntityNotAvailableException) { + logger.debug("Error while accessing group: {}", e.getMessage(), e); + notifyGroupStatusListeners(group, StatusType.GONE); + } else if (e instanceof ApiException) { + // This should not happen - if it does, it is most likely some bug that should be reported. + logger.warn("Error while accessing group: {}", e.getMessage(), e); + } else if (e instanceof IllegalStateException) { + logger.trace("Error while accessing group: {}", e.getMessage()); + } + } + private void handleConfigUpdateException(FullSensor sensor, ConfigUpdate configUpdate, Throwable e) { if (e instanceof IOException) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage()); @@ -430,8 +531,9 @@ public void initialize() { private synchronized void onUpdate() { if (hueBridge != null) { - // start light polling only if a light handler has been registered, otherwise stop polling - if (lightStatusListeners.isEmpty()) { + // start light and group polling only if a light handler or a group handler has been registered, + // otherwise stop polling + if (lightStatusListeners.isEmpty() && groupStatusListeners.isEmpty()) { stopLightPolling(); } else { startLightPolling(); @@ -585,7 +687,7 @@ private void handleExceptionWhileCreatingUser(Exception ex) { public boolean registerLightStatusListener(LightStatusListener lightStatusListener) { boolean result = lightStatusListeners.add(lightStatusListener); if (result && hueBridge != null) { - // start light polling only if a light handler has been registered + // start light and group polling only if a light handler has been registered startLightPolling(); // inform the listener initially about all lights and their states for (FullLight light : lastLightStates.values()) { @@ -599,8 +701,8 @@ public boolean registerLightStatusListener(LightStatusListener lightStatusListen public boolean unregisterLightStatusListener(LightStatusListener lightStatusListener) { boolean result = lightStatusListeners.remove(lightStatusListener); if (result) { - // stop stop light polling - if (lightStatusListeners.isEmpty()) { + // stop the light and group polling + if (lightStatusListeners.isEmpty() && groupStatusListeners.isEmpty()) { stopLightPolling(); } } @@ -633,6 +735,32 @@ public boolean unregisterSensorStatusListener(SensorStatusListener sensorStatusL return result; } + @Override + public boolean registerGroupStatusListener(GroupStatusListener groupStatusListener) { + boolean result = groupStatusListeners.add(groupStatusListener); + if (result && hueBridge != null) { + // start light and group polling only if a group handler has been registered + startLightPolling(); + // inform the listener initially about all groups and their states + for (FullGroup group : lastGroupStates.values()) { + groupStatusListener.onGroupAdded(hueBridge, group); + } + } + return result; + } + + @Override + public boolean unregisterGroupStatusListener(GroupStatusListener groupStatusListener) { + boolean result = groupStatusListeners.remove(groupStatusListener); + if (result) { + // stop the light and group polling + if (lightStatusListeners.isEmpty() && groupStatusListeners.isEmpty()) { + stopLightPolling(); + } + } + return result; + } + @Override public @Nullable FullLight getLightById(String lightId) { return lastLightStates.get(lightId); @@ -643,6 +771,11 @@ public boolean unregisterSensorStatusListener(SensorStatusListener sensorStatusL return lastSensorStates.get(sensorId); } + @Override + public @Nullable FullGroup getGroupById(String groupId) { + return lastGroupStates.get(groupId); + } + public List getFullLights() { List ret = withReAuthentication("search for new lights", () -> { return hueBridge.getFullLights(); @@ -657,6 +790,13 @@ public List getFullSensors() { return ret != null ? ret : Collections.emptyList(); } + public List getFullGroups() { + List ret = withReAuthentication("search for new groups", () -> { + return hueBridge.getGroups(); + }); + return ret != null ? ret : Collections.emptyList(); + } + public void startSearch() { withReAuthentication("start search mode", () -> { hueBridge.startSearch(); @@ -683,7 +823,7 @@ private T withReAuthentication(String taskDescription, Callable runnable) } } } catch (Exception e) { - logger.error("Bridge cannot {}.", taskDescription, e); + logger.debug("Bridge cannot {}.", taskDescription, e); } } return null; @@ -720,7 +860,7 @@ private void notifyLightStatusListeners(final FullLight fullLight, StatusType ty break; } } catch (Exception e) { - logger.error("An exception occurred while calling the BridgeHeartbeatListener", e); + logger.debug("An exception occurred while calling the BridgeHeartbeatListener", e); } } } @@ -750,7 +890,37 @@ private void notifySensorStatusListeners(final FullSensor fullSensor, StatusType break; } } catch (Exception e) { - logger.error("An exception occurred while calling the Sensor Listeners", e); + logger.debug("An exception occurred while calling the Sensor Listeners", e); + } + } + } + + private void notifyGroupStatusListeners(final FullGroup fullGroup, StatusType type) { + if (groupStatusListeners.isEmpty()) { + logger.debug("No group status listeners to notify of group change for group '{}'", fullGroup.getId()); + return; + } + + for (GroupStatusListener groupStatusListener : groupStatusListeners) { + try { + switch (type) { + case ADDED: + logger.debug("Sending groupAdded for group '{}'", fullGroup.getId()); + groupStatusListener.onGroupAdded(hueBridge, fullGroup); + break; + case REMOVED: + groupStatusListener.onGroupRemoved(hueBridge, fullGroup); + break; + case GONE: + groupStatusListener.onGroupGone(hueBridge, fullGroup); + break; + case CHANGED: + logger.debug("Sending groupStateChanged for group '{}'", fullGroup.getId()); + groupStatusListener.onGroupStateChanged(hueBridge, fullGroup); + break; + } + } catch (Exception e) { + logger.debug("An exception occurred while calling the Group Listeners", e); } } } diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/HueClient.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/HueClient.java index 6e2af114e41cb..1bd3c83d2142a 100644 --- a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/HueClient.java +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/HueClient.java @@ -15,6 +15,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.hue.internal.ConfigUpdate; +import org.openhab.binding.hue.internal.FullGroup; import org.openhab.binding.hue.internal.FullLight; import org.openhab.binding.hue.internal.FullSensor; import org.openhab.binding.hue.internal.StateUpdate; @@ -25,6 +26,7 @@ * @author Simon Kaufmann - initial contribution and API * @author Samuel Leisering - Added support for sensor API * @author Christoph Weitkamp - Added support for sensor API + * @author Laurent Garnier - Added support for groups */ @NonNullByDefault public interface HueClient { @@ -61,6 +63,22 @@ public interface HueClient { */ boolean unregisterSensorStatusListener(SensorStatusListener sensorStatusListener); + /** + * Register a group status listener. + * + * @param groupStatusListener the group status listener + * @return {@code true} if the collection of listeners has changed as a result of this call + */ + boolean registerGroupStatusListener(GroupStatusListener groupStatusListener); + + /** + * Unregister a group status listener. + * + * @param groupStatusListener the group status listener + * @return {@code true} if the collection of listeners has changed as a result of this call + */ + boolean unregisterGroupStatusListener(GroupStatusListener groupStatusListener); + /** * Get the light by its ID. * @@ -79,6 +97,15 @@ public interface HueClient { @Nullable FullSensor getSensorById(String sensorId); + /** + * Get the group by its ID. + * + * @param groupId the group ID + * @return the full group representation of {@code null} if it could not be found + */ + @Nullable + FullGroup getGroupById(String groupId); + /** * Updates the given light. * @@ -102,4 +129,12 @@ public interface HueClient { * @param stateUpdate the state update */ void updateSensorState(FullSensor sensor, StateUpdate stateUpdate); + + /** + * Updates the given group. + * + * @param group the group to be updated + * @param stateUpdate the state update + */ + void updateGroupState(FullGroup group, StateUpdate stateUpdate); } diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/HueGroupHandler.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/HueGroupHandler.java new file mode 100644 index 0000000000000..e0e5569c7fd86 --- /dev/null +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/HueGroupHandler.java @@ -0,0 +1,373 @@ +/** + * Copyright (c) 2010-2020 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.hue.internal.handler; + +import static org.openhab.binding.hue.internal.HueBindingConstants.*; + +import java.math.BigDecimal; +import java.util.Collections; +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.smarthome.core.library.types.HSBType; +import org.eclipse.smarthome.core.library.types.IncreaseDecreaseType; +import org.eclipse.smarthome.core.library.types.OnOffType; +import org.eclipse.smarthome.core.library.types.PercentType; +import org.eclipse.smarthome.core.thing.Bridge; +import org.eclipse.smarthome.core.thing.ChannelUID; +import org.eclipse.smarthome.core.thing.Thing; +import org.eclipse.smarthome.core.thing.ThingStatus; +import org.eclipse.smarthome.core.thing.ThingStatusDetail; +import org.eclipse.smarthome.core.thing.ThingTypeUID; +import org.eclipse.smarthome.core.thing.binding.BaseThingHandler; +import org.eclipse.smarthome.core.thing.binding.ThingHandler; +import org.eclipse.smarthome.core.types.Command; +import org.eclipse.smarthome.core.types.UnDefType; +import org.openhab.binding.hue.internal.FullGroup; +import org.openhab.binding.hue.internal.HueBridge; +import org.openhab.binding.hue.internal.State; +import org.openhab.binding.hue.internal.State.ColorMode; +import org.openhab.binding.hue.internal.StateUpdate; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * {@link HueGroupHandler} is the handler for a hue group of lights. It uses the {@link HueClient} to execute the + * actual command. + * + * @author Laurent Garnier - Initial contribution + */ +@NonNullByDefault +public class HueGroupHandler extends BaseThingHandler implements GroupStatusListener { + public static final Set SUPPORTED_THING_TYPES = Collections.singleton(THING_TYPE_GROUP); + + private final Logger logger = LoggerFactory.getLogger(HueGroupHandler.class); + + private @NonNullByDefault({}) String groupId; + + private @Nullable Integer lastSentColorTemp; + private @Nullable Integer lastSentBrightness; + + private long defaultFadeTime = 400; + + private @Nullable HueClient hueClient; + + public HueGroupHandler(Thing thing) { + super(thing); + } + + @Override + public void initialize() { + logger.debug("Initializing hue group handler."); + Bridge bridge = getBridge(); + initializeThing((bridge == null) ? null : bridge.getStatus()); + } + + private void initializeThing(@Nullable ThingStatus bridgeStatus) { + logger.debug("initializeThing thing {} bridge status {}", getThing().getUID(), bridgeStatus); + final String configGroupId = (String) getConfig().get(GROUP_ID); + if (configGroupId != null) { + BigDecimal time = (BigDecimal) getConfig().get(FADETIME); + if (time != null) { + defaultFadeTime = time.longValueExact(); + } + + groupId = configGroupId; + // note: this call implicitly registers our handler as a listener on the bridge + if (getHueClient() != null) { + if (bridgeStatus == ThingStatus.ONLINE) { + updateStatus(ThingStatus.ONLINE); + } else { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE); + } + } else { + updateStatus(ThingStatus.OFFLINE); + } + } else { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "@text/offline.conf-error-no-group-id"); + } + } + + @Override + public void dispose() { + logger.debug("Hue group handler disposes. Unregistering listener."); + if (groupId != null) { + HueClient bridgeHandler = getHueClient(); + if (bridgeHandler != null) { + bridgeHandler.unregisterGroupStatusListener(this); + hueClient = null; + } + groupId = null; + } + } + + protected synchronized @Nullable HueClient getHueClient() { + if (hueClient == null) { + Bridge bridge = getBridge(); + if (bridge == null) { + return null; + } + ThingHandler handler = bridge.getHandler(); + if (handler instanceof HueBridgeHandler) { + hueClient = (HueClient) handler; + hueClient.registerGroupStatusListener(this); + } else { + return null; + } + } + return hueClient; + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + handleCommand(channelUID.getId(), command, defaultFadeTime); + } + + public void handleCommand(String channel, Command command, long fadeTime) { + HueClient bridgeHandler = getHueClient(); + if (bridgeHandler == null) { + logger.debug("hue bridge handler not found. Cannot handle command without bridge."); + return; + } + + FullGroup group = bridgeHandler.getGroupById(groupId); + if (group == null) { + logger.debug("hue group not known on bridge. Cannot handle command."); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "@text/offline.conf-error-wrong-group-id"); + return; + } + + StateUpdate groupState = null; + switch (channel) { + case CHANNEL_COLOR: + if (command instanceof HSBType) { + HSBType hsbCommand = (HSBType) command; + if (hsbCommand.getBrightness().intValue() == 0) { + groupState = LightStateConverter.toOnOffLightState(OnOffType.OFF); + } else { + groupState = LightStateConverter.toColorLightState(hsbCommand, group.getState()); + if (groupState != null) { + groupState.setTransitionTime(fadeTime); + } + } + } else if (command instanceof PercentType) { + groupState = LightStateConverter.toBrightnessLightState((PercentType) command); + if (groupState != null) { + groupState.setTransitionTime(fadeTime); + } + } else if (command instanceof OnOffType) { + groupState = LightStateConverter.toOnOffLightState((OnOffType) command); + } else if (command instanceof IncreaseDecreaseType) { + groupState = convertBrightnessChangeToStateUpdate((IncreaseDecreaseType) command, group); + if (groupState != null) { + groupState.setTransitionTime(fadeTime); + } + } + break; + case CHANNEL_COLORTEMPERATURE: + if (command instanceof PercentType) { + groupState = LightStateConverter.toColorTemperatureLightState((PercentType) command); + if (groupState != null) { + groupState.setTransitionTime(fadeTime); + } + } else if (command instanceof OnOffType) { + groupState = LightStateConverter.toOnOffLightState((OnOffType) command); + } else if (command instanceof IncreaseDecreaseType) { + groupState = convertColorTempChangeToStateUpdate((IncreaseDecreaseType) command, group); + if (groupState != null) { + groupState.setTransitionTime(fadeTime); + } + } + break; + case CHANNEL_BRIGHTNESS: + if (command instanceof PercentType) { + groupState = LightStateConverter.toBrightnessLightState((PercentType) command); + if (groupState != null) { + groupState.setTransitionTime(fadeTime); + } + } else if (command instanceof OnOffType) { + groupState = LightStateConverter.toOnOffLightState((OnOffType) command); + } else if (command instanceof IncreaseDecreaseType) { + groupState = convertBrightnessChangeToStateUpdate((IncreaseDecreaseType) command, group); + if (groupState != null) { + groupState.setTransitionTime(fadeTime); + } + } + if (groupState != null && lastSentColorTemp != null) { + // make sure that the light also has the latest color temp + // this might not have been yet set in the light, if it was off + groupState.setColorTemperature(lastSentColorTemp); + groupState.setTransitionTime(fadeTime); + } + break; + case CHANNEL_SWITCH: + if (command instanceof OnOffType) { + groupState = LightStateConverter.toOnOffLightState((OnOffType) command); + } + if (groupState != null && lastSentColorTemp != null) { + // make sure that the light also has the latest color temp + // this might not have been yet set in the light, if it was off + groupState.setColorTemperature(lastSentColorTemp); + groupState.setTransitionTime(fadeTime); + } + break; + default: + break; + } + if (groupState != null) { + // Cache values which we have sent + Integer tmpBrightness = groupState.getBrightness(); + if (tmpBrightness != null) { + lastSentBrightness = tmpBrightness; + } + Integer tmpColorTemp = groupState.getColorTemperature(); + if (tmpColorTemp != null) { + lastSentColorTemp = tmpColorTemp; + } + bridgeHandler.updateGroupState(group, groupState); + } else { + logger.debug("Command sent to an unknown channel id: {}:{}", getThing().getUID(), channel); + } + } + + private @Nullable StateUpdate convertColorTempChangeToStateUpdate(IncreaseDecreaseType command, FullGroup group) { + StateUpdate stateUpdate = null; + Integer currentColorTemp = getCurrentColorTemp(group.getState()); + if (currentColorTemp != null) { + int newColorTemp = LightStateConverter.toAdjustedColorTemp(command, currentColorTemp); + stateUpdate = new StateUpdate().setColorTemperature(newColorTemp); + } + return stateUpdate; + } + + private @Nullable Integer getCurrentColorTemp(@Nullable State groupState) { + Integer colorTemp = lastSentColorTemp; + if (colorTemp == null && groupState != null) { + colorTemp = groupState.getColorTemperature(); + } + return colorTemp; + } + + private @Nullable StateUpdate convertBrightnessChangeToStateUpdate(IncreaseDecreaseType command, FullGroup group) { + StateUpdate stateUpdate = null; + Integer currentBrightness = getCurrentBrightness(group.getState()); + if (currentBrightness != null) { + int newBrightness = LightStateConverter.toAdjustedBrightness(command, currentBrightness); + stateUpdate = createBrightnessStateUpdate(currentBrightness, newBrightness); + } + return stateUpdate; + } + + private @Nullable Integer getCurrentBrightness(@Nullable State groupState) { + Integer brightness = lastSentBrightness; + if (brightness == null && groupState != null) { + if (!groupState.isOn()) { + brightness = 0; + } else { + brightness = groupState.getBrightness(); + } + } + return brightness; + } + + private StateUpdate createBrightnessStateUpdate(int currentBrightness, int newBrightness) { + StateUpdate lightUpdate = new StateUpdate(); + if (newBrightness == 0) { + lightUpdate.turnOff(); + } else { + lightUpdate.setBrightness(newBrightness); + if (currentBrightness == 0) { + lightUpdate.turnOn(); + } + } + return lightUpdate; + } + + @Override + public void channelLinked(ChannelUID channelUID) { + HueClient handler = getHueClient(); + if (handler != null) { + FullGroup group = handler.getGroupById(groupId); + if (group != null) { + onGroupStateChanged(null, group); + } + } + } + + @Override + public void onGroupStateChanged(@Nullable HueBridge bridge, FullGroup group) { + logger.trace("onGroupStateChanged() was called for group {}", group.getId()); + + if (!group.getId().equals(groupId)) { + logger.trace("Received state change for another handler's group ({}). Will be ignored.", group.getId()); + return; + } + + lastSentColorTemp = null; + lastSentBrightness = null; + + updateStatus(ThingStatus.ONLINE); + + State state = group.getState(); + + logger.debug("onGroupStateChanged Group {}: on {} bri {} hue {} sat {} temp {} mode {} XY {}", group.getName(), + state.isOn(), state.getBrightness(), state.getHue(), state.getSaturation(), state.getColorTemperature(), + state.getColorMode(), state.getXY()); + + HSBType hsbType = LightStateConverter.toHSBType(state); + if (!state.isOn()) { + hsbType = new HSBType(hsbType.getHue(), hsbType.getSaturation(), new PercentType(0)); + } + updateState(CHANNEL_COLOR, hsbType); + + ColorMode colorMode = state.getColorMode(); + if (ColorMode.CT.equals(colorMode)) { + PercentType colorTempPercentType = LightStateConverter.toColorTemperaturePercentType(state); + updateState(CHANNEL_COLORTEMPERATURE, colorTempPercentType); + } else { + updateState(CHANNEL_COLORTEMPERATURE, UnDefType.NULL); + } + + PercentType brightnessPercentType = LightStateConverter.toBrightnessPercentType(state); + if (!state.isOn()) { + brightnessPercentType = new PercentType(0); + } + updateState(CHANNEL_BRIGHTNESS, brightnessPercentType); + + updateState(CHANNEL_SWITCH, state.isOn() ? OnOffType.ON : OnOffType.OFF); + } + + @Override + public void onGroupAdded(@Nullable HueBridge bridge, FullGroup group) { + if (group.getId().equals(groupId)) { + onGroupStateChanged(bridge, group); + } + } + + @Override + public void onGroupRemoved(@Nullable HueBridge bridge, FullGroup group) { + if (group.getId().equals(groupId)) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "@text/offline.group-removed"); + } + } + + @Override + public void onGroupGone(@Nullable HueBridge bridge, FullGroup group) { + if (group.getId().equals(groupId)) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.GONE, "@text/offline.group-removed"); + } + } +} diff --git a/bundles/org.openhab.binding.hue/src/main/resources/ESH-INF/i18n/hue.properties b/bundles/org.openhab.binding.hue/src/main/resources/ESH-INF/i18n/hue.properties index cffd5faff1b9b..c5e822dfe0e39 100644 --- a/bundles/org.openhab.binding.hue/src/main/resources/ESH-INF/i18n/hue.properties +++ b/bundles/org.openhab.binding.hue/src/main/resources/ESH-INF/i18n/hue.properties @@ -10,12 +10,15 @@ offline.conf-error-creation-username = Failed to create new user on Hue bridge. offline.bridge-connection-lost = Hue bridge connection lost. offline.conf-error-no-light-id = Light ID not available in configuration. offline.conf-error-no-sensor-id = Sensor ID not available in configuration. +offline.conf-error-no-group-id = Group ID not available in configuration. offline.conf-error-wrong-light-id = No light with given ID available on Hue bridge. offline.conf-error-wrong-sensor-id = No sensor with given ID available on Hue bridge. +offline.conf-error-wrong-group-id = No group with given ID available on Hue bridge. offline.light-not-reachable = Hue bridge reports light as not reachable. offline.sensor-not-reachable = Hue bridge reports sensor as not reachable. offline.light-removed = Hue bridge reports light as removed. offline.sensor-removed = Hue bridge reports sensor as removed. +offline.group-removed = Hue bridge reports group as removed. #LightActions actionLabel=send a light command with a custom fade time diff --git a/bundles/org.openhab.binding.hue/src/main/resources/ESH-INF/i18n/hue_fr.properties b/bundles/org.openhab.binding.hue/src/main/resources/ESH-INF/i18n/hue_fr.properties index f5b100821ee58..6fc186e11b94e 100644 --- a/bundles/org.openhab.binding.hue/src/main/resources/ESH-INF/i18n/hue_fr.properties +++ b/bundles/org.openhab.binding.hue/src/main/resources/ESH-INF/i18n/hue_fr.properties @@ -19,6 +19,8 @@ thing-type.hue.0210.label = Ampoule couleur thing-type.hue.0210.description = Une ampoule avec réglages de l'intensité lumineuse, de la couleur et de la température de couleur thing-type.hue.0220.label = Ampoule couleur thing-type.hue.0220.description = Une ampoule avec réglages de l'intensité lumineuse et de la température de couleur. +thing-type.hue.group.label = Groupe hue +thing-type.hue.group.description = Un groupe d'ampoules ou une pièce pouvant être allumé et éteint. # thing type configuration thing-type.config.hue.bridge.ipAddress.label = Adresse réseau @@ -41,6 +43,8 @@ thing-type.config.hue.0210.lightId.label = ID ampoule thing-type.config.hue.0210.lightId.description = L'identifiant d'ampoule identifie l'une des ampoules hue. thing-type.config.hue.0220.lightId.label = ID ampoule thing-type.config.hue.0220.lightId.description = L'identifiant d'ampoule identifie l'une des ampoules hue. +thing-type.config.hue.group.groupId.label = ID groupe +thing-type.config.hue.group.groupId.description = L'identifiant de groupe identifie l'un des groupes d'ampoules hue ou une pièce. # channel types channel-type.hue.color.label = Couleur @@ -70,5 +74,9 @@ offline.conf-error-press-pairing-button = Non autentifi offline.conf-error-creation-username = Echec de la créatiion du nouvel utilisateur sur le pont de connexion hue. offline.bridge-connection-lost = Perte de la connexion au pont hue. offline.conf-error-no-light-id = ID ampoule non renseigné dans la configuration. +offline.conf-error-no-group-id = ID groupe non renseigné dans la configuration. +offline.conf-error-wrong-light-id = Pas d''ampoule avec cet ID dans le pont de connexion hue. +offline.conf-error-wrong-group-id = Pas de groupe avec cet ID dans le pont de connexion hue. offline.light-not-reachable = Le pont de connexion hue signale l''ampoule comme inaccessible. offline.light-removed = Le pont de connexion hue signale l''ampoule comme supprimée. +offline.group-removed = Le pont de connexion hue signale le groupe comme supprimé. diff --git a/bundles/org.openhab.binding.hue/src/main/resources/ESH-INF/thing/Group.xml b/bundles/org.openhab.binding.hue/src/main/resources/ESH-INF/thing/Group.xml new file mode 100644 index 0000000000000..a19f10151f122 --- /dev/null +++ b/bundles/org.openhab.binding.hue/src/main/resources/ESH-INF/thing/Group.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + A group of lights or a room that could be switched on and off. + + + + + + + + + groupId + + + + + The group identifier identifies one certain hue group or room. + true + + + + Fade time in milliseconds for changing values + 400 + + + +