[hue] Add support for enabling automations (#16980)

Signed-off-by: Andrew Fiddian-Green <software@whitebear.ch>
Signed-off-by: Ciprian Pascu <contact@ciprianpascu.ro>
This commit is contained in:
Andrew Fiddian-Green 2024-08-19 20:14:29 +01:00 committed by Ciprian Pascu
parent 7b82d88a71
commit 63d3548711
15 changed files with 652 additions and 122 deletions

View File

@ -55,6 +55,17 @@ See [console command](#console-command-for-finding-resourceids)
The configuration of all things (as described above) is the same regardless of whether it is a device containing a light, a button, or (one or more) sensors, or whether it is a room or zone.
### Channels for Bridges
Bridge Things support the following channels:
| Channel ID | Item Type | Description |
|-------------------------------------------------|--------------------|---------------------------------------------|
| automation#11111111-2222-3333-4444-555555555555 | Switch | Enable / disable the respective automation. |
The Bridge dynamically creates `automation` channels corresponding to the automations in the Hue App;
the '11111111-2222-3333-4444-555555555555' is the unique id of the respective automation.
### Channels for Devices
Device things support some of the following channels:

View File

@ -17,6 +17,7 @@ import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.type.ChannelTypeUID;
/**
* The {@link HueBindingConstants} class defines common constants, which are
@ -200,4 +201,7 @@ public class HueBindingConstants {
Map.entry(CHANNEL_LAST_UPDATED, CHANNEL_2_LAST_UPDATED));
public static final String ALL_LIGHTS_KEY = "discovery.group.all-lights.label";
public static final String CHANNEL_GROUP_AUTOMATION = "automation";
public static final ChannelTypeUID CHANNEL_TYPE_AUTOMATION = new ChannelTypeUID(BINDING_ID, "automation-enable");
}

View File

@ -13,13 +13,14 @@
package org.openhab.binding.hue.internal.api.dto.clip2;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.hue.internal.api.dto.clip2.enums.ContentType;
import com.google.gson.annotations.SerializedName;
import com.google.gson.reflect.TypeToken;
/**
@ -32,7 +33,13 @@ public class Event {
public static final Type EVENT_LIST_TYPE = new TypeToken<List<Event>>() {
}.getType();
private @Nullable List<Resource> data = new ArrayList<>();
private @Nullable List<Resource> data;
private @Nullable @SerializedName("type") ContentType contentType; // content type of resources
public ContentType getContentType() {
ContentType contentType = this.contentType;
return Objects.nonNull(contentType) ? contentType : ContentType.ERROR;
}
public List<Resource> getData() {
List<Resource> data = this.data;

View File

@ -15,6 +15,7 @@ package org.openhab.binding.hue.internal.api.dto.clip2;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.hue.internal.api.dto.clip2.enums.Archetype;
import org.openhab.binding.hue.internal.api.dto.clip2.enums.CategoryType;
import com.google.gson.annotations.SerializedName;
@ -28,6 +29,7 @@ public class MetaData {
private @Nullable String archetype;
private @Nullable String name;
private @Nullable @SerializedName("control_id") Integer controlId;
private @Nullable String category;
public Archetype getArchetype() {
return Archetype.of(archetype);
@ -37,6 +39,10 @@ public class MetaData {
return name;
}
public CategoryType getCategory() {
return CategoryType.of(category);
}
public int getControlId() {
Integer controlId = this.controlId;
return controlId != null ? controlId.intValue() : 0;

View File

@ -28,7 +28,9 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.hue.internal.api.dto.clip2.enums.ActionType;
import org.openhab.binding.hue.internal.api.dto.clip2.enums.ButtonEventType;
import org.openhab.binding.hue.internal.api.dto.clip2.enums.CategoryType;
import org.openhab.binding.hue.internal.api.dto.clip2.enums.ContactStateType;
import org.openhab.binding.hue.internal.api.dto.clip2.enums.ContentType;
import org.openhab.binding.hue.internal.api.dto.clip2.enums.EffectType;
import org.openhab.binding.hue.internal.api.dto.clip2.enums.ResourceType;
import org.openhab.binding.hue.internal.api.dto.clip2.enums.SceneRecallAction;
@ -55,6 +57,7 @@ import org.openhab.core.util.ColorUtil.Gamut;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive;
import com.google.gson.annotations.SerializedName;
/**
@ -74,8 +77,16 @@ public class Resource {
* values have changed. A sparse resource does not contain the full state of the resource. And the absence of any
* field from such a resource does not indicate that the field value is UNDEF, but rather that the value is the same
* as what it was previously set to by the last non-sparse resource.
* <p>
* The following content types are defined:
*
* <li><b>ADD</b> resource being added; contains (assumed) all fields</li>
* <li><b>DELETE</b> resource being deleted; contains id and type only</li>
* <li><b>UPDATE</b> resource being updated; contains id, type and changed fields</li>
* <li><b>ERROR</b> resource with error; contents unknown</li>
* <li><b>FULL_STATE</b> existing resource being downloaded; contains all fields</li>
*/
private transient boolean hasSparseData;
private transient ContentType contentType;
private @Nullable String type;
private @Nullable String id;
@ -107,7 +118,15 @@ public class Resource {
private @Nullable Dynamics dynamics;
private @Nullable @SerializedName("contact_report") ContactReport contactReport;
private @Nullable @SerializedName("tamper_reports") List<TamperReport> tamperReports;
private @Nullable String state;
private @Nullable JsonElement state;
private @Nullable @SerializedName("script_id") String scriptId;
/**
* Constructor
*/
public Resource() {
contentType = ContentType.FULL_STATE;
}
/**
* Constructor
@ -115,6 +134,7 @@ public class Resource {
* @param resourceType
*/
public Resource(@Nullable ResourceType resourceType) {
this();
if (Objects.nonNull(resourceType)) {
setType(resourceType);
}
@ -343,6 +363,14 @@ public class Resource {
return color;
}
/**
* Return the resource's metadata category.
*/
public CategoryType getCategory() {
MetaData metaData = getMetaData();
return Objects.nonNull(metaData) ? metaData.getCategory() : CategoryType.NULL;
}
/**
* Return an HSB where the HS part is derived from the color xy JSON element (only), so the B part is 100%
*
@ -375,6 +403,10 @@ public class Resource {
: OpenClosedType.OPEN;
}
public ContentType getContentType() {
return contentType;
}
public int getControlId() {
MetaData metadata = this.metadata;
return Objects.nonNull(metadata) ? metadata.getControlId() : 0;
@ -648,6 +680,13 @@ public class Resource {
return Optional.empty();
}
/**
* Return the scriptId if any.
*/
public @Nullable String getScriptId() {
return scriptId;
}
/**
* If the getSceneActive() optional result is empty return 'UnDefType.NULL'. Otherwise if the optional result is
* present and 'true' (i.e. the scene is active) return the scene name. Or finally (the optional result is present
@ -661,13 +700,14 @@ public class Resource {
/**
* Check if the smart scene resource contains a 'state' element. If such an element is present, returns a Boolean
* Optional whose value depends on the value of that element, or an empty Optional if it is not.
* Optional whose value depends on the value of that element, or an empty Optional if it is not. Note that in some
* resource types the 'state' element is not a String primitive.
*
* @return true, false, or empty.
*/
public Optional<Boolean> getSmartSceneActive() {
if (ResourceType.SMART_SCENE == getType()) {
String state = this.state;
if (ResourceType.SMART_SCENE == getType() && (state instanceof JsonPrimitive statePrimitive)) {
String state = statePrimitive.getAsString();
if (Objects.nonNull(state)) {
return Optional.of(SmartSceneState.ACTIVE == SmartSceneState.of(state));
}
@ -785,17 +825,12 @@ public class Resource {
}
public boolean hasFullState() {
return !hasSparseData;
return ContentType.FULL_STATE == contentType;
}
/**
* Mark that the resource has sparse data.
*
* @return this instance.
*/
public Resource markAsSparse() {
hasSparseData = true;
return this;
public boolean hasName() {
MetaData metaData = getMetaData();
return Objects.nonNull(metaData) && Objects.nonNull(metaData.getName());
}
public Resource setAlerts(Alerts alert) {
@ -818,6 +853,11 @@ public class Resource {
return this;
}
public Resource setContentType(ContentType contentType) {
this.contentType = contentType;
return this;
}
public Resource setDimming(@Nullable Dimming dimming) {
this.dimming = dimming;
return this;

View File

@ -0,0 +1,41 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.hue.internal.api.dto.clip2.enums;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* Enum for 'category' fields.
*
* @author Andrew Fiddian-Green - Initial contribution
*/
@NonNullByDefault
public enum CategoryType {
ACCESSORY,
AUTOMATION,
ENTERTAINMENT,
NULL,
UNDEF;
public static CategoryType of(@Nullable String value) {
if (value != null) {
try {
return valueOf(value.toUpperCase());
} catch (IllegalArgumentException e) {
return UNDEF;
}
}
return NULL;
}
}

View File

@ -0,0 +1,36 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.hue.internal.api.dto.clip2.enums;
import org.eclipse.jdt.annotation.NonNullByDefault;
import com.google.gson.annotations.SerializedName;
/**
* Enum for content type of Resource instances
*
* @author Andrew Fiddian-Green - Initial contribution
*/
@NonNullByDefault
public enum ContentType {
@SerializedName("add") // resource being added; contains (maybe) all fields
ADD,
@SerializedName("delete") // resource being deleted; contains id and type only
DELETE,
@SerializedName("update") // resource being updated; contains id, type and updated fields
UPDATE,
@SerializedName("error") // resource error event
ERROR,
// existing resource being downloaded; contains all fields; excluded from (de-)serialization
FULL_STATE
}

View File

@ -921,12 +921,15 @@ public class Clip2Bridge implements Closeable {
return;
}
List<Resource> resources = new ArrayList<>();
events.forEach(event -> resources.addAll(event.getData()));
events.forEach(event -> {
List<Resource> eventResources = event.getData();
eventResources.forEach(resource -> resource.setContentType(event.getContentType()));
resources.addAll(eventResources);
});
if (resources.isEmpty()) {
LOGGER.debug("onEventData() resource list is empty");
return;
}
resources.forEach(resource -> resource.markAsSparse());
bridgeHandler.onResourcesEvent(resources);
}

View File

@ -27,6 +27,8 @@ import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
@ -36,6 +38,7 @@ import org.openhab.binding.hue.internal.api.dto.clip2.Resource;
import org.openhab.binding.hue.internal.api.dto.clip2.ResourceReference;
import org.openhab.binding.hue.internal.api.dto.clip2.Resources;
import org.openhab.binding.hue.internal.api.dto.clip2.enums.Archetype;
import org.openhab.binding.hue.internal.api.dto.clip2.enums.CategoryType;
import org.openhab.binding.hue.internal.api.dto.clip2.enums.ResourceType;
import org.openhab.binding.hue.internal.api.dto.clip2.helper.Setters;
import org.openhab.binding.hue.internal.config.Clip2BridgeConfig;
@ -50,7 +53,10 @@ import org.openhab.core.i18n.LocaleProvider;
import org.openhab.core.i18n.TranslationProvider;
import org.openhab.core.io.net.http.HttpClientFactory;
import org.openhab.core.io.net.http.TlsTrustManagerProvider;
import org.openhab.core.library.CoreItemFactory;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.ChannelGroupUID;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingRegistry;
@ -62,6 +68,7 @@ import org.openhab.core.thing.binding.BaseBridgeHandler;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerService;
import org.openhab.core.thing.binding.builder.BridgeBuilder;
import org.openhab.core.thing.binding.builder.ChannelBuilder;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.osgi.framework.Bundle;
@ -93,6 +100,11 @@ public class Clip2BridgeHandler extends BaseBridgeHandler {
private static final ResourceReference BRIDGE_HOME = new ResourceReference().setType(ResourceType.BRIDGE_HOME);
private static final ResourceReference SCENE = new ResourceReference().setType(ResourceType.SCENE);
private static final ResourceReference SMART_SCENE = new ResourceReference().setType(ResourceType.SMART_SCENE);
private static final ResourceReference SCRIPT = new ResourceReference().setType(ResourceType.BEHAVIOR_SCRIPT);
private static final ResourceReference BEHAVIOR = new ResourceReference().setType(ResourceType.BEHAVIOR_INSTANCE);
private static final String AUTOMATION_CHANNEL_LABEL_KEY = "dynamic-channel.automation-enable.label";
private static final String AUTOMATION_CHANNEL_DESCRIPTION_KEY = "dynamic-channel.automation-enable.description";
/**
* List of resource references that need to be mass down loaded.
@ -107,11 +119,15 @@ public class Clip2BridgeHandler extends BaseBridgeHandler {
private final Bundle bundle;
private final LocaleProvider localeProvider;
private final TranslationProvider translationProvider;
private final Map<String, Resource> automationsCache = new ConcurrentHashMap<>();;
private final Set<String> automationScriptIds = ConcurrentHashMap.newKeySet();
private final ChannelGroupUID automationChannelGroupUID;
private @Nullable Clip2Bridge clip2Bridge;
private @Nullable ServiceRegistration<?> trustManagerRegistration;
private @Nullable Clip2ThingDiscoveryService discoveryService;
private @Nullable Future<?> updateAutomationChannelsTask;
private @Nullable Future<?> checkConnectionTask;
private @Nullable Future<?> updateOnlineStateTask;
private @Nullable ScheduledFuture<?> scheduledUpdateTask;
@ -129,6 +145,7 @@ public class Clip2BridgeHandler extends BaseBridgeHandler {
this.bundle = FrameworkUtil.getBundle(getClass());
this.localeProvider = localeProvider;
this.translationProvider = translationProvider;
this.automationChannelGroupUID = new ChannelGroupUID(thing.getUID(), CHANNEL_GROUP_AUTOMATION);
}
/**
@ -265,9 +282,11 @@ public class Clip2BridgeHandler extends BaseBridgeHandler {
logger.debug("disposeAssets() {}", this);
synchronized (this) {
assetsLoaded = false;
cancelTask(updateAutomationChannelsTask, true);
cancelTask(checkConnectionTask, true);
cancelTask(updateOnlineStateTask, true);
cancelTask(scheduledUpdateTask, true);
updateAutomationChannelsTask = null;
checkConnectionTask = null;
updateOnlineStateTask = null;
scheduledUpdateTask = null;
@ -418,10 +437,25 @@ public class Clip2BridgeHandler extends BaseBridgeHandler {
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
if (RefreshType.REFRESH.equals(command)) {
return;
if (CHANNEL_GROUP_AUTOMATION.equals(channelUID.getGroupId())) {
try {
if (RefreshType.REFRESH.equals(command)) {
updateAutomationChannelsNow();
return;
} else {
Resources resources = getClip2Bridge().putResource(new Resource(ResourceType.BEHAVIOR_INSTANCE)
.setId(channelUID.getIdWithoutGroup()).setEnabled(command));
if (resources.hasErrors()) {
logger.warn("handleCommand({}, {}) succeeded with errors: {}", channelUID, command,
String.join("; ", resources.getErrors()));
}
}
} catch (ApiException | AssetNotLoadedException e) {
logger.warn("handleCommand({}, {}) error {}", channelUID, command, e.getMessage(),
logger.isDebugEnabled() ? e : null);
} catch (InterruptedException e) {
}
}
logger.warn("Bridge thing '{}' has no channels, only REFRESH command supported.", thing.getUID());
}
@Override
@ -533,6 +567,9 @@ public class Clip2BridgeHandler extends BaseBridgeHandler {
if (numberOfResources != resources.size()) {
logger.debug("onResourcesEventTask() merged to {} resources", resources.size());
}
if (onResources(resources)) {
updateAutomationChannelsNow();
}
getThing().getThings().forEach(thing -> {
if (thing.getHandler() instanceof Clip2ThingHandler clip2ThingHandler) {
clip2ThingHandler.onResources(resources);
@ -598,6 +635,8 @@ public class Clip2BridgeHandler extends BaseBridgeHandler {
logger.debug("updateOnlineState()");
connectRetriesRemaining = RECONNECT_MAX_TRIES;
updateStatus(ThingStatus.ONLINE);
loadAutomationScriptIds();
updateAutomationChannelsNow();
updateThingsScheduled(500);
Clip2ThingDiscoveryService discoveryService = this.discoveryService;
if (Objects.nonNull(discoveryService)) {
@ -775,4 +814,124 @@ public class Clip2BridgeHandler extends BaseBridgeHandler {
scheduledUpdateTask = scheduler.schedule(() -> updateThingsNow(), delayMilliSeconds, TimeUnit.MILLISECONDS);
}
}
/**
* Load the set of automation script ids.
*/
private void loadAutomationScriptIds() {
try {
synchronized (automationScriptIds) {
automationScriptIds.clear();
automationScriptIds.addAll(getClip2Bridge().getResources(SCRIPT).getResources().stream()
.filter(r -> CategoryType.AUTOMATION == r.getCategory()).map(r -> r.getId())
.collect(Collectors.toSet()));
}
} catch (ApiException | AssetNotLoadedException e) {
logger.warn("loadAutomationScriptIds() unexpected exception {}", e.getMessage(),
logger.isDebugEnabled() ? e : null);
} catch (InterruptedException e) {
}
}
/**
* Create resp. update the automation channels
*/
private void updateAutomationChannels() {
List<Resource> automations;
try {
automations = getClip2Bridge().getResources(BEHAVIOR).getResources().stream()
.filter(r -> automationScriptIds.contains(r.getScriptId())).toList();
} catch (ApiException | AssetNotLoadedException e) {
logger.warn("Unexpected exception '{}' while updating channels.", e.getMessage(),
logger.isDebugEnabled() ? e : null);
return;
} catch (InterruptedException e) {
return;
}
if (automations.size() != automationsCache.size() || automations.stream().anyMatch(automation -> {
Resource cachedAutomation = automationsCache.get(automation.getId());
return Objects.isNull(cachedAutomation) || !automation.getName().equals(cachedAutomation.getName());
})) {
synchronized (automationsCache) {
automationsCache.clear();
automationsCache.putAll(automations.stream().collect(Collectors.toMap(a -> a.getId(), a -> a)));
}
Stream<Channel> newChannels = automations.stream().map(a -> createAutomationChannel(a));
Stream<Channel> oldchannels = thing.getChannels().stream()
.filter(c -> !CHANNEL_TYPE_AUTOMATION.equals(c.getChannelTypeUID()));
updateThing(editThing().withChannels(Stream.concat(oldchannels, newChannels).toList()).build());
onResources(automations);
logger.debug("Bridge created {} automation channels", automations.size());
}
}
/**
* Start a task to update the automation channels
*/
private void updateAutomationChannelsNow() {
cancelTask(updateAutomationChannelsTask, false);
updateAutomationChannelsTask = scheduler.submit(() -> updateAutomationChannels());
}
/**
* Create an automation channel from an automation resource
*/
private Channel createAutomationChannel(Resource automation) {
String label = Objects.requireNonNullElse(translationProvider.getText(bundle, AUTOMATION_CHANNEL_LABEL_KEY,
AUTOMATION_CHANNEL_LABEL_KEY, localeProvider.getLocale(), automation.getName()),
AUTOMATION_CHANNEL_LABEL_KEY);
String description = Objects.requireNonNullElse(
translationProvider.getText(bundle, AUTOMATION_CHANNEL_DESCRIPTION_KEY,
AUTOMATION_CHANNEL_DESCRIPTION_KEY, localeProvider.getLocale(), automation.getName()),
AUTOMATION_CHANNEL_DESCRIPTION_KEY);
return ChannelBuilder
.create(new ChannelUID(automationChannelGroupUID, automation.getId()), CoreItemFactory.SWITCH)
.withLabel(label).withDescription(description).withType(CHANNEL_TYPE_AUTOMATION).build();
}
/**
* Process event resources list
*
* @return true if the automation channels require updating
*/
public boolean onResources(List<Resource> resources) {
boolean requireUpdateChannels = false;
for (Resource resource : resources) {
if (ResourceType.BEHAVIOR_INSTANCE != resource.getType()) {
continue;
}
String resourceId = resource.getId();
switch (resource.getContentType()) {
case ADD:
requireUpdateChannels |= automationScriptIds.contains(resource.getScriptId());
break;
case DELETE:
requireUpdateChannels |= automationsCache.containsKey(resourceId);
break;
case UPDATE:
case FULL_STATE:
Resource cachedAutomation = automationsCache.get(resourceId);
if (Objects.isNull(cachedAutomation)) {
requireUpdateChannels |= automationScriptIds.contains(resource.getScriptId());
} else {
if (resource.hasName() && !resource.getName().equals(cachedAutomation.getName())) {
requireUpdateChannels = true;
} else if (Objects.nonNull(resource.getEnabled())) {
updateState(new ChannelUID(automationChannelGroupUID, resourceId),
resource.getEnabledState());
}
}
break;
default:
}
}
return requireUpdateChannels;
}
}

View File

@ -130,6 +130,10 @@ thing-type.config.hue.room.resourceId.description = Unique Resource ID of the ro
thing-type.config.hue.zone.resourceId.label = Resource ID
thing-type.config.hue.zone.resourceId.description = Unique Resource ID of the zone in the Hue bridge
# channel group types
channel-group-type.hue.automation.label = Automations
# channel types
channel-type.hue.advanced-brightness.label = Dimming Only
@ -144,6 +148,7 @@ channel-type.hue.alert.description = The alert channel allows a temporary change
channel-type.hue.alert.state.option.NONE = None
channel-type.hue.alert.state.option.SELECT = Alert
channel-type.hue.alert.state.option.LSELECT = Long Alert
channel-type.hue.automation-enable.label = Enable
channel-type.hue.button-last-event.label = Button Last Event
channel-type.hue.button-last-event.description = Numeric code (e.g. 1003) representing the last push button event.
channel-type.hue.dark.label = Dark
@ -292,3 +297,8 @@ dynamics.command.label = Target Command
dynamics.command.description = The target command state for the light(s) to transition to.
dynamics.duration.label = Duration
dynamics.duration.description = The dynamic transition duration in ms.
# dynamic channels
dynamic-channel.automation-enable.label = Enable ''{0}''
dynamic-channel.automation-enable.description = Enable the ''{0}'' automation

View File

@ -67,6 +67,10 @@
<label>Hue API v2 Bridge</label>
<description>The Hue Bridge represents a Philips Hue Bridge supporting API v2.</description>
<channel-groups>
<channel-group id="automation" typeId="automation"/>
</channel-groups>
<representation-property>serialNumber</representation-property>
<config-description>

View File

@ -286,4 +286,14 @@
<category>Siren</category>
</channel-type>
<channel-type id="automation-enable">
<item-type>Switch</item-type>
<label>Enable</label>
<category>Switch</category>
</channel-type>
<channel-group-type id="automation">
<label>Automations</label>
</channel-group-type>
</thing:thing-descriptions>

View File

@ -905,4 +905,14 @@ class Clip2DtoTest {
assertTrue(resultEffect instanceof TimedEffects);
assertEquals(Duration.ofMillis(44), ((TimedEffects) resultEffect).getDuration());
}
@Test
void testBehaviorInstance() {
String json = load(ResourceType.BEHAVIOR_INSTANCE.name().toLowerCase());
Resources resources = GSON.fromJson(json, Resources.class);
assertNotNull(resources);
List<Resource> list = resources.getResources();
assertNotNull(list);
assertEquals(2, list.size());
}
}

View File

@ -30,13 +30,14 @@ import org.openhab.binding.hue.internal.api.dto.clip2.Dimming;
import org.openhab.binding.hue.internal.api.dto.clip2.Effects;
import org.openhab.binding.hue.internal.api.dto.clip2.OnState;
import org.openhab.binding.hue.internal.api.dto.clip2.Resource;
import org.openhab.binding.hue.internal.api.dto.clip2.enums.ContentType;
import org.openhab.binding.hue.internal.api.dto.clip2.enums.ResourceType;
import org.openhab.binding.hue.internal.api.dto.clip2.helper.Setters;
import org.openhab.binding.hue.internal.exceptions.DTOPresentButEmptyException;
/**
* Tests for {@link Setters}.
*
*
* @author Jacob Laursen - Initial contribution
*/
@NonNullByDefault
@ -51,7 +52,7 @@ public class SettersTest {
*
* Expected output:
* - Resource 1: type=light/grouped_light, sparse, id=1, on=on, dimming=50
*
*
* @throws DTOPresentButEmptyException
*/
@ParameterizedTest
@ -100,7 +101,7 @@ public class SettersTest {
*
* Expected output:
* - Resource 1: type=light, sparse, id=1, dimming=50
*
*
* @throws DTOPresentButEmptyException
*/
@Test
@ -137,7 +138,7 @@ public class SettersTest {
* Expected output:
* - Resource 1: type=light, sparse, id=1, on=on, dimming=50
* - Resource 2: type=light, sparse, id=1, effect=xxx
*
*
* @throws DTOPresentButEmptyException
*/
@Test
@ -185,7 +186,7 @@ public class SettersTest {
* Expected output:
* - Resource 1: type=light, sparse, id=1, on=on
* - Resource 2: type=light, sparse, id=2, dimming=50
*
*
* @throws DTOPresentButEmptyException
*/
@Test
@ -228,7 +229,7 @@ public class SettersTest {
*
* Expected output:
* - Exception thrown, full state is not supported/expected.
*
*
* @throws DTOPresentButEmptyException
*/
@Test
@ -254,7 +255,7 @@ public class SettersTest {
* Expected output:
* - Resource 1: type=light, sparse, id=1, on=on
* - Resource 2: type=light, sparse, id=1, color temperature=370 mirek
*
*
* @throws DTOPresentButEmptyException
*/
@Test
@ -301,7 +302,7 @@ public class SettersTest {
* Expected output:
* - Resource 1: type=light, sparse, id=1, on=on, dimming=50
* - Resource 2: type=light, sparse, id=1, color temperature=370 mirek
*
*
* @throws DTOPresentButEmptyException
*/
@Test
@ -352,7 +353,7 @@ public class SettersTest {
*
* Expected output:
* - Resource 1: type=light, sparse, id=1, on=on, color temperature=370
*
*
* @throws DTOPresentButEmptyException
*/
@Test
@ -389,7 +390,7 @@ public class SettersTest {
*
* Expected output:
* - Resource 1: type=motion, sparse, id=1
*
*
* @throws DTOPresentButEmptyException
*/
@Test
@ -431,7 +432,7 @@ public class SettersTest {
private Resource createResource(ResourceType resourceType, String id) {
Resource resource = new Resource(resourceType);
resource.setId(id);
resource.markAsSparse();
resource.setContentType(ContentType.UPDATE);
return resource;
}

View File

@ -1,91 +1,279 @@
{
"errors": [],
"data": [
{
"configuration": {
"what": [
{
"group": {
"rid": "b8d28681-eba1-4156-85e2-96c9c5179fba",
"rtype": "room"
},
"recall": {
"rid": "11ac9c82-d031-43a6-a8d5-b6efdee72fe6",
"rtype": "scene"
}
},
{
"group": {
"rid": "8b529073-36dd-409b-8006-80df304048ea",
"rtype": "room"
},
"recall": {
"rid": "8b65c749-3ad8-435e-a7ed-94e3cc99e9d7",
"rtype": "scene"
}
}
],
"when_constrained": {
"type": "nighttime"
},
"where": [
{
"group": {
"rid": "b8d28681-eba1-4156-85e2-96c9c5179fba",
"rtype": "room"
}
},
{
"group": {
"rid": "8b529073-36dd-409b-8006-80df304048ea",
"rtype": "room"
}
}
]
},
"dependees": [
{
"level": "critical",
"target": {
"rid": "b8d28681-eba1-4156-85e2-96c9c5179fba",
"rtype": "room"
},
"type": "ResourceDependee"
},
{
"level": "critical",
"target": {
"rid": "11ac9c82-d031-43a6-a8d5-b6efdee72fe6",
"rtype": "scene"
},
"type": "ResourceDependee"
},
{
"level": "critical",
"target": {
"rid": "8b529073-36dd-409b-8006-80df304048ea",
"rtype": "room"
},
"type": "ResourceDependee"
},
{
"level": "critical",
"target": {
"rid": "8b65c749-3ad8-435e-a7ed-94e3cc99e9d7",
"rtype": "scene"
},
"type": "ResourceDependee"
}
],
"enabled": true,
"id": "8d0ffbee-e24e-4d3e-b91a-5adc9ef5d49c",
"last_error": "",
"metadata": {
"name": "Coming home"
},
"script_id": "fd60fcd1-4809-4813-b510-4a18856a595c",
"status": "running",
"type": "behavior_instance"
}
]
}
"errors": [
],
"data": [
{
"id": "042284f9-eeae-4f1e-9560-cc73750c7d28",
"type": "behavior_instance",
"script_id": "67d9395b-4403-42cc-b5f0-740b699d67c6",
"enabled": true,
"state": {
"model_id": "RWL021",
"source_type": "device"
},
"configuration": {
"buttons": {
"6615f1f1-f3f1-4a05-b8f7-581097458e34": {
"on_repeat": {
"action": "dim_down"
}
},
"91ba8839-2bac-4175-9f8c-ed192842d549": {
"on_long_press": {
"action": "do_nothing"
},
"on_short_release": {
"time_based_extended": {
"slots": [
{
"actions": [
{
"action": {
"recall": {
"rid": "f021deb5-5104-4752-aab3-2849f84da690",
"rtype": "scene"
}
}
}
],
"start_time": {
"hour": 7,
"minute": 0
}
},
{
"actions": [
{
"action": {
"recall": {
"rid": "4ddd4f8b-428c-4089-a9a1-c27df5259b9a",
"rtype": "scene"
}
}
}
],
"start_time": {
"hour": 20,
"minute": 0
}
},
{
"actions": [
{
"action": {
"recall": {
"rid": "af0c88c4-9dae-4767-8475-a3cca906390d",
"rtype": "scene"
}
}
}
],
"start_time": {
"hour": 23,
"minute": 0
}
}
],
"with_off": {
"enabled": false
}
}
}
},
"b0d5a0af-31fd-4189-9150-c551ff9033d7": {
"on_long_press": {
"action": "do_nothing"
},
"on_short_release": {
"action": "all_off"
}
},
"f95addfc-2f7c-453f-924d-ba496e07e5f9": {
"on_repeat": {
"action": "dim_up"
}
}
},
"device": {
"rid": "e130feac-3a5c-452e-a97d-5bca470783b3",
"rtype": "device"
},
"model_id": "RWL021",
"where": [
{
"group": {
"rid": "bdc282b3-750d-45dd-b6c4-12a2927d8951",
"rtype": "zone"
}
}
]
},
"dependees": [
{
"target": {
"rid": "e130feac-3a5c-452e-a97d-5bca470783b3",
"rtype": "device"
},
"level": "critical",
"type": "ResourceDependee"
},
{
"target": {
"rid": "bdc282b3-750d-45dd-b6c4-12a2927d8951",
"rtype": "zone"
},
"level": "critical",
"type": "ResourceDependee"
},
{
"target": {
"rid": "f021deb5-5104-4752-aab3-2849f84da690",
"rtype": "scene"
},
"level": "critical",
"type": "ResourceDependee"
},
{
"target": {
"rid": "4ddd4f8b-428c-4089-a9a1-c27df5259b9a",
"rtype": "scene"
},
"level": "critical",
"type": "ResourceDependee"
},
{
"target": {
"rid": "af0c88c4-9dae-4767-8475-a3cca906390d",
"rtype": "scene"
},
"level": "critical",
"type": "ResourceDependee"
},
{
"target": {
"rid": "91ba8839-2bac-4175-9f8c-ed192842d549",
"rtype": "button"
},
"level": "critical",
"type": "ResourceDependee"
},
{
"target": {
"rid": "f95addfc-2f7c-453f-924d-ba496e07e5f9",
"rtype": "button"
},
"level": "critical",
"type": "ResourceDependee"
},
{
"target": {
"rid": "6615f1f1-f3f1-4a05-b8f7-581097458e34",
"rtype": "button"
},
"level": "critical",
"type": "ResourceDependee"
},
{
"target": {
"rid": "b0d5a0af-31fd-4189-9150-c551ff9033d7",
"rtype": "button"
},
"level": "critical",
"type": "ResourceDependee"
}
],
"status": "running",
"last_error": "",
"metadata": {
"name": "Worktops Dimmer Pad Right"
},
"migrated_from": "/resourcelinks/5338"
},
{
"configuration": {
"what": [
{
"group": {
"rid": "b8d28681-eba1-4156-85e2-96c9c5179fba",
"rtype": "room"
},
"recall": {
"rid": "11ac9c82-d031-43a6-a8d5-b6efdee72fe6",
"rtype": "scene"
}
},
{
"group": {
"rid": "8b529073-36dd-409b-8006-80df304048ea",
"rtype": "room"
},
"recall": {
"rid": "8b65c749-3ad8-435e-a7ed-94e3cc99e9d7",
"rtype": "scene"
}
}
],
"when_constrained": {
"type": "nighttime"
},
"where": [
{
"group": {
"rid": "b8d28681-eba1-4156-85e2-96c9c5179fba",
"rtype": "room"
}
},
{
"group": {
"rid": "8b529073-36dd-409b-8006-80df304048ea",
"rtype": "room"
}
}
]
},
"dependees": [
{
"level": "critical",
"target": {
"rid": "b8d28681-eba1-4156-85e2-96c9c5179fba",
"rtype": "room"
},
"type": "ResourceDependee"
},
{
"level": "critical",
"target": {
"rid": "11ac9c82-d031-43a6-a8d5-b6efdee72fe6",
"rtype": "scene"
},
"type": "ResourceDependee"
},
{
"level": "critical",
"target": {
"rid": "8b529073-36dd-409b-8006-80df304048ea",
"rtype": "room"
},
"type": "ResourceDependee"
},
{
"level": "critical",
"target": {
"rid": "8b65c749-3ad8-435e-a7ed-94e3cc99e9d7",
"rtype": "scene"
},
"type": "ResourceDependee"
}
],
"enabled": true,
"id": "8d0ffbee-e24e-4d3e-b91a-5adc9ef5d49c",
"last_error": "",
"metadata": {
"name": "Coming home"
},
"script_id": "fd60fcd1-4809-4813-b510-4a18856a595c",
"status": "running",
"type": "behavior_instance"
}
]
}