mirror of
https://github.com/openhab/openhab-addons.git
synced 2025-01-10 15:11:59 +01:00
[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:
parent
7b82d88a71
commit
63d3548711
@ -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:
|
||||
|
@ -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");
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user