Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[deconz] Add actions for device joining and scenes #14134

Closed
wants to merge 11 commits into from
Closed
46 changes: 46 additions & 0 deletions bundles/org.openhab.binding.deconz/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,26 @@ Both will be added during runtime if supported by the switch.
| GESTURE_ROTATE_CLOCKWISE | 7 |
| GESTURE_ROTATE_COUNTER_CLOCKWISE | 8 |

## Thing Actions

Thing actions can be used to manage the network and its content.

The `deconz` thing supports a thing action to allow new devices to join the network:

| Action name | Input Value | Return Value | Description |
|------------------------|----------------------|--------------|----------------------------------------------------------------------------------------------------------------|
| `permitJoin(duration)` | `duration` (Integer) | - | Allows new devices to join for `duration` seconds. Allowed values are 1-240, default is 120 if no value given. |

The `lightgroup` thing supports thing actions for managing scenes:

| Action name | Input Value | Return Value | Description |
|---------------------|----------------------|--------------|------------------------------------------------------------------------------------------|
| `createScene(name)` | `name` (String) | `newSceneId` | Creates a new scene with the name `name` and returns the new scene's id (if successful). |
| `deleteScene(id)` | `sceneId` (Integer) | - | Deletes the scene with the given id. |
| `storeScene(id)` | `sceneId` (Integer) | - | Store the current group's state as scene with the given id. |

The return value refers to a key of the given name within the returned Map. See [example](#thing-actions-example).

## Full Example

### Things file
Expand Down Expand Up @@ -260,6 +280,32 @@ then
end
```

### Thing Actions Example

:::: tabs

::: tab JavaScript

```javascript
deconzActions = actions.get("deconz", "deconz:lightgroup:00212E040ED9:5");
retVal = deconzActions.createScene("TestScene");
deconzActions.storeScene(retVal["newSceneId"]);
lolodomo marked this conversation as resolved.
Show resolved Hide resolved
```

:::

::: tab DSL

```java
val deconzActions = getActions("deconz", "deconz:lightgroup:00212E040ED9:5");
var retVal = deconzActions.createScene("TestScene");
deconzActions.storeScene(retVal.get("newSceneId"));
```

:::

::::

### Troubleshooting

By default state updates are ignored for 250ms after a command.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
/**
* Copyright (c) 2010-2023 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
*/

/**
* Copyright (c) 2021-2022 Contributors to the SmartHome/J 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.deconz.internal.action;

import java.util.Map;
import java.util.Objects;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.http.HttpMethod;
import org.openhab.binding.deconz.internal.Util;
import org.openhab.binding.deconz.internal.handler.DeconzBridgeHandler;
import org.openhab.core.automation.annotation.ActionInput;
import org.openhab.core.automation.annotation.RuleAction;
import org.openhab.core.thing.binding.ThingActions;
import org.openhab.core.thing.binding.ThingActionsScope;
import org.openhab.core.thing.binding.ThingHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* The {@link BridgeActions} provides actions for managing scenes in groups
*
* @author Jan N. Klug - Initial contribution
*/
@ThingActionsScope(name = "deconz")
@NonNullByDefault
public class BridgeActions implements ThingActions {
private final Logger logger = LoggerFactory.getLogger(BridgeActions.class);

private @Nullable DeconzBridgeHandler handler;

@RuleAction(label = "@text/action.permit-join-network.label", description = "@text/action.permit-join-network.description")
public void permitJoin(
@ActionInput(name = "duration", label = "@text/action.permit-join-network.duration.label", description = "@text/action.permit-join-network.duration.description") @Nullable Integer duration) {
DeconzBridgeHandler handler = this.handler;

if (handler == null) {
logger.warn("Deconz BridgeActions service ThingHandler is null!");
return;
}

int searchDuration = Util.constrainToRange(Objects.requireNonNullElse(duration, 120), 1, 240);

Object object = Map.of("permitjoin", searchDuration);
handler.sendObject("config", object, HttpMethod.PUT).thenAccept(v -> {
if (v.getResponseCode() != java.net.HttpURLConnection.HTTP_OK) {
logger.warn("Sending {} via PUT to config failed: {} - {}", object, v.getResponseCode(), v.getBody());
} else {
logger.trace("Result code={}, body={}", v.getResponseCode(), v.getBody());
logger.info("Enabled device searching for {} seconds on bridge {}.", searchDuration,
handler.getThing().getUID());
}
}).exceptionally(e -> {
logger.warn("Sending {} via PUT to config failed: {} - {}", object, e.getClass(), e.getMessage());
return null;
});
}

public static void permitJoin(ThingActions actions, @Nullable Integer duration) {
if (actions instanceof BridgeActions) {
((BridgeActions) actions).permitJoin(duration);
}
}

@Override
public void setThingHandler(@Nullable ThingHandler handler) {
if (handler instanceof DeconzBridgeHandler) {
this.handler = (DeconzBridgeHandler) handler;
}
}

@Override
public @Nullable ThingHandler getThingHandler() {
return handler;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
/**
* Copyright (c) 2010-2023 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
*/

/**
* Copyright (c) 2021-2022 Contributors to the SmartHome/J 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.deconz.internal.action;

import java.lang.reflect.Type;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.http.HttpMethod;
import org.openhab.binding.deconz.internal.dto.NewSceneResponse;
import org.openhab.binding.deconz.internal.handler.GroupThingHandler;
import org.openhab.core.automation.annotation.ActionInput;
import org.openhab.core.automation.annotation.ActionOutput;
import org.openhab.core.automation.annotation.RuleAction;
import org.openhab.core.thing.binding.ThingActions;
import org.openhab.core.thing.binding.ThingActionsScope;
import org.openhab.core.thing.binding.ThingHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.gson.Gson;
import com.google.gson.JsonParseException;
import com.google.gson.reflect.TypeToken;

/**
* The {@link GroupActions} provides actions for managing scenes in groups
*
* @author Jan N. Klug - Initial contribution
*/
@ThingActionsScope(name = "deconz")
@NonNullByDefault
public class GroupActions implements ThingActions {
private static final String NEW_SCENE_ID_OUTPUT = "newSceneId";
private static final Type NEW_SCENE_RESPONSE_TYPE = new TypeToken<List<NewSceneResponse>>() {
}.getType();

private final Logger logger = LoggerFactory.getLogger(GroupActions.class);
private final Gson gson = new Gson();

private @Nullable GroupThingHandler handler;

@RuleAction(label = "@text/action.create-scene.label", description = "@text/action.create-scene.description")
public @ActionOutput(name = NEW_SCENE_ID_OUTPUT, type = "java.lang.Integer") Map<String, Object> createScene(
lolodomo marked this conversation as resolved.
Show resolved Hide resolved
@ActionInput(name = "name", label = "@text/action.create-scene.name.label", description = "@text/action.create-scene.name.description") @Nullable String name) {
GroupThingHandler handler = this.handler;

if (handler == null) {
logger.warn("Deconz GroupActions service ThingHandler is null!");
return Map.of();
}

if (name == null) {
logger.warn("Skipping scene creation due to missing scene name");
return Map.of();
}

CompletableFuture<String> newSceneId = new CompletableFuture<>();
handler.doNetwork(Map.of("name", name), "scenes", HttpMethod.POST, newSceneId::complete);

try {
String returnedJson = newSceneId.get(2000, TimeUnit.MILLISECONDS);
List<NewSceneResponse> newSceneResponses = gson.fromJson(returnedJson, NEW_SCENE_RESPONSE_TYPE);
if (newSceneResponses != null && !newSceneResponses.isEmpty()) {
return Map.of(NEW_SCENE_ID_OUTPUT, newSceneResponses.get(0).success.id);
}
throw new IllegalStateException("response is empty");
} catch (InterruptedException | ExecutionException | TimeoutException | JsonParseException
| IllegalStateException e) {
logger.warn("Couldn't get newSceneId", e);
return Map.of();
}
}

public static Map<String, Object> createScene(ThingActions actions, @Nullable String name) {
if (actions instanceof GroupActions) {
return ((GroupActions) actions).createScene(name);
}
return Map.of();
}

@RuleAction(label = "@text/action.delete-scene.label", description = "@text/action.delete-scene.description")
public void deleteScene(
@ActionInput(name = "sceneId", label = "@text/action.delete-scene.sceneId.label", description = "@text/action.delete-scene.sceneId.description") @Nullable Integer sceneId) {
GroupThingHandler handler = this.handler;

if (handler == null) {
logger.warn("Deconz GroupActions service ThingHandler is null!");
return;
}

if (sceneId == null) {
logger.warn("Skipping scene deletion due to missing scene id");
return;
}

handler.doNetwork(null, "scenes/" + sceneId, HttpMethod.DELETE, null);
}

public static void deleteScene(ThingActions actions, @Nullable Integer sceneId) {
if (actions instanceof GroupActions) {
((GroupActions) actions).deleteScene(sceneId);
}
}

@RuleAction(label = "@text/action.store-as-scene.label", description = "@text/action.store-as-scene.description")
public void storeScene(
@ActionInput(name = "sceneId", label = "@text/action.store-as-scene.sceneId.label", description = "@text/action.store-as-scene.sceneId.description") @Nullable Integer sceneId) {
GroupThingHandler handler = this.handler;

if (handler == null) {
logger.warn("Deconz GroupActions service ThingHandler is null!");
return;
}

if (sceneId == null) {
logger.warn("Skipping scene storage due to missing scene id");
return;
}

handler.doNetwork(null, "scenes/" + sceneId + "/store", HttpMethod.PUT, null);
}

public static void storeScene(ThingActions actions, @Nullable Integer sceneId) {
if (actions instanceof GroupActions) {
((GroupActions) actions).storeScene(sceneId);
}
}

@Override
public void setThingHandler(@Nullable ThingHandler handler) {
if (handler instanceof GroupThingHandler) {
this.handler = (GroupThingHandler) handler;
}
}

@Override
public @Nullable ThingHandler getThingHandler() {
return handler;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/**
* Copyright (c) 2010-2023 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
*/

/**
* Copyright (c) 2021-2022 Contributors to the SmartHome/J 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.deconz.internal.dto;

import org.eclipse.jdt.annotation.NonNullByDefault;

/**
* The {@link NewSceneResponse} is the response after a successfull scene creation
*
* @author Jan N. Klug - Initial contribution
*/
@NonNullByDefault
public class NewSceneResponse {
public Success success = new Success();

public class Success {
public int id = 0;
}
}
Loading