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:
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
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");
}
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;
/**
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;
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;
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);
return name;
}
+ public CategoryType getCategory() {
+ return CategoryType.of(category);
+ }
+
public int getControlId() {
Integer controlId = this.controlId;
return controlId != null ? controlId.intValue() : 0;
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;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
+import com.google.gson.JsonPrimitive;
import com.google.gson.annotations.SerializedName;
/**
* 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;
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
* @param resourceType
*/
public Resource(@Nullable ResourceType resourceType) {
+ this();
if (Objects.nonNull(resourceType)) {
setType(resourceType);
}
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%
*
: OpenClosedType.OPEN;
}
+ public ContentType getContentType() {
+ return contentType;
+ }
+
public int getControlId() {
MetaData metadata = this.metadata;
return Objects.nonNull(metadata) ? metadata.getControlId() : 0;
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
/**
* 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));
}
}
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) {
return this;
}
+ public Resource setContentType(ContentType contentType) {
+ this.contentType = contentType;
+ return this;
+ }
+
public Resource setDimming(@Nullable Dimming dimming) {
this.dimming = dimming;
return this;
--- /dev/null
+/**
+ * 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;
+ }
+}
--- /dev/null
+/**
+ * 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
+}
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);
}
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;
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;
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;
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;
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.
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;
this.bundle = FrameworkUtil.getBundle(getClass());
this.localeProvider = localeProvider;
this.translationProvider = translationProvider;
+ this.automationChannelGroupUID = new ChannelGroupUID(thing.getUID(), CHANNEL_GROUP_AUTOMATION);
}
/**
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;
@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
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);
logger.debug("updateOnlineState()");
connectRetriesRemaining = RECONNECT_MAX_TRIES;
updateStatus(ThingStatus.ONLINE);
+ loadAutomationScriptIds();
+ updateAutomationChannelsNow();
updateThingsScheduled(500);
Clip2ThingDiscoveryService discoveryService = this.discoveryService;
if (Objects.nonNull(discoveryService)) {
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;
+ }
}
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
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
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
<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>
<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>
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());
+ }
}
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
*
* Expected output:
* - Resource 1: type=light/grouped_light, sparse, id=1, on=on, dimming=50
- *
+ *
* @throws DTOPresentButEmptyException
*/
@ParameterizedTest
*
* Expected output:
* - Resource 1: type=light, sparse, id=1, dimming=50
- *
+ *
* @throws DTOPresentButEmptyException
*/
@Test
* 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
* Expected output:
* - Resource 1: type=light, sparse, id=1, on=on
* - Resource 2: type=light, sparse, id=2, dimming=50
- *
+ *
* @throws DTOPresentButEmptyException
*/
@Test
*
* Expected output:
* - Exception thrown, full state is not supported/expected.
- *
+ *
* @throws DTOPresentButEmptyException
*/
@Test
* 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
* 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
*
* Expected output:
* - Resource 1: type=light, sparse, id=1, on=on, color temperature=370
- *
+ *
* @throws DTOPresentButEmptyException
*/
@Test
*
* Expected output:
* - Resource 1: type=motion, sparse, id=1
- *
+ *
* @throws DTOPresentButEmptyException
*/
@Test
private Resource createResource(ResourceType resourceType, String id) {
Resource resource = new Resource(resourceType);
resource.setId(id);
- resource.markAsSparse();
+ resource.setContentType(ContentType.UPDATE);
return resource;
}
{
- "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"
- }
- ]
-}
\ No newline at end of file
+ "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"
+ }
+ ]
+}