mirror of
https://github.com/danieldemus/openhab-addons
synced 2026-07-29 12:34:21 +02:00
[zwavejs] Initial contribution (#18694)
* Initial contribution Signed-off-by: Leo Siepel <leosiepel@gmail.com> Co-authored-by: Andrew Fiddian-Green <software@whitebear.ch> Co-authored-by: Cody Cutrer <cody@cutrer.us>
This commit is contained in:
co-authored by
Andrew Fiddian-Green
Cody Cutrer
parent
af3d22d500
commit
0f91185566
@@ -449,6 +449,7 @@
|
||||
/bundles/org.openhab.binding.yeelight/ @claell
|
||||
/bundles/org.openhab.binding.yioremote/ @miloit
|
||||
/bundles/org.openhab.binding.zoneminder/ @mhilbush
|
||||
/bundles/org.openhab.binding.zwavejs/ @lsiepel
|
||||
/bundles/org.openhab.binding.zway/ @pathec
|
||||
/bundles/org.openhab.io.homekit/ @andylintner @ccutrer @yfre
|
||||
/bundles/org.openhab.io.hueemulation/ @digitaldan
|
||||
|
||||
@@ -2221,6 +2221,11 @@
|
||||
<artifactId>org.openhab.binding.zoneminder</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.binding.zwavejs</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.binding.zway</artifactId>
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
This content is produced and maintained by the openHAB project.
|
||||
|
||||
* Project home: https://www.openhab.org
|
||||
|
||||
== Declared Project Licenses
|
||||
|
||||
This program and the accompanying materials are made available under the terms
|
||||
of the Eclipse Public License 2.0 which is available at
|
||||
https://www.eclipse.org/legal/epl-2.0/.
|
||||
|
||||
== Source Code
|
||||
|
||||
https://github.com/openhab/openhab-addons
|
||||
|
||||
== Third-party Content
|
||||
|
||||
error_prone_annotations
|
||||
* License: Apache License 2.0
|
||||
* Project: https://github.com/google/error-prone
|
||||
* Source: https://github.com/google/error-prone
|
||||
|
||||
gson RuntimeTypeAdapterFactory code snippet
|
||||
* License: Apache License 2.0
|
||||
* Project: https://github.com/google/gson/
|
||||
* Source: https://github.com/google/gson/blob/main/extras/src/main/java/com/google/gson/typeadapters/RuntimeTypeAdapterFactory.java
|
||||
@@ -0,0 +1,91 @@
|
||||
# Z-Wave JS Binding
|
||||
|
||||
The `zwavejs` binding integrates Z-Wave JS with openHAB, allowing you to control and monitor Z-Wave devices using the Z-Wave JS Webservice (part of Z-Wave JS UI).
|
||||
This binding supports a wide range of Z-Wave devices, including sensors, switches, dimmers, and more.
|
||||
For documentation about Z-Wave JS UI, please visit <https://zwave-js.github.io/zwave-js-ui/>.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before using the `zwavejs` binding, ensure the following prerequisites are met:
|
||||
|
||||
- You have a running instance of [Z-Wave JS](https://zwave-js.github.io/zwave-js-ui/).
|
||||
- Your Z-Wave controller is properly connected and recognized by Z-Wave JS.
|
||||
- The Z-Wave JS instance has the Webservice enabled (Settings -> Home Assistant -> WS Server).
|
||||
- Network connectivity exists between openHAB and the Z-Wave JS Webservice (the hostname and port must be reachable from openHAB).
|
||||
|
||||
**Note**: If you are transitioning from the native Z-Wave binding to the `zwavejs` binding, you can safely test this binding without permanently affecting your setup. All node information will remain on your controller as usual. However, please avoid performing a manual reset unless absolutely necessary, as it may complicate reverting to the native binding.
|
||||
|
||||
## Supported Things
|
||||
|
||||
This binding supports the following types of things:
|
||||
|
||||
- `gateway`: Represents the Z-Wave JS Webservice bridge. This is required to communicate with the Z-Wave network.
|
||||
- `node`: Represents a Z-Wave device (node) in the network. Each node can have multiple channels corresponding to its capabilities.
|
||||
|
||||
**Note**: This binding does not maintain a Z-Wave device database and relies on the external Z-Wave JS project for device compatibility and functionality.
|
||||
|
||||
## Discovery
|
||||
|
||||
The `zwavejs` binding supports auto-discovery of Z-Wave devices.
|
||||
When the bridge is added and connected, it will automatically discover and add Z-Wave nodes to the openHAB system.
|
||||
The following discovery features are available:
|
||||
|
||||
- Automatic discovery of Z-Wave nodes when they are added to the network.
|
||||
- Automatic update of node information when the node is updated in the Z-Wave network.
|
||||
|
||||
## Bridge Configuration
|
||||
|
||||
The `zwavejs` binding requires configuration of the bridge to connect to the Z-Wave JS Webservice.
|
||||
The configuration options include:
|
||||
|
||||
| Name | Type | Description | Default | Required | Advanced |
|
||||
|-----------------------|---------|------------------------------------------------------|---------|----------|----------|
|
||||
| hostname | text | Hostname or IP address of the server | N/A | yes | no |
|
||||
| port | number | Port number to access the service | 3000 | yes | no |
|
||||
| maxMessageSize | number | Maximum size of messages in bytes | 2097152 | no | yes |
|
||||
| configurationChannels | boolean | Expose the command class 'configuration' as channels | false | no | yes |
|
||||
|
||||
## Thing Configuration
|
||||
|
||||
Each Z-Wave node can have its own configuration options.
|
||||
All configuration parameters are set by the binding during startup and are read-only.
|
||||
Only the advanced parameter `id` can (optionally) be set in the Thing configuration in the openHAB UI or in the `.things` file.
|
||||
|
||||
## Channels
|
||||
|
||||
Z-Wave nodes can have multiple channels corresponding to their capabilities.
|
||||
The channels can be linked to items in openHAB to control and monitor the device.
|
||||
These channels are dynamically added to the Thing during node initialization; therefore, there is no list of possible channels in this documentation.
|
||||
|
||||
## Full Example
|
||||
|
||||
### `demo.things` Example
|
||||
|
||||
```java
|
||||
Bridge zwavejs:gateway:myBridge "Z-Wave JS Bridge" [ hostname="localhost", port=3000 ] {
|
||||
Thing node node1 "Z-Wave Node 1" [ id=1 ]
|
||||
Thing node node2 "Z-Wave Node 2" [ id=2 ]
|
||||
}
|
||||
```
|
||||
|
||||
### `demo.items` Example
|
||||
|
||||
```java
|
||||
Switch LightSwitch "Light Switch" { channel="zwavejs:node:myBridge:node1:binary-switch-value" }
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
If you encounter issues with the `zwavejs` binding, check the following:
|
||||
|
||||
- Ensure the Z-Wave JS Webservice is running and accessible at the configured hostname and port.
|
||||
- Check the openHAB logs for any error messages related to the `zwavejs` binding.
|
||||
- Verify the configuration of the bridge and nodes in the openHAB UI or configuration files.
|
||||
|
||||
For further assistance, refer to the openHAB community forums or the Z-Wave JS documentation.
|
||||
|
||||
## Resources
|
||||
|
||||
- [Z-Wave JS Documentation](https://zwave-js.github.io/node-zwave-js/)
|
||||
- [openHAB Documentation](https://www.openhab.org/docs/)
|
||||
- [openHAB Community](https://community.openhab.org/)
|
||||
@@ -0,0 +1,47 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.addons.reactor.bundles</artifactId>
|
||||
<version>5.0.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>org.openhab.binding.zwavejs</artifactId>
|
||||
|
||||
<name>openHAB Add-ons :: Bundles :: Z-Wave JS Binding</name>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.google.errorprone</groupId>
|
||||
<artifactId>error_prone_annotations</artifactId>
|
||||
<version>2.36.0</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.codehaus.mojo</groupId>
|
||||
<artifactId>build-helper-maven-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>add-source</goal>
|
||||
</goals>
|
||||
<phase>generate-sources</phase>
|
||||
<configuration>
|
||||
<sources>
|
||||
<source>src/3rdparty/java</source>
|
||||
</sources>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
</project>
|
||||
+320
@@ -0,0 +1,320 @@
|
||||
/*
|
||||
* Copyright (C) 2011 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.gson.typeadapters;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import com.google.errorprone.annotations.CanIgnoreReturnValue;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParseException;
|
||||
import com.google.gson.JsonPrimitive;
|
||||
import com.google.gson.TypeAdapter;
|
||||
import com.google.gson.TypeAdapterFactory;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import com.google.gson.stream.JsonReader;
|
||||
import com.google.gson.stream.JsonWriter;
|
||||
|
||||
/**
|
||||
* Adapts values whose runtime type may differ from their declaration type. This is necessary when a
|
||||
* field's type is not the same type that GSON should create when deserializing that field. For
|
||||
* example, consider these types:
|
||||
*
|
||||
* <pre>{@code
|
||||
* abstract class Shape {
|
||||
* int x;
|
||||
* int y;
|
||||
* }
|
||||
*
|
||||
* class Circle extends Shape {
|
||||
* int radius;
|
||||
* }
|
||||
*
|
||||
* class Rectangle extends Shape {
|
||||
* int width;
|
||||
* int height;
|
||||
* }
|
||||
*
|
||||
* class Diamond extends Shape {
|
||||
* int width;
|
||||
* int height;
|
||||
* }
|
||||
*
|
||||
* class Drawing {
|
||||
* Shape bottomShape;
|
||||
* Shape topShape;
|
||||
* }
|
||||
* }</pre>
|
||||
*
|
||||
* <p>
|
||||
* Without additional type information, the serialized JSON is ambiguous. Is the bottom shape in
|
||||
* this drawing a rectangle or a diamond?
|
||||
*
|
||||
* <pre>{@code
|
||||
* {
|
||||
* "bottomShape": {
|
||||
* "width": 10,
|
||||
* "height": 5,
|
||||
* "x": 0,
|
||||
* "y": 0
|
||||
* },
|
||||
* "topShape": {
|
||||
* "radius": 2,
|
||||
* "x": 4,
|
||||
* "y": 1
|
||||
* }
|
||||
* }
|
||||
* }</pre>
|
||||
*
|
||||
* This class addresses this problem by adding type information to the serialized JSON and honoring
|
||||
* that type information when the JSON is deserialized:
|
||||
*
|
||||
* <pre>{@code
|
||||
* {
|
||||
* "bottomShape": {
|
||||
* "type": "Diamond",
|
||||
* "width": 10,
|
||||
* "height": 5,
|
||||
* "x": 0,
|
||||
* "y": 0
|
||||
* },
|
||||
* "topShape": {
|
||||
* "type": "Circle",
|
||||
* "radius": 2,
|
||||
* "x": 4,
|
||||
* "y": 1
|
||||
* }
|
||||
* }
|
||||
* }</pre>
|
||||
*
|
||||
* Both the type field name ({@code "type"}) and the type labels ({@code "Rectangle"}) are
|
||||
* configurable.
|
||||
*
|
||||
* <h2>Registering Types</h2>
|
||||
*
|
||||
* Create a {@code RuntimeTypeAdapterFactory} by passing the base type and type field name to the
|
||||
* {@link #of} factory method. If you don't supply an explicit type field name, {@code "type"} will
|
||||
* be used.
|
||||
*
|
||||
* <pre>{@code
|
||||
* RuntimeTypeAdapterFactory<Shape> shapeAdapterFactory = RuntimeTypeAdapterFactory.of(Shape.class, "type");
|
||||
* }</pre>
|
||||
*
|
||||
* Next register all of your subtypes. Every subtype must be explicitly registered. This protects
|
||||
* your application from injection attacks. If you don't supply an explicit type label, the type's
|
||||
* simple name will be used.
|
||||
*
|
||||
* <pre>{@code
|
||||
* shapeAdapterFactory.registerSubtype(Rectangle.class, "Rectangle");
|
||||
* shapeAdapterFactory.registerSubtype(Circle.class, "Circle");
|
||||
* shapeAdapterFactory.registerSubtype(Diamond.class, "Diamond");
|
||||
* }</pre>
|
||||
*
|
||||
* Finally, register the type adapter factory in your application's GSON builder:
|
||||
*
|
||||
* <pre>{@code
|
||||
* Gson gson = new GsonBuilder().registerTypeAdapterFactory(shapeAdapterFactory).create();
|
||||
* }</pre>
|
||||
*
|
||||
* Like {@code GsonBuilder}, this API supports chaining:
|
||||
*
|
||||
* <pre>{@code
|
||||
* RuntimeTypeAdapterFactory<Shape> shapeAdapterFactory = RuntimeTypeAdapterFactory.of(Shape.class)
|
||||
* .registerSubtype(Rectangle.class).registerSubtype(Circle.class).registerSubtype(Diamond.class);
|
||||
* }</pre>
|
||||
*
|
||||
* <h2>Serialization and deserialization</h2>
|
||||
*
|
||||
* In order to serialize and deserialize a polymorphic object, you must specify the base type
|
||||
* explicitly.
|
||||
*
|
||||
* <pre>{@code
|
||||
* Diamond diamond = new Diamond();
|
||||
* String json = gson.toJson(diamond, Shape.class);
|
||||
* }</pre>
|
||||
*
|
||||
* And then:
|
||||
*
|
||||
* <pre>{@code
|
||||
* Shape shape = gson.fromJson(json, Shape.class);
|
||||
* }</pre>
|
||||
*/
|
||||
public final class RuntimeTypeAdapterFactory<T> implements TypeAdapterFactory {
|
||||
private final Class<?> baseType;
|
||||
private final String typeFieldName;
|
||||
private final Map<String, Class<?>> labelToSubtype = new LinkedHashMap<>();
|
||||
private final Map<Class<?>, String> subtypeToLabel = new LinkedHashMap<>();
|
||||
private final boolean maintainType;
|
||||
private boolean recognizeSubtypes;
|
||||
|
||||
private RuntimeTypeAdapterFactory(Class<?> baseType, String typeFieldName, boolean maintainType) {
|
||||
if (typeFieldName == null || baseType == null) {
|
||||
throw new NullPointerException();
|
||||
}
|
||||
this.baseType = baseType;
|
||||
this.typeFieldName = typeFieldName;
|
||||
this.maintainType = maintainType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new runtime type adapter for {@code baseType} using {@code typeFieldName} as the type
|
||||
* field name. Type field names are case sensitive.
|
||||
*
|
||||
* @param maintainType true if the type field should be included in deserialized objects
|
||||
*/
|
||||
public static <T> RuntimeTypeAdapterFactory<T> of(Class<T> baseType, String typeFieldName, boolean maintainType) {
|
||||
return new RuntimeTypeAdapterFactory<>(baseType, typeFieldName, maintainType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new runtime type adapter for {@code baseType} using {@code typeFieldName} as the type
|
||||
* field name. Type field names are case sensitive.
|
||||
*/
|
||||
public static <T> RuntimeTypeAdapterFactory<T> of(Class<T> baseType, String typeFieldName) {
|
||||
return new RuntimeTypeAdapterFactory<>(baseType, typeFieldName, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new runtime type adapter for {@code baseType} using {@code "type"} as the type field
|
||||
* name.
|
||||
*/
|
||||
public static <T> RuntimeTypeAdapterFactory<T> of(Class<T> baseType) {
|
||||
return new RuntimeTypeAdapterFactory<>(baseType, "type", false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that this factory will handle not just the given {@code baseType}, but any subtype of
|
||||
* that type.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public RuntimeTypeAdapterFactory<T> recognizeSubtypes() {
|
||||
this.recognizeSubtypes = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers {@code type} identified by {@code label}. Labels are case sensitive.
|
||||
*
|
||||
* @throws IllegalArgumentException if either {@code type} or {@code label} have already been
|
||||
* registered on this type adapter.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public RuntimeTypeAdapterFactory<T> registerSubtype(Class<? extends T> type, String label) {
|
||||
if (type == null || label == null) {
|
||||
throw new NullPointerException();
|
||||
}
|
||||
if (subtypeToLabel.containsKey(type) || labelToSubtype.containsKey(label)) {
|
||||
throw new IllegalArgumentException("types and labels must be unique");
|
||||
}
|
||||
labelToSubtype.put(label, type);
|
||||
subtypeToLabel.put(type, label);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers {@code type} identified by its {@link Class#getSimpleName simple name}. Labels are
|
||||
* case sensitive.
|
||||
*
|
||||
* @throws IllegalArgumentException if either {@code type} or its simple name have already been
|
||||
* registered on this type adapter.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public RuntimeTypeAdapterFactory<T> registerSubtype(Class<? extends T> type) {
|
||||
return registerSubtype(type, type.getSimpleName());
|
||||
}
|
||||
|
||||
@Override
|
||||
public <R> TypeAdapter<R> create(Gson gson, TypeToken<R> type) {
|
||||
if (type == null) {
|
||||
return null;
|
||||
}
|
||||
Class<?> rawType = type.getRawType();
|
||||
boolean handle = recognizeSubtypes ? baseType.isAssignableFrom(rawType) : baseType.equals(rawType);
|
||||
if (!handle) {
|
||||
return null;
|
||||
}
|
||||
|
||||
TypeAdapter<JsonElement> jsonElementAdapter = gson.getAdapter(JsonElement.class);
|
||||
Map<String, TypeAdapter<?>> labelToDelegate = new LinkedHashMap<>();
|
||||
Map<Class<?>, TypeAdapter<?>> subtypeToDelegate = new LinkedHashMap<>();
|
||||
for (Map.Entry<String, Class<?>> entry : labelToSubtype.entrySet()) {
|
||||
TypeAdapter<?> delegate = gson.getDelegateAdapter(this, TypeToken.get(entry.getValue()));
|
||||
labelToDelegate.put(entry.getKey(), delegate);
|
||||
subtypeToDelegate.put(entry.getValue(), delegate);
|
||||
}
|
||||
|
||||
return new TypeAdapter<R>() {
|
||||
@Override
|
||||
public R read(JsonReader in) throws IOException {
|
||||
JsonElement jsonElement = jsonElementAdapter.read(in);
|
||||
JsonElement labelJsonElement;
|
||||
if (maintainType) {
|
||||
labelJsonElement = jsonElement.getAsJsonObject().get(typeFieldName);
|
||||
} else {
|
||||
labelJsonElement = jsonElement.getAsJsonObject().remove(typeFieldName);
|
||||
}
|
||||
|
||||
if (labelJsonElement == null) {
|
||||
throw new JsonParseException("cannot deserialize " + baseType
|
||||
+ " because it does not define a field named " + typeFieldName);
|
||||
}
|
||||
String label = labelJsonElement.getAsString();
|
||||
@SuppressWarnings("unchecked") // registration requires that subtype extends T
|
||||
TypeAdapter<R> delegate = (TypeAdapter<R>) labelToDelegate.get(label);
|
||||
if (delegate == null) {
|
||||
throw new JsonParseException("cannot deserialize " + baseType + " subtype named " + label
|
||||
+ "; did you forget to register a subtype?");
|
||||
}
|
||||
return delegate.fromJsonTree(jsonElement);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(JsonWriter out, R value) throws IOException {
|
||||
Class<?> srcType = value.getClass();
|
||||
String label = subtypeToLabel.get(srcType);
|
||||
@SuppressWarnings("unchecked") // registration requires that subtype extends T
|
||||
TypeAdapter<R> delegate = (TypeAdapter<R>) subtypeToDelegate.get(srcType);
|
||||
if (delegate == null) {
|
||||
throw new JsonParseException(
|
||||
"cannot serialize " + srcType.getName() + "; did you forget to register a subtype?");
|
||||
}
|
||||
JsonObject jsonObject = delegate.toJsonTree(value).getAsJsonObject();
|
||||
|
||||
if (maintainType) {
|
||||
jsonElementAdapter.write(out, jsonObject);
|
||||
return;
|
||||
}
|
||||
|
||||
JsonObject clone = new JsonObject();
|
||||
|
||||
if (jsonObject.has(typeFieldName)) {
|
||||
throw new JsonParseException("cannot serialize " + srcType.getName()
|
||||
+ " because it already defines a field named " + typeFieldName);
|
||||
}
|
||||
clone.add(typeFieldName, new JsonPrimitive(label));
|
||||
|
||||
for (Map.Entry<String, JsonElement> e : jsonObject.entrySet()) {
|
||||
clone.add(e.getKey(), e.getValue());
|
||||
}
|
||||
jsonElementAdapter.write(out, clone);
|
||||
}
|
||||
}.nullSafe();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<features name="org.openhab.binding.zwavejs-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
|
||||
<repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
|
||||
|
||||
<feature name="openhab-binding-zwavejs" description="Z-Wave JS Binding" version="${project.version}">
|
||||
<feature>openhab-runtime-base</feature>
|
||||
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.zwavejs/${project.version}</bundle>
|
||||
</feature>
|
||||
</features>
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2025 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.zwavejs.internal;
|
||||
|
||||
import static org.openhab.binding.zwavejs.internal.CommandClassConstants.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
|
||||
/**
|
||||
* The {@link BindingConstants} class defines common constants, which are
|
||||
* used across the whole binding.
|
||||
*
|
||||
* @author Leo Siepel - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class BindingConstants {
|
||||
|
||||
public static final String BINDING_ID = "zwavejs";
|
||||
|
||||
public static final String DISCOVERY_NODE_LABEL_PATTERN = "%s %s (node %s)";
|
||||
|
||||
// List of all Thing Type UIDs
|
||||
public static final ThingTypeUID THING_TYPE_GATEWAY = new ThingTypeUID(BINDING_ID, "gateway");
|
||||
public static final ThingTypeUID THING_TYPE_NODE = new ThingTypeUID(BINDING_ID, "node");
|
||||
|
||||
// List of all Thing Configuration Parameters
|
||||
public static final String CONFIG_HOSTNAME = "hostname";
|
||||
public static final String CONFIG_PORT = "port";
|
||||
public static final String CONFIG_NODE_ID = "id";
|
||||
public static final String CONFIG_CONFIG_AS_CHANNEL = "configurationChannels";
|
||||
|
||||
// List of all Channel Configuration Parameters
|
||||
public static final String CONFIG_CHANNEL_INCOMING_UNIT = "incomingUnit";
|
||||
public static final String CONFIG_CHANNEL_COMMANDCLASS_NAME = "commandClassName";
|
||||
public static final String CONFIG_CHANNEL_COMMANDCLASS_ID = "commandClassId";
|
||||
public static final String CONFIG_CHANNEL_ENDPOINT = "endpoint";
|
||||
public static final String CONFIG_CHANNEL_PROPERTY_KEY_STR = "propertyKeyStr";
|
||||
public static final String CONFIG_CHANNEL_PROPERTY_KEY_INT = "propertyKeyInt";
|
||||
public static final String CONFIG_CHANNEL_READ_PROPERTY = "readProperty";
|
||||
public static final String CONFIG_CHANNEL_WRITE_PROPERTY_STR = "writePropertyStr";
|
||||
public static final String CONFIG_CHANNEL_WRITE_PROPERTY_INT = "writePropertyInt";
|
||||
public static final String CONFIG_CHANNEL_INVERTED = "inverted";
|
||||
public static final String CONFIG_CHANNEL_FACTOR = "factor";
|
||||
|
||||
// List of all Thing Properties
|
||||
public static final String PROPERTY_HOME_ID = "homeId";
|
||||
public static final String PROPERTY_DRIVER_VERSION = "driverVersion";
|
||||
public static final String PROPERTY_SERVER_VERSION = "serverVersion";
|
||||
public static final String PROPERTY_SCHEMA_MIN = "minSchemaVersion";
|
||||
public static final String PROPERTY_SCHEMA_MAX = "maxSchemaVersion";
|
||||
public static final String PROPERTY_NODE_IS_SECURE = "isSecure";
|
||||
public static final String PROPERTY_NODE_IS_LISTENING = "isListening";
|
||||
public static final String PROPERTY_NODE_IS_ROUTING = "isRouting";
|
||||
public static final String PROPERTY_NODE_LASTSEEN = "lastSeen";
|
||||
public static final String PROPERTY_NODE_FREQ_LISTENING = "isFrequentListening";
|
||||
|
||||
public static final List<Integer> CONFIGURATION_COMMAND_CLASSES = List.of(COMMAND_CLASS_CONFIGURATION,
|
||||
COMMAND_CLASS_WAKE_UP);
|
||||
|
||||
// color related property keys
|
||||
public static final int WARM_PROPERTY_KEY = 0;
|
||||
public static final int COLD_PROPERTY_KEY = 1;
|
||||
public static final int RED_PROPERTY_KEY = 2;
|
||||
public static final int GREEN_PROPERTY_KEY = 3;
|
||||
public static final int BLUE_PROPERTY_KEY = 4;
|
||||
|
||||
// color related strings
|
||||
public static final String RED = "red";
|
||||
public static final String GREEN = "green";
|
||||
public static final String BLUE = "blue";
|
||||
public static final String WARM_WHITE = "warmWhite";
|
||||
public static final String COLD_WHITE = "coldWhite";
|
||||
public static final String HEX = "hex";
|
||||
public static final String COLOR_TEMP_CHANNEL_COMMAND_CLASS_NAME = "Color Switch";
|
||||
public static final String COLOR_TEMP_CHANNEL_PROPERTY_NAME = "colorTemperature";
|
||||
}
|
||||
+244
@@ -0,0 +1,244 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2025 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.zwavejs.internal;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.core.semantics.SemanticTag;
|
||||
import org.openhab.core.semantics.model.DefaultSemanticTags.Equipment;
|
||||
|
||||
/**
|
||||
* Contains the Z-Wave predefined command class constants.
|
||||
* Used for semantic tagging and differentiation of control functions.
|
||||
*
|
||||
* @author Andrew Fiddian-Green - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class CommandClassConstants {
|
||||
|
||||
public static final Integer COMMAND_CLASS_ALARM = 0x71;
|
||||
public static final Integer COMMAND_CLASS_ANTITHEFT = 0x5D;
|
||||
public static final Integer COMMAND_CLASS_ANTITHEFT_UNLOCK = 0x7E;
|
||||
public static final Integer COMMAND_CLASS_APPLICATION_STATUS = 0x22;
|
||||
public static final Integer COMMAND_CLASS_ASSOCIATION = 0x85;
|
||||
public static final Integer COMMAND_CLASS_ASSOCIATION_COMMAND_CONFIGURATION = 0x9B;
|
||||
public static final Integer COMMAND_CLASS_ASSOCIATION_GRP_INFO = 0x59;
|
||||
public static final Integer COMMAND_CLASS_AUTHENTICATION = 0xA1;
|
||||
public static final Integer COMMAND_CLASS_AUTHENTICATION_MEDIA_WRITE = 0xA2;
|
||||
public static final Integer COMMAND_CLASS_BARRIER_OPERATOR = 0x66;
|
||||
public static final Integer COMMAND_CLASS_BASIC = 0x20;
|
||||
public static final Integer COMMAND_CLASS_BASIC_TARIFF_INFO = 0x36;
|
||||
public static final Integer COMMAND_CLASS_BATTERY = 0x80;
|
||||
public static final Integer COMMAND_CLASS_CENTRAL_SCENE = 0x5B;
|
||||
public static final Integer COMMAND_CLASS_CLIMATE_CONTROL_SCHEDULE = 0x46;
|
||||
public static final Integer COMMAND_CLASS_CLOCK = 0x81;
|
||||
public static final Integer COMMAND_CLASS_CONFIGURATION = 0x70;
|
||||
public static final Integer COMMAND_CLASS_CONTROLLER_REPLICATION = 0x21;
|
||||
public static final Integer COMMAND_CLASS_CRC_16_ENCAP = 0x56;
|
||||
public static final Integer COMMAND_CLASS_DCP_CONFIG = 0x3A;
|
||||
public static final Integer COMMAND_CLASS_DCP_MONITOR = 0x3B;
|
||||
public static final Integer COMMAND_CLASS_DEVICE_RESET_LOCALLY = 0x5A;
|
||||
public static final Integer COMMAND_CLASS_DOOR_LOCK = 0x62;
|
||||
public static final Integer COMMAND_CLASS_DOOR_LOCK_LOGGING = 0x4C;
|
||||
public static final Integer COMMAND_CLASS_ENERGY_PRODUCTION = 0x90;
|
||||
public static final Integer COMMAND_CLASS_ENTRY_CONTROL = 0x6F;
|
||||
public static final Integer COMMAND_CLASS_FIRMWARE_UPDATE_MD = 0x7A;
|
||||
public static final Integer COMMAND_CLASS_GENERIC_SCHEDULE = 0xA3;
|
||||
public static final Integer COMMAND_CLASS_GEOGRAPHIC_LOCATION = 0x8C;
|
||||
public static final Integer COMMAND_CLASS_GROUPING_NAME = 0x7B;
|
||||
public static final Integer COMMAND_CLASS_HRV_CONTROL = 0x39;
|
||||
public static final Integer COMMAND_CLASS_HRV_STATUS = 0x37;
|
||||
public static final Integer COMMAND_CLASS_HUMIDITY_CONTROL_MODE = 0x6D;
|
||||
public static final Integer COMMAND_CLASS_HUMIDITY_CONTROL_OPERATING_STATE = 0x6E;
|
||||
public static final Integer COMMAND_CLASS_HUMIDITY_CONTROL_SETPOINT = 0x64;
|
||||
public static final Integer COMMAND_CLASS_INCLUSION_CONTROLLER = 0x74;
|
||||
public static final Integer COMMAND_CLASS_INDICATOR = 0x87;
|
||||
public static final Integer COMMAND_CLASS_IP_ASSOCIATION = 0x5C;
|
||||
public static final Integer COMMAND_CLASS_IR_REPEATER = 0xA0;
|
||||
public static final Integer COMMAND_CLASS_IRRIGATION = 0x6B;
|
||||
public static final Integer COMMAND_CLASS_LANGUAGE = 0x89;
|
||||
public static final Integer COMMAND_CLASS_LOCK = 0x76;
|
||||
public static final Integer COMMAND_CLASS_MAILBOX = 0x69;
|
||||
public static final Integer COMMAND_CLASS_MANUFACTURER_PROPRIETARY = 0x91;
|
||||
public static final Integer COMMAND_CLASS_MANUFACTURER_SPECIFIC = 0x72;
|
||||
public static final Integer COMMAND_CLASS_MARK = 0xEF;
|
||||
public static final Integer COMMAND_CLASS_METER = 0x32;
|
||||
public static final Integer COMMAND_CLASS_METER_PULSE = 0x35;
|
||||
public static final Integer COMMAND_CLASS_METER_TBL_CONFIG = 0x3C;
|
||||
public static final Integer COMMAND_CLASS_METER_TBL_MONITOR = 0x3D;
|
||||
public static final Integer COMMAND_CLASS_METER_TBL_PUSH = 0x3E;
|
||||
public static final Integer COMMAND_CLASS_MULTI_CHANNEL = 0x60;
|
||||
public static final Integer COMMAND_CLASS_MULTI_CHANNEL_ASSOCIATION = 0x8E;
|
||||
public static final Integer COMMAND_CLASS_MULTI_CMD = 0x8F;
|
||||
public static final Integer COMMAND_CLASS_NETWORK_MANAGEMENT_BASIC = 0x4D;
|
||||
public static final Integer COMMAND_CLASS_NETWORK_MANAGEMENT_INCLUSION = 0x34;
|
||||
public static final Integer NETWORK_MANAGEMENT_INSTALLATION_MAINTENANCE = 0x67;
|
||||
public static final Integer COMMAND_CLASS_NETWORK_MANAGEMENT_PROXY = 0x52;
|
||||
public static final Integer COMMAND_CLASS_NO_OPERATION = 0x00;
|
||||
public static final Integer COMMAND_CLASS_NODE_NAMING = 0x77;
|
||||
public static final Integer COMMAND_CLASS_NODE_PROVISIONING = 0x78;
|
||||
public static final Integer COMMAND_CLASS_POWERLEVEL = 0x73;
|
||||
public static final Integer COMMAND_CLASS_PREPAYMENT = 0x3F;
|
||||
public static final Integer COMMAND_CLASS_PREPAYMENT_ENCAPSULATION = 0x41;
|
||||
public static final Integer COMMAND_CLASS_PROPRIETARY = 0x88;
|
||||
public static final Integer COMMAND_CLASS_PROTECTION = 0x75;
|
||||
public static final Integer COMMAND_CLASS_RATE_TBL_CONFIG = 0x48;
|
||||
public static final Integer COMMAND_CLASS_RATE_TBL_MONITOR = 0x49;
|
||||
public static final Integer COMMAND_CLASS_SCENE_ACTIVATION = 0x2B;
|
||||
public static final Integer COMMAND_CLASS_SCENE_ACTUATOR_CONF = 0x2C;
|
||||
public static final Integer COMMAND_CLASS_SCENE_CONTROLLER_CONF = 0x2D;
|
||||
public static final Integer COMMAND_CLASS_SCHEDULE = 0x53;
|
||||
public static final Integer COMMAND_CLASS_SCHEDULE_ENTRY_LOCK = 0x4E;
|
||||
public static final Integer COMMAND_CLASS_SCREEN_ATTRIBUTES = 0x93;
|
||||
public static final Integer COMMAND_CLASS_SCREEN_MD = 0x92;
|
||||
public static final Integer COMMAND_CLASS_SECURITY = 0x98;
|
||||
public static final Integer COMMAND_CLASS_SECURITY_2 = 0x9F;
|
||||
public static final Integer COMMAND_CLASS_SECURITY_SCHEME0_MARK = 0xF100;
|
||||
public static final Integer COMMAND_CLASS_SENSOR_ALARM = 0x9C;
|
||||
public static final Integer COMMAND_CLASS_SENSOR_BINARY = 0x30;
|
||||
public static final Integer COMMAND_CLASS_SENSOR_MULTILEVEL = 0x31;
|
||||
public static final Integer COMMAND_CLASS_SILENCE_ALARM = 0x9D;
|
||||
public static final Integer COMMAND_CLASS_SIMPLE_AV_CONTROL = 0x94;
|
||||
public static final Integer COMMAND_CLASS_SOUND_SWITCH = 0x79;
|
||||
public static final Integer COMMAND_CLASS_SUPERVISION = 0x6C;
|
||||
public static final Integer COMMAND_CLASS_SWITCH_BINARY = 0x25;
|
||||
public static final Integer COMMAND_CLASS_SWITCH_COLOR = 0x33;
|
||||
public static final Integer COMMAND_CLASS_SWITCH_MULTILEVEL = 0x26;
|
||||
public static final Integer COMMAND_CLASS_SWITCH_TOGGLE_MULTILEVEL = 0x29;
|
||||
public static final Integer COMMAND_CLASS_TARIFF_CONFIG = 0x4A;
|
||||
public static final Integer COMMAND_CLASS_TARIFF_TBL_MONITOR = 0x4B;
|
||||
public static final Integer COMMAND_CLASS_THERMOSTAT_FAN_MODE = 0x44;
|
||||
public static final Integer COMMAND_CLASS_THERMOSTAT_FAN_STATE = 0x45;
|
||||
public static final Integer COMMAND_CLASS_THERMOSTAT_MODE = 0x40;
|
||||
public static final Integer COMMAND_CLASS_THERMOSTAT_OPERATING_STATE = 0x42;
|
||||
public static final Integer COMMAND_CLASS_THERMOSTAT_SETBACK = 0x47;
|
||||
public static final Integer COMMAND_CLASS_THERMOSTAT_SETPOINT = 0x43;
|
||||
public static final Integer COMMAND_CLASS_TIME = 0x8A;
|
||||
public static final Integer COMMAND_CLASS_TIME_PARAMETERS = 0x8B;
|
||||
public static final Integer COMMAND_CLASS_TRANSPORT_SERVICE = 0x55;
|
||||
public static final Integer COMMAND_CLASS_USER_CODE = 0x63;
|
||||
public static final Integer COMMAND_CLASS_VERSION = 0x86;
|
||||
public static final Integer COMMAND_CLASS_WAKE_UP = 0x84;
|
||||
public static final Integer COMMAND_CLASS_WINDOW_COVERING = 0x6A;
|
||||
public static final Integer COMMAND_CLASS_ZIP = 0x23;
|
||||
public static final Integer COMMAND_CLASS_ZIP_6LOWPAN = 0x4F;
|
||||
public static final Integer COMMAND_CLASS_ZIP_GATEWAY = 0x5F;
|
||||
public static final Integer COMMAND_CLASS_ZIP_NAMING = 0x68;
|
||||
public static final Integer COMMAND_CLASS_ZIP_ND = 0x58;
|
||||
public static final Integer COMMAND_CLASS_ZIP_PORTAL = 0x61;
|
||||
public static final Integer COMMAND_CLASS_ZWAVEPLUS_INFO = 0x5E;
|
||||
|
||||
//@formatter:off
|
||||
public static final Set<Integer> COMMAND_SET_ALARM_DEVICE_EQUIPMENT = Set.of(
|
||||
COMMAND_CLASS_ALARM,
|
||||
COMMAND_CLASS_ANTITHEFT,
|
||||
COMMAND_CLASS_ANTITHEFT_UNLOCK,
|
||||
COMMAND_CLASS_SECURITY,
|
||||
COMMAND_CLASS_SECURITY_2,
|
||||
COMMAND_CLASS_SECURITY_SCHEME0_MARK,
|
||||
COMMAND_CLASS_SENSOR_ALARM,
|
||||
COMMAND_CLASS_SILENCE_ALARM);
|
||||
|
||||
public static final Set<Integer> COMMAND_SET_AUDIO_VISUAL_EQUIPMENT = Set.of(
|
||||
COMMAND_CLASS_SOUND_SWITCH);
|
||||
|
||||
public static final Set<Integer> COMMAND_SET_BATTERY_EQUIPMENT = Set.of(
|
||||
COMMAND_CLASS_BATTERY);
|
||||
|
||||
public static final Set<Integer> COMMAND_SET_CONTROL_DEVICE_EQUIPMENT = Set.of(
|
||||
COMMAND_CLASS_SWITCH_BINARY);
|
||||
|
||||
public static final Set<Integer> COMMAND_SET_DISPLAY_EQUIPMENT = Set.of(
|
||||
COMMAND_CLASS_INDICATOR);
|
||||
|
||||
public static final Set<Integer> COMMAND_SET_GATE_EQUIPMENT = Set.of(
|
||||
COMMAND_CLASS_BARRIER_OPERATOR);
|
||||
|
||||
public static final Set<Integer> COMMAND_SET_HUMIDIFIER_EQUIPMENT = Set.of(
|
||||
COMMAND_CLASS_HUMIDITY_CONTROL_MODE,
|
||||
COMMAND_CLASS_HUMIDITY_CONTROL_OPERATING_STATE,
|
||||
COMMAND_CLASS_HUMIDITY_CONTROL_SETPOINT);
|
||||
|
||||
public static final Set<Integer> COMMAND_SET_HVAC_EQUIPMENT = Set.of(
|
||||
COMMAND_CLASS_HRV_STATUS,
|
||||
COMMAND_CLASS_HRV_CONTROL);
|
||||
|
||||
public static final Set<Integer> COMMAND_SET_IRRIGATION_EQUIPMENT = Set.of(
|
||||
COMMAND_CLASS_IRRIGATION);
|
||||
|
||||
public static final Set<Integer> COMMAND_SET_LIGHT_SOURCE_EQUIPMENT = Set.of(
|
||||
COMMAND_CLASS_SWITCH_COLOR,
|
||||
COMMAND_CLASS_SWITCH_MULTILEVEL,
|
||||
COMMAND_CLASS_SWITCH_TOGGLE_MULTILEVEL);
|
||||
|
||||
public static final Set<Integer> COMMAND_SET_LOCK_EQUIPMENT = Set.of(
|
||||
COMMAND_CLASS_DOOR_LOCK,
|
||||
COMMAND_CLASS_DOOR_LOCK_LOGGING,
|
||||
COMMAND_CLASS_ENTRY_CONTROL);
|
||||
|
||||
public static final Set<Integer> COMMAND_SET_METER_EQUIPMENT = Set.of(
|
||||
COMMAND_CLASS_BASIC_TARIFF_INFO,
|
||||
COMMAND_CLASS_METER,
|
||||
COMMAND_CLASS_METER_PULSE,
|
||||
COMMAND_CLASS_METER_TBL_CONFIG,
|
||||
COMMAND_CLASS_METER_TBL_MONITOR,
|
||||
COMMAND_CLASS_METER_TBL_PUSH,
|
||||
COMMAND_CLASS_POWERLEVEL,
|
||||
COMMAND_CLASS_TARIFF_CONFIG,
|
||||
COMMAND_CLASS_TARIFF_TBL_MONITOR);
|
||||
|
||||
public static final Set<Integer> COMMAND_SET_POWER_SUPPLY_EQUIPMENT = Set.of(
|
||||
COMMAND_CLASS_ENERGY_PRODUCTION);
|
||||
|
||||
public static final Set<Integer> COMMAND_SET_SENSOR_EQUIPMENT = Set.of(
|
||||
COMMAND_CLASS_SENSOR_BINARY,
|
||||
COMMAND_CLASS_SENSOR_MULTILEVEL);
|
||||
|
||||
public static final Set<Integer> COMMAND_SET_THERMOSTAT_EQUIPMENT = Set.of(
|
||||
COMMAND_CLASS_THERMOSTAT_FAN_MODE,
|
||||
COMMAND_CLASS_THERMOSTAT_FAN_STATE,
|
||||
COMMAND_CLASS_THERMOSTAT_MODE,
|
||||
COMMAND_CLASS_THERMOSTAT_OPERATING_STATE,
|
||||
COMMAND_CLASS_THERMOSTAT_SETBACK,
|
||||
COMMAND_CLASS_THERMOSTAT_SETPOINT);
|
||||
|
||||
public static final Set<Integer> COMMAND_SET_WINDOW_COVERING_EQUIPMENT = Set.of(
|
||||
COMMAND_CLASS_WINDOW_COVERING);
|
||||
|
||||
public static final Set<Integer> COMMAND_SET_ZONE_EQUIPMENT = Set.of(
|
||||
COMMAND_CLASS_SCENE_ACTIVATION);
|
||||
|
||||
//@formatter:on`
|
||||
|
||||
// Map of command class sets to their corresponding equipment tags
|
||||
public static final Map<Set<Integer>, SemanticTag> EQUIPMENT_MAP = Map.ofEntries(
|
||||
Map.entry(COMMAND_SET_LIGHT_SOURCE_EQUIPMENT, Equipment.LIGHT_SOURCE),
|
||||
Map.entry(COMMAND_SET_ALARM_DEVICE_EQUIPMENT, Equipment.ALARM_DEVICE),
|
||||
Map.entry(COMMAND_SET_AUDIO_VISUAL_EQUIPMENT, Equipment.AUDIO_VISUAL),
|
||||
Map.entry(COMMAND_SET_BATTERY_EQUIPMENT, Equipment.BATTERY),
|
||||
Map.entry(COMMAND_SET_CONTROL_DEVICE_EQUIPMENT, Equipment.CONTROL_DEVICE),
|
||||
Map.entry(COMMAND_SET_DISPLAY_EQUIPMENT, Equipment.DISPLAY),
|
||||
Map.entry(COMMAND_SET_GATE_EQUIPMENT, Equipment.GATE),
|
||||
Map.entry(COMMAND_SET_HUMIDIFIER_EQUIPMENT, Equipment.HUMIDIFIER),
|
||||
Map.entry(COMMAND_SET_HVAC_EQUIPMENT, Equipment.HVAC),
|
||||
Map.entry(COMMAND_SET_IRRIGATION_EQUIPMENT, Equipment.IRRIGATION),
|
||||
Map.entry(COMMAND_SET_LOCK_EQUIPMENT, Equipment.LOCK),
|
||||
Map.entry(COMMAND_SET_METER_EQUIPMENT, Equipment.ELECTRIC_METER),
|
||||
Map.entry(COMMAND_SET_POWER_SUPPLY_EQUIPMENT, Equipment.POWER_SUPPLY),
|
||||
Map.entry(COMMAND_SET_SENSOR_EQUIPMENT, Equipment.SENSOR),
|
||||
Map.entry(COMMAND_SET_THERMOSTAT_EQUIPMENT, Equipment.THERMOSTAT),
|
||||
Map.entry(COMMAND_SET_WINDOW_COVERING_EQUIPMENT, Equipment.WINDOW_COVERING),
|
||||
Map.entry(COMMAND_SET_ZONE_EQUIPMENT, Equipment.ZONE));
|
||||
}
|
||||
+112
@@ -0,0 +1,112 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2025 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.zwavejs.internal.action;
|
||||
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.zwavejs.internal.BindingConstants;
|
||||
import org.openhab.binding.zwavejs.internal.handler.ZwaveJSBridgeHandler;
|
||||
import org.openhab.core.automation.annotation.ActionInput;
|
||||
import org.openhab.core.automation.annotation.RuleAction;
|
||||
import org.openhab.core.common.ThreadPoolManager;
|
||||
import org.openhab.core.thing.binding.ThingActions;
|
||||
import org.openhab.core.thing.binding.ThingActionsScope;
|
||||
import org.openhab.core.thing.binding.ThingHandler;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
import org.osgi.service.component.annotations.ServiceScope;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* This class allows interaction with Z-Wave devices through rule actions.
|
||||
* It implements the {@link ThingActions} interface and provides methods forn
|
||||
* inclusion, exclusion, and sending multicast commands to Z-Wave nodes.
|
||||
*
|
||||
* @author Leo Siepel - Initial contribution
|
||||
*/
|
||||
@Component(scope = ServiceScope.PROTOTYPE, service = ZwaveJSActions.class)
|
||||
@ThingActionsScope(name = "zwavejs")
|
||||
@NonNullByDefault
|
||||
public class ZwaveJSActions implements ThingActions {
|
||||
private final Logger logger = LoggerFactory.getLogger(ZwaveJSActions.class);
|
||||
private static final ScheduledExecutorService SCHEDULER = ThreadPoolManager
|
||||
.getScheduledPool(BindingConstants.BINDING_ID);
|
||||
private static final int INCLUSION_EXCLUSION_TIMEOUT_SECONDS = 30;
|
||||
private @Nullable ZwaveJSBridgeHandler handler;
|
||||
|
||||
@RuleAction(label = "@text/action.start-inclusion.label", description = "@text/action.start-inclusion.description")
|
||||
public void startInclusion() {
|
||||
ZwaveJSBridgeHandler localHandler = handler;
|
||||
if (localHandler != null) {
|
||||
logger.debug("Inclusion action issued");
|
||||
localHandler.startInclusion();
|
||||
SCHEDULER.schedule(() -> {
|
||||
localHandler.stopInclusion();
|
||||
}, INCLUSION_EXCLUSION_TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||
}
|
||||
}
|
||||
|
||||
public static void startInclusion(ThingActions actions) {
|
||||
((ZwaveJSActions) actions).startInclusion();
|
||||
}
|
||||
|
||||
@RuleAction(label = "@text/action.start-exclusion.label", description = "@text/action.start-exclusion.description")
|
||||
public void startExclusion() {
|
||||
ZwaveJSBridgeHandler localHandler = handler;
|
||||
if (localHandler != null) {
|
||||
logger.debug("Exclusion action issued");
|
||||
localHandler.startExclusion();
|
||||
SCHEDULER.schedule(() -> {
|
||||
localHandler.stopExclusion();
|
||||
}, INCLUSION_EXCLUSION_TIMEOUT_SECONDS, java.util.concurrent.TimeUnit.SECONDS);
|
||||
}
|
||||
}
|
||||
|
||||
public static void startExclusion(ThingActions actions) {
|
||||
((ZwaveJSActions) actions).startExclusion();
|
||||
}
|
||||
|
||||
@RuleAction(label = "@text/action.send-multicast-command.label", description = "@text/action.send-multicast-command.description")
|
||||
public void sendMulticastCommand(
|
||||
@ActionInput(name = "nodeIDs", label = "node IDs", description = "Comma separated list of Z-Wave node IDs") String nodeIDs,
|
||||
@ActionInput(name = "commandClass", label = "command class", description = "Z-Wave command class") Integer commandClass,
|
||||
@ActionInput(name = "endpoint", label = "endpoint", description = "Z-Wave endpoint") Integer endpoint,
|
||||
@ActionInput(name = "property", label = "property", description = "Z-Wave write property") String property,
|
||||
@ActionInput(name = "value", label = "value", description = "Value to write") String value) {
|
||||
ZwaveJSBridgeHandler localHandler = handler;
|
||||
if (localHandler != null) {
|
||||
logger.debug("Multicast action issued");
|
||||
localHandler.sendMulticastCommand(nodeIDs, commandClass, endpoint, property, value);
|
||||
}
|
||||
}
|
||||
|
||||
public static void sendMulticastCommand(ThingActions actions, String nodeIDs, Integer commandClass,
|
||||
Integer endpoint, String property, String value) {
|
||||
((ZwaveJSActions) actions).sendMulticastCommand(nodeIDs, commandClass, endpoint, property, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setThingHandler(@Nullable ThingHandler handler) {
|
||||
if (handler instanceof ZwaveJSBridgeHandler bridgeHandler) {
|
||||
this.handler = bridgeHandler;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable ThingHandler getThingHandler() {
|
||||
return handler;
|
||||
}
|
||||
}
|
||||
+403
@@ -0,0 +1,403 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2025 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.zwavejs.internal.api;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Instant;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CopyOnWriteArraySet;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import javax.naming.CommunicationException;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.eclipse.jetty.websocket.api.RemoteEndpoint;
|
||||
import org.eclipse.jetty.websocket.api.Session;
|
||||
import org.eclipse.jetty.websocket.api.StatusCode;
|
||||
import org.eclipse.jetty.websocket.api.WebSocketListener;
|
||||
import org.eclipse.jetty.websocket.api.WebSocketPolicy;
|
||||
import org.eclipse.jetty.websocket.client.WebSocketClient;
|
||||
import org.openhab.binding.zwavejs.internal.BindingConstants;
|
||||
import org.openhab.binding.zwavejs.internal.api.adapter.InstantAdapter;
|
||||
import org.openhab.binding.zwavejs.internal.api.dto.commands.BaseCommand;
|
||||
import org.openhab.binding.zwavejs.internal.api.dto.commands.ServerInitializeCommand;
|
||||
import org.openhab.binding.zwavejs.internal.api.dto.commands.ServerListeningCommand;
|
||||
import org.openhab.binding.zwavejs.internal.api.dto.messages.BaseMessage;
|
||||
import org.openhab.binding.zwavejs.internal.api.dto.messages.EventMessage;
|
||||
import org.openhab.binding.zwavejs.internal.api.dto.messages.ResultMessage;
|
||||
import org.openhab.binding.zwavejs.internal.api.dto.messages.VersionMessage;
|
||||
import org.openhab.binding.zwavejs.internal.handler.ZwaveEventListener;
|
||||
import org.openhab.core.common.ThreadPoolManager;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.JsonParseException;
|
||||
import com.google.gson.ToNumberPolicy;
|
||||
import com.google.gson.typeadapters.RuntimeTypeAdapterFactory;
|
||||
|
||||
/**
|
||||
* The {@code ZWaveJSClient} class is responsible for managing the WebSocket connection
|
||||
* to the Z-Wave JS Webservice. It implements the {@link WebSocketListener} interface to
|
||||
* handle WebSocket events such as connection, disconnection, and message reception.
|
||||
*
|
||||
* <p>
|
||||
* This class provides methods to start and stop the WebSocket connection, as well as
|
||||
* to add and remove event listeners for Z-Wave events. It also includes functionality
|
||||
* to send commands to the Z-Wave JS server.
|
||||
*
|
||||
* <p>
|
||||
* Thread Safety: This class is thread-safe. It uses a {@link CopyOnWriteArraySet} for
|
||||
* managing event listeners and ensures that the WebSocket session is accessed in a
|
||||
* thread-safe manner.
|
||||
*
|
||||
* @see WebSocketListener
|
||||
* @see WebSocketClient
|
||||
* @see BaseMessage
|
||||
* @see BaseCommand
|
||||
*
|
||||
* @author Leo Siepel - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class ZWaveJSClient implements WebSocketListener {
|
||||
|
||||
private Logger logger = LoggerFactory.getLogger(ZWaveJSClient.class);
|
||||
private int bufferSize = 1048576 * 2; // 2 MiB
|
||||
private static final int RECONNECT_INTERVAL_MINUTES = 2;
|
||||
private static final String BINDING_SHUTDOWN_MESSAGE = "Binding shutdown";
|
||||
|
||||
private final WebSocketClient wsClient;
|
||||
private final ScheduledExecutorService scheduler = ThreadPoolManager.getScheduledPool(BindingConstants.BINDING_ID);
|
||||
private volatile @Nullable Session session;
|
||||
private final Set<ZwaveEventListener> listeners = new CopyOnWriteArraySet<>();
|
||||
private @Nullable Future<?> sessionFuture;
|
||||
private @Nullable ScheduledFuture<?> keepAliveFuture;
|
||||
private @Nullable ScheduledFuture<?> reconnectFuture;
|
||||
private final Gson gson;
|
||||
private final Object sendLock = new Object();
|
||||
private String uri = "";
|
||||
|
||||
public ZWaveJSClient(WebSocketClient wsClient) {
|
||||
this.wsClient = wsClient;
|
||||
RuntimeTypeAdapterFactory<BaseMessage> typeAdapterFactory = RuntimeTypeAdapterFactory.of(BaseMessage.class,
|
||||
"type", true);
|
||||
typeAdapterFactory.registerSubtype(VersionMessage.class, "version")
|
||||
.registerSubtype(ResultMessage.class, "result").registerSubtype(EventMessage.class, "event");
|
||||
|
||||
this.gson = new GsonBuilder().setObjectToNumberStrategy(ToNumberPolicy.LONG_OR_DOUBLE)
|
||||
.registerTypeAdapter(Instant.class, new InstantAdapter()).registerTypeAdapterFactory(typeAdapterFactory)
|
||||
.create();
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the WebSocket connection to the Z-Wave JS Webservice.
|
||||
*
|
||||
* @param uri the URI of the WebSocket server
|
||||
*
|
||||
* @throws CommunicationException if there is an error during communication
|
||||
*
|
||||
* @throws InterruptedException if the thread is interrupted
|
||||
*/
|
||||
public void start(String uri) throws CommunicationException, InterruptedException {
|
||||
logger.debug("Connecting to Z-Wave JS Webservice");
|
||||
this.uri = uri;
|
||||
try {
|
||||
sessionFuture = wsClient.connect(this, new URI(uri));
|
||||
} catch (IOException | URISyntaxException e) {
|
||||
throw new CommunicationException("Failed to connect to Z-Wave JS Webservice: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the WebSocket connection to the Z-Wave JS Webservice.
|
||||
*/
|
||||
public void stop() {
|
||||
logger.debug("Disconnecting from Z-Wave JS Webservice");
|
||||
Session localSession = this.session;
|
||||
if (localSession != null) {
|
||||
stopKeepAlive();
|
||||
try {
|
||||
localSession.close(StatusCode.NORMAL, BINDING_SHUTDOWN_MESSAGE);
|
||||
} catch (Exception e) {
|
||||
logger.debug("Error while closing websocket communication: {} ({})", e.getClass().getName(),
|
||||
e.getMessage());
|
||||
}
|
||||
session = null;
|
||||
}
|
||||
|
||||
Future<?> localSessionFuture = sessionFuture;
|
||||
if (localSessionFuture != null) {
|
||||
localSessionFuture.cancel(true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a Z-Wave event listener to the list of listeners.
|
||||
*
|
||||
* @param listener the Z-Wave event listener to be added
|
||||
*/
|
||||
public void addEventListener(ZwaveEventListener listener) {
|
||||
listeners.add(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the specified event listener from the list of listeners.
|
||||
*
|
||||
* @param listener the event listener to be removed
|
||||
*/
|
||||
public void removeEventListener(ZwaveEventListener listener) {
|
||||
listeners.remove(listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onWebSocketClose(int statusCode, @NonNullByDefault({}) String reason) {
|
||||
logger.debug("onWebSocketClose({}, '{}')", statusCode, reason);
|
||||
|
||||
try {
|
||||
for (ZwaveEventListener listener : listeners) {
|
||||
listener.onConnectionError(String.format("Connection closed: %d, %s", statusCode, reason));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.warn("Error invoking event listener on close", e);
|
||||
}
|
||||
stopKeepAlive();
|
||||
session = null;
|
||||
sessionFuture = null;
|
||||
if (statusCode == StatusCode.NORMAL && BINDING_SHUTDOWN_MESSAGE.equals(reason)) {
|
||||
logger.debug("Z-Wave JS Webservice closed normally");
|
||||
return;
|
||||
}
|
||||
scheduleReconnect();
|
||||
}
|
||||
|
||||
private void scheduleReconnect() {
|
||||
ScheduledFuture<?> reconnectFuture = this.reconnectFuture;
|
||||
if (reconnectFuture != null) {
|
||||
return;
|
||||
}
|
||||
logger.info("Scheduling reconnect to Z-Wave JS Webservice every {} minutes", RECONNECT_INTERVAL_MINUTES);
|
||||
this.reconnectFuture = scheduler.scheduleWithFixedDelay(() -> {
|
||||
try {
|
||||
start(this.uri);
|
||||
ScheduledFuture<?> future = this.reconnectFuture;
|
||||
if (future != null) {
|
||||
future.cancel(false);
|
||||
this.reconnectFuture = null;
|
||||
}
|
||||
} catch (CommunicationException | InterruptedException e) {
|
||||
// silently ignore as the thing state is already updated when the connection is lost
|
||||
logger.debug("Error while reconnecting to Z-Wave JS Webservice: {}", e.getMessage());
|
||||
}
|
||||
}, RECONNECT_INTERVAL_MINUTES, RECONNECT_INTERVAL_MINUTES, TimeUnit.MINUTES);
|
||||
}
|
||||
|
||||
private void stopKeepAlive() {
|
||||
ScheduledFuture<?> keepAliveFuture = this.keepAliveFuture;
|
||||
if (keepAliveFuture != null) {
|
||||
keepAliveFuture.cancel(true);
|
||||
}
|
||||
this.keepAliveFuture = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onWebSocketConnect(@NonNullByDefault({}) Session session) {
|
||||
logger.debug("onWebSocketConnect('{}')", session);
|
||||
this.session = session;
|
||||
if (session != null) {
|
||||
final WebSocketPolicy currentPolicy = session.getPolicy();
|
||||
currentPolicy.setInputBufferSize(bufferSize);
|
||||
currentPolicy.setMaxTextMessageSize(bufferSize);
|
||||
currentPolicy.setMaxBinaryMessageSize(bufferSize);
|
||||
keepAliveFuture = scheduler.scheduleWithFixedDelay(() -> {
|
||||
try {
|
||||
String data = "Ping";
|
||||
ByteBuffer payload = ByteBuffer.wrap(data.getBytes(StandardCharsets.UTF_8));
|
||||
session.getRemote().sendPing(payload);
|
||||
} catch (IOException e) {
|
||||
logger.warn("Problem starting periodic Ping. {}", e.getMessage());
|
||||
}
|
||||
}, 25, 25, TimeUnit.SECONDS);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onWebSocketError(@NonNullByDefault({}) Throwable cause) {
|
||||
Throwable localThrowable = (cause != null) ? cause
|
||||
: new IllegalStateException("Null Exception passed to onWebSocketError");
|
||||
logger.debug("Error during websocket communication: {}", localThrowable.getMessage());
|
||||
|
||||
Session localSession = session;
|
||||
if (localSession != null) {
|
||||
stopKeepAlive();
|
||||
try {
|
||||
// Close the session with an error status code
|
||||
localSession.close(StatusCode.SERVER_ERROR, "Error: " + localThrowable.getMessage());
|
||||
} catch (Exception e) {
|
||||
logger.warn("Error while closing websocket session: {}", e.getMessage());
|
||||
}
|
||||
|
||||
session = null;
|
||||
}
|
||||
|
||||
notifyListenersOnError(String.format("Error: %s", localThrowable.getMessage()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onWebSocketBinary(@NonNullByDefault({}) byte[] payload, int offset, int len) {
|
||||
throw new UnsupportedOperationException("Unimplemented method 'onWebSocketBinary'");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onWebSocketText(@NonNullByDefault({}) String message) {
|
||||
if (message.contains("\"event\":\"statistics updated\"")) {
|
||||
return;
|
||||
}
|
||||
|
||||
BaseMessage baseEvent = null;
|
||||
try {
|
||||
baseEvent = gson.fromJson(message, BaseMessage.class);
|
||||
} catch (JsonParseException ex) {
|
||||
logger.warn("Failed to parse incoming WebSocket message: {}", ex.getMessage());
|
||||
logger.trace("RECV | {}", message);
|
||||
notifyListenersOnError("Failed to parse message: " + ex.getMessage());
|
||||
return;
|
||||
}
|
||||
|
||||
if (baseEvent == null || baseEvent.type == null) {
|
||||
logger.warn("Received event with unknown or null type.");
|
||||
logger.trace("RECV | {}", message);
|
||||
notifyListenersOnError("Received event with unknown or null type.");
|
||||
return;
|
||||
}
|
||||
|
||||
logEventResponse(baseEvent, message);
|
||||
|
||||
// Notify listeners
|
||||
try {
|
||||
for (ZwaveEventListener listener : listeners) {
|
||||
listener.onEvent(baseEvent);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Error invoking event listener on websockettext: {}", e.toString());
|
||||
} else {
|
||||
logger.warn("Error invoking event listener on websockettext");
|
||||
}
|
||||
}
|
||||
|
||||
// Special handling for VersionMessage
|
||||
if (baseEvent instanceof VersionMessage) {
|
||||
// the binding is starting up, perform schema version handshake
|
||||
// also start listening to events
|
||||
sendCommand(new ServerInitializeCommand());
|
||||
sendCommand(new ServerListeningCommand());
|
||||
}
|
||||
}
|
||||
|
||||
private void logEventResponse(BaseMessage baseEvent, String message) {
|
||||
if (baseEvent instanceof ResultMessage resultMessage) {
|
||||
if (resultMessage.success && (resultMessage.result != null && resultMessage.result.status != 5)) {
|
||||
logger.debug("Received ResultMessage type: {}, success: {}", baseEvent.type, resultMessage.success);
|
||||
} else {
|
||||
logger.warn("Received ResultMessage type: {}, success: {}, status: {}, error_code: {}, message: {}",
|
||||
baseEvent.type, resultMessage.success,
|
||||
resultMessage.result != null ? resultMessage.result.status : "null", resultMessage.errorCode,
|
||||
resultMessage.message != null ? resultMessage.message : resultMessage.zwaveErrorMessage);
|
||||
}
|
||||
} else if (baseEvent instanceof EventMessage eventMessage) {
|
||||
logger.trace("Received EventMessage, type: {}", eventMessage.event.event);
|
||||
} else {
|
||||
logger.trace("Received message class type: {}, event type: {}", baseEvent.getClass().getSimpleName(),
|
||||
baseEvent.type);
|
||||
}
|
||||
logger.trace("RECV | {}", message);
|
||||
}
|
||||
|
||||
private void notifyListenersOnError(String errorMsg) {
|
||||
for (ZwaveEventListener listener : listeners) {
|
||||
try {
|
||||
listener.onConnectionError(errorMsg);
|
||||
} catch (Exception e) {
|
||||
logger.warn("Error invoking event listener on error", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a command to the remote endpoint in JSON format.
|
||||
* Converts the given {@link BaseCommand} object to a JSON string and sends it
|
||||
* using the active session's remote endpoint.
|
||||
*
|
||||
* @param command The {@link BaseCommand} object to be sent.
|
||||
*/
|
||||
public void sendCommand(BaseCommand command) {
|
||||
String commandAsJson = gson.toJson(command);
|
||||
Session session = this.session;
|
||||
try {
|
||||
if (session == null || !(session.getRemote() instanceof RemoteEndpoint endpoint)) {
|
||||
logger.warn("Failed while sending command: {}. Problem with session or remote endpoint",
|
||||
command.getClass());
|
||||
return;
|
||||
}
|
||||
logger.debug("Sending command: {}.", command.getClass().getSimpleName());
|
||||
logger.debug("SEND | {}", commandAsJson);
|
||||
synchronized (sendLock) {
|
||||
endpoint.sendString(commandAsJson);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
logger.warn("IOException while sending command: {}. Error {}", command.getClass().getSimpleName(),
|
||||
e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public void setBufferSize(int maxMessageSize) {
|
||||
bufferSize = maxMessageSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* Disposes the client by clearing the connection and scheduled futures.
|
||||
*/
|
||||
public void dispose() {
|
||||
logger.debug("Disposing ZWaveJSClient");
|
||||
stop(); // Ensure the connection is stopped
|
||||
|
||||
ScheduledFuture<?> localReconnectFuture = reconnectFuture;
|
||||
if (localReconnectFuture != null) {
|
||||
localReconnectFuture.cancel(true);
|
||||
reconnectFuture = null;
|
||||
}
|
||||
|
||||
stopKeepAlive();
|
||||
|
||||
Future<?> localSessionFuture = sessionFuture;
|
||||
if (localSessionFuture != null) {
|
||||
if (!localSessionFuture.isDone()) {
|
||||
localSessionFuture.cancel(true);
|
||||
}
|
||||
sessionFuture = null;
|
||||
}
|
||||
|
||||
listeners.clear();
|
||||
logger.debug("ZWaveJSClient disposed");
|
||||
}
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2025 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.zwavejs.internal.api.adapter;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.time.Instant;
|
||||
import java.time.format.DateTimeParseException;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
|
||||
import com.google.gson.TypeAdapter;
|
||||
import com.google.gson.stream.JsonReader;
|
||||
import com.google.gson.stream.JsonWriter;
|
||||
|
||||
/**
|
||||
* * The {@link InstantAdapter} is a custom Gson TypeAdapter for serializing and deserializing an Instant object.
|
||||
*
|
||||
* @author Leo Siepel - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class InstantAdapter extends TypeAdapter<@Nullable Instant> {
|
||||
@Override
|
||||
public void write(@Nullable JsonWriter out, @Nullable Instant value) throws IOException {
|
||||
if (out == null) {
|
||||
return;
|
||||
}
|
||||
out.value(value == null ? null : value.toString());
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable Instant read(@Nullable JsonReader in) throws IOException {
|
||||
if (in == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return Instant.parse(in.nextString());
|
||||
} catch (DateTimeParseException e) {
|
||||
// If the string cannot be parsed as an Instant, return null
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2025 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.zwavejs.internal.api.dto;
|
||||
|
||||
/**
|
||||
* @author Leo Siepel - Initial contribution
|
||||
*/
|
||||
public class Args {
|
||||
public String commandClassName;
|
||||
public int commandClass;
|
||||
public String property;
|
||||
public int endpoint;
|
||||
public Object newValue;
|
||||
public Object prevValue;
|
||||
public String propertyName;
|
||||
public Object propertyKey;
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2025 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.zwavejs.internal.api.dto;
|
||||
|
||||
/**
|
||||
* @author Leo Siepel - Initial contribution
|
||||
*/
|
||||
public class BackgroundRSSI {
|
||||
public ChannelRSSI channel0;
|
||||
public ChannelRSSI channel1;
|
||||
public ChannelRSSI channel2;
|
||||
public ChannelRSSI channel3;
|
||||
public long timestamp;
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2025 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.zwavejs.internal.api.dto;
|
||||
|
||||
/**
|
||||
* @author Leo Siepel - Initial contribution
|
||||
*/
|
||||
public class Basic {
|
||||
public int key;
|
||||
public String label;
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2025 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.zwavejs.internal.api.dto;
|
||||
|
||||
/**
|
||||
* @author Leo Siepel - Initial contribution
|
||||
*/
|
||||
public class CcSpecific {
|
||||
public int meterType;
|
||||
public int scale;
|
||||
public int rateType;
|
||||
public int notificationType;
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2025 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.zwavejs.internal.api.dto;
|
||||
|
||||
/**
|
||||
* @author Leo Siepel - Initial contribution
|
||||
*/
|
||||
public class ChannelRSSI {
|
||||
public int current;
|
||||
public int average;
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2025 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.zwavejs.internal.api.dto;
|
||||
|
||||
/**
|
||||
* @author Leo Siepel - Initial contribution
|
||||
*/
|
||||
public class CommandClass {
|
||||
public int id;
|
||||
public String name;
|
||||
public int version;
|
||||
public boolean isSecure;
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2025 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.zwavejs.internal.api.dto;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author Leo Siepel - Initial contribution
|
||||
*/
|
||||
public class Controller {
|
||||
public int type;
|
||||
public long homeId;
|
||||
public int ownNodeId;
|
||||
public boolean isUsingHomeIdFromOtherNetwork;
|
||||
public boolean isSISPresent;
|
||||
public boolean wasRealPrimary;
|
||||
public int manufacturerId;
|
||||
public int productType;
|
||||
public int productId;
|
||||
public List<Integer> supportedFunctionTypes;
|
||||
public int sucNodeId;
|
||||
public boolean supportsTimers;
|
||||
public Statistics statistics;
|
||||
public int inclusionState;
|
||||
public String sdkVersion;
|
||||
public String firmwareVersion;
|
||||
public boolean isPrimary;
|
||||
public boolean isSUC;
|
||||
public int nodeType;
|
||||
public int rfRegion;
|
||||
public int status;
|
||||
public boolean isRebuildingRoutes;
|
||||
public boolean supportsLongRange;
|
||||
public int maxLongRangePowerlevel;
|
||||
public int longRangeChannel;
|
||||
public boolean supportsLongRangeAutoChannelSelection;
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2025 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.zwavejs.internal.api.dto;
|
||||
|
||||
/**
|
||||
* @author Leo Siepel - Initial contribution
|
||||
*/
|
||||
public class Device {
|
||||
public int productType;
|
||||
public int productId;
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2025 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.zwavejs.internal.api.dto;
|
||||
|
||||
/**
|
||||
* @author Leo Siepel - Initial contribution
|
||||
*/
|
||||
public class DeviceClass {
|
||||
public Basic basic;
|
||||
public Generic generic;
|
||||
public Specific specific;
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2025 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.zwavejs.internal.api.dto;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author Leo Siepel - Initial contribution
|
||||
*/
|
||||
public class DeviceConfig {
|
||||
public String filename;
|
||||
public boolean isEmbedded;
|
||||
public String manufacturer;
|
||||
public int manufacturerId;
|
||||
public String label;
|
||||
public String description;
|
||||
public List<Device> devices;
|
||||
public FirmwareVersion firmwareVersion;
|
||||
public boolean preferred;
|
||||
public Metadata metadata;
|
||||
public ParamInformation paramInformation;
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2025 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.zwavejs.internal.api.dto;
|
||||
|
||||
/**
|
||||
* @author Leo Siepel - Initial contribution
|
||||
*/
|
||||
public class Driver {
|
||||
public LogConfig logConfig;
|
||||
public boolean statisticsEnabled;
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2025 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.zwavejs.internal.api.dto;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author Leo Siepel - Initial contribution
|
||||
*/
|
||||
public class Endpoint {
|
||||
public int nodeId;
|
||||
public int index;
|
||||
public DeviceClass deviceClass;
|
||||
public List<CommandClass> commandClasses;
|
||||
public int installerIcon;
|
||||
public int userIcon;
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2025 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.zwavejs.internal.api.dto;
|
||||
|
||||
/**
|
||||
* @author Leo Siepel - Initial contribution
|
||||
*/
|
||||
public class Event {
|
||||
public Source source;
|
||||
public String event;
|
||||
public int nodeId;
|
||||
public Args args;
|
||||
|
||||
enum Source {
|
||||
driver,
|
||||
controller,
|
||||
node,
|
||||
zniffer
|
||||
}
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2025 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.zwavejs.internal.api.dto;
|
||||
|
||||
/**
|
||||
* @author Leo Siepel - Initial contribution
|
||||
*/
|
||||
public class FirmwareVersion {
|
||||
public String min;
|
||||
public String max;
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2025 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.zwavejs.internal.api.dto;
|
||||
|
||||
/**
|
||||
* @author Leo Siepel - Initial contribution
|
||||
*/
|
||||
public class Generic {
|
||||
public int key;
|
||||
public String label;
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2025 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.zwavejs.internal.api.dto;
|
||||
|
||||
/**
|
||||
* @author Leo Siepel - Initial contribution
|
||||
*/
|
||||
public class LogConfig {
|
||||
public boolean enabled;
|
||||
public String level;
|
||||
public boolean logToFile;
|
||||
public int maxFiles;
|
||||
public String filename;
|
||||
public boolean forceConsole;
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2025 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.zwavejs.internal.api.dto;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author Leo Siepel - Initial contribution
|
||||
*/
|
||||
public class Lwr {
|
||||
public int protocolDataRate;
|
||||
public List<Object> repeaters;
|
||||
public int rssi;
|
||||
public List<Object> repeaterRSSI;
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2025 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.zwavejs.internal.api.dto;
|
||||
|
||||
/**
|
||||
* @author Leo Siepel - Initial contribution
|
||||
*/
|
||||
public class Map {
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2025 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.zwavejs.internal.api.dto;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
/**
|
||||
* @author Leo Siepel - Initial contribution
|
||||
*/
|
||||
public class Metadata {
|
||||
public Object comments;
|
||||
public MetadataType type;
|
||||
public boolean readable;
|
||||
public boolean writeable;
|
||||
public String label;
|
||||
public List<String> valueChangeOptions;
|
||||
public boolean stateful;
|
||||
public boolean secret;
|
||||
public CcSpecific ccSpecific;
|
||||
public String unit;
|
||||
public Map<String, String> states;
|
||||
@SerializedName("default")
|
||||
public Object defaultValue;
|
||||
public Integer min;
|
||||
public Long max;
|
||||
public Integer steps;
|
||||
public int valueSize;
|
||||
public int format;
|
||||
public boolean allowManualEntry;
|
||||
public boolean isFromConfig;
|
||||
public String description;
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2025 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.zwavejs.internal.api.dto;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
/**
|
||||
* @author Leo Siepel - Initial contribution
|
||||
*/
|
||||
public enum MetadataType {
|
||||
@SerializedName("any")
|
||||
ANY,
|
||||
@SerializedName("buffer")
|
||||
BUFFER,
|
||||
@SerializedName("color")
|
||||
COLOR,
|
||||
@SerializedName("duration")
|
||||
DURATION,
|
||||
@SerializedName("string")
|
||||
STRING,
|
||||
@SerializedName("string[]")
|
||||
STRING_ARRAY,
|
||||
@SerializedName("number")
|
||||
NUMBER,
|
||||
@SerializedName("boolean")
|
||||
BOOLEAN
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2025 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.zwavejs.internal.api.dto;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author Leo Siepel - Initial contribution
|
||||
*/
|
||||
public class Node {
|
||||
public int nodeId;
|
||||
public int index;
|
||||
public Status status;
|
||||
public boolean ready;
|
||||
public boolean isListening;
|
||||
public boolean isRouting;
|
||||
public int manufacturerId;
|
||||
public int productId;
|
||||
public int productType;
|
||||
public String firmwareVersion;
|
||||
public DeviceConfig deviceConfig;
|
||||
public String label;
|
||||
public int interviewAttempts;
|
||||
public List<Endpoint> endpoints;
|
||||
public List<Value> values;
|
||||
public boolean isFrequentListening;
|
||||
public int maxDataRate;
|
||||
public List<Integer> supportedDataRates;
|
||||
public int protocolVersion;
|
||||
public boolean supportsBeaming;
|
||||
public boolean supportsSecurity;
|
||||
public DeviceClass deviceClass;
|
||||
public String interviewStage;
|
||||
public String deviceDatabaseUrl;
|
||||
public Statistics statistics;
|
||||
public boolean isControllerNode;
|
||||
public boolean keepAwake;
|
||||
public int protocol;
|
||||
public int installerIcon;
|
||||
public int userIcon;
|
||||
public boolean isSecure;
|
||||
public int zwavePlusVersion;
|
||||
public int nodeType;
|
||||
public int zwavePlusNodeType;
|
||||
public int zwavePlusRoleType;
|
||||
public int highestSecurityClass;
|
||||
public Instant lastSeen;
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2025 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.zwavejs.internal.api.dto;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
/**
|
||||
* @author Leo Siepel - Initial contribution
|
||||
*/
|
||||
public class ParamInformation {
|
||||
@SerializedName("_map")
|
||||
public Map map;
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2025 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.zwavejs.internal.api.dto;
|
||||
|
||||
/**
|
||||
* @author Leo Siepel - Initial contribution
|
||||
*/
|
||||
public class Result {
|
||||
public State state;
|
||||
public int status;
|
||||
public String message;
|
||||
public Object value;
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2025 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.zwavejs.internal.api.dto;
|
||||
|
||||
/**
|
||||
* @author Leo Siepel - Initial contribution
|
||||
*/
|
||||
public class Root {
|
||||
public String type;
|
||||
public boolean success;
|
||||
public Result result;
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2025 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.zwavejs.internal.api.dto;
|
||||
|
||||
/**
|
||||
* @author Leo Siepel - Initial contribution
|
||||
*/
|
||||
public class Specific {
|
||||
public int key;
|
||||
public String label;
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2025 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.zwavejs.internal.api.dto;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author Leo Siepel - Initial contribution
|
||||
*/
|
||||
public class State {
|
||||
public Driver driver;
|
||||
public Controller controller;
|
||||
public List<Node> nodes;
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2025 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.zwavejs.internal.api.dto;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
/**
|
||||
* @author Leo Siepel - Initial contribution
|
||||
*/
|
||||
public class Statistics {
|
||||
public int messagesTX;
|
||||
public int messagesRX;
|
||||
public int messagesDroppedRX;
|
||||
@SerializedName("NAK")
|
||||
public int nAK;
|
||||
@SerializedName("CAN")
|
||||
public int cAN;
|
||||
public int timeoutACK;
|
||||
public int timeoutResponse;
|
||||
public int timeoutCallback;
|
||||
public int messagesDroppedTX;
|
||||
public BackgroundRSSI backgroundRSSI;
|
||||
public int commandsTX;
|
||||
public int commandsRX;
|
||||
public int commandsDroppedRX;
|
||||
public int commandsDroppedTX;
|
||||
public double rtt;
|
||||
public Instant lastSeen;
|
||||
public int rssi;
|
||||
public Lwr lwr;
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2025 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.zwavejs.internal.api.dto;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
/**
|
||||
* @author Leo Siepel - Initial contribution
|
||||
*/
|
||||
public enum Status {
|
||||
@SerializedName("0")
|
||||
UNKNOWN,
|
||||
|
||||
@SerializedName("1")
|
||||
ASLEEP,
|
||||
|
||||
@SerializedName("2")
|
||||
AWAKE,
|
||||
|
||||
@SerializedName("3")
|
||||
DEAD,
|
||||
|
||||
@SerializedName("4")
|
||||
ALIVE,
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2025 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.zwavejs.internal.api.dto;
|
||||
|
||||
/**
|
||||
* @author Leo Siepel - Initial contribution
|
||||
*/
|
||||
public class Value {
|
||||
public int endpoint;
|
||||
public int commandClass;
|
||||
public String commandClassName;
|
||||
public Object property;
|
||||
public String propertyName;
|
||||
public int ccVersion;
|
||||
public Metadata metadata;
|
||||
public Object value;
|
||||
public Object propertyKey;
|
||||
public String propertyKeyName;
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2025 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.zwavejs.internal.api.dto;
|
||||
|
||||
/**
|
||||
* @author Leo Siepel - Initial contribution
|
||||
*/
|
||||
public class ValueId {
|
||||
public Object commandClass;
|
||||
public Integer endpoint;
|
||||
public Object property;
|
||||
public Object propertyKey;
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2025 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.zwavejs.internal.api.dto.commands;
|
||||
|
||||
/**
|
||||
* @author Leo Siepel - Initial contribution
|
||||
*/
|
||||
public abstract class BaseCommand {
|
||||
String messageId;
|
||||
String command;
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2025 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.zwavejs.internal.api.dto.commands;
|
||||
|
||||
/**
|
||||
* @author Leo Siepel - Initial contribution
|
||||
*/
|
||||
public class ControllerExclusionCommand extends BaseCommand {
|
||||
|
||||
public ControllerExclusionCommand(boolean stop) {
|
||||
command = stop ? "controller.stop_exclusion" : "controller.begin_exclusion";
|
||||
}
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2025 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.zwavejs.internal.api.dto.commands;
|
||||
|
||||
/**
|
||||
* @author Leo Siepel - Initial contribution
|
||||
*/
|
||||
public class ControllerInclusionCommand extends BaseCommand {
|
||||
|
||||
public ControllerInclusionCommand(boolean stop) {
|
||||
command = stop ? "controller.stop_inclusion" : "controller.begin_inclusion";
|
||||
}
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2025 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.zwavejs.internal.api.dto.commands;
|
||||
|
||||
/**
|
||||
* @author Leo Siepel - Initial contribution
|
||||
*/
|
||||
public class DriverStatisticsCommand extends BaseCommand {
|
||||
|
||||
public DriverStatisticsCommand(boolean enable) {
|
||||
command = enable ? "driver.enable_statistics" : "driver.disable_statistics";
|
||||
}
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2025 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.zwavejs.internal.api.dto.commands;
|
||||
|
||||
import org.openhab.binding.zwavejs.internal.api.dto.ValueId;
|
||||
|
||||
/**
|
||||
* @author Garrett Scoville - Initial contribution
|
||||
*/
|
||||
public class MulticastSetValueCommand extends BaseCommand {
|
||||
public int[] nodeIDs;
|
||||
public ValueId valueId;
|
||||
public Object value;
|
||||
|
||||
public MulticastSetValueCommand(int[] nodeIDs, Object commandClass, Integer endpoint, String property,
|
||||
Object value) {
|
||||
command = "multicast_group.set_value";
|
||||
this.nodeIDs = nodeIDs;
|
||||
this.value = value;
|
||||
|
||||
this.valueId = new ValueId();
|
||||
this.valueId.commandClass = commandClass;
|
||||
this.valueId.endpoint = endpoint;
|
||||
this.valueId.property = property;
|
||||
}
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2025 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.zwavejs.internal.api.dto.commands;
|
||||
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
|
||||
import org.openhab.binding.zwavejs.internal.api.dto.ValueId;
|
||||
import org.openhab.binding.zwavejs.internal.config.ZwaveJSChannelConfiguration;
|
||||
|
||||
/**
|
||||
* @author Leo Siepel - Initial contribution
|
||||
*/
|
||||
public class NodeGetValueCommand extends BaseCommand {
|
||||
public int nodeId;
|
||||
public ValueId valueId;
|
||||
|
||||
public NodeGetValueCommand(int nodeId, ZwaveJSChannelConfiguration config) {
|
||||
command = "node.get_value";
|
||||
this.nodeId = nodeId;
|
||||
this.messageId = String.format("getvalue|%s|%s|%s|%s|%s|%s|%s", config.endpoint, config.commandClassId,
|
||||
config.commandClassName, config.propertyKeyInt != null ? config.propertyKeyInt : config.propertyKeyStr,
|
||||
config.readProperty, nodeId, ThreadLocalRandom.current().nextInt(1, 5001));
|
||||
|
||||
this.valueId = new ValueId();
|
||||
this.valueId.commandClass = config.commandClassId;
|
||||
this.valueId.endpoint = config.endpoint;
|
||||
this.valueId.propertyKey = config.propertyKeyInt != null ? config.propertyKeyInt : config.propertyKeyStr;
|
||||
this.valueId.property = config.readProperty;
|
||||
}
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2025 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.zwavejs.internal.api.dto.commands;
|
||||
|
||||
/**
|
||||
* @author Leo Siepel - Initial contribution
|
||||
*/
|
||||
public class NodeRefreshCommand extends BaseCommand {
|
||||
public int nodeId;
|
||||
|
||||
public NodeRefreshCommand(int nodeId) {
|
||||
command = "node.refresh_info";
|
||||
this.nodeId = nodeId;
|
||||
}
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2025 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.zwavejs.internal.api.dto.commands;
|
||||
|
||||
import org.openhab.binding.zwavejs.internal.api.dto.ValueId;
|
||||
import org.openhab.binding.zwavejs.internal.config.ZwaveJSChannelConfiguration;
|
||||
|
||||
/**
|
||||
* @author Leo Siepel - Initial contribution
|
||||
*/
|
||||
public class NodeSetValueCommand extends BaseCommand {
|
||||
public int nodeId;
|
||||
public ValueId valueId;
|
||||
public Object value;
|
||||
public Object options;
|
||||
|
||||
public NodeSetValueCommand(int nodeId, ZwaveJSChannelConfiguration config) {
|
||||
command = "node.set_value";
|
||||
this.nodeId = nodeId;
|
||||
|
||||
this.valueId = new ValueId();
|
||||
this.valueId.commandClass = config.commandClassId;
|
||||
this.valueId.endpoint = config.endpoint;
|
||||
this.valueId.propertyKey = config.propertyKeyInt != null ? config.propertyKeyInt : config.propertyKeyStr;
|
||||
this.valueId.property = config.writePropertyInt != null ? config.writePropertyInt : config.writePropertyStr;
|
||||
}
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2025 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.zwavejs.internal.api.dto.commands;
|
||||
|
||||
import org.osgi.framework.FrameworkUtil;
|
||||
|
||||
/**
|
||||
* @author Leo Siepel - Initial contribution
|
||||
*/
|
||||
public class ServerInitializeCommand extends BaseCommand {
|
||||
public int schemaVersion;
|
||||
public String additionalUserAgentComponents;
|
||||
|
||||
public ServerInitializeCommand() {
|
||||
command = "initialize";
|
||||
schemaVersion = 40;
|
||||
additionalUserAgentComponents = "openHAB/" + FrameworkUtil.getBundle(this.getClass()).getVersion().toString();
|
||||
}
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2025 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.zwavejs.internal.api.dto.commands;
|
||||
|
||||
/**
|
||||
* @author Leo Siepel - Initial contribution
|
||||
*/
|
||||
public class ServerListeningCommand extends BaseCommand {
|
||||
public ServerListeningCommand() {
|
||||
command = "start_listening";
|
||||
}
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2025 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.zwavejs.internal.api.dto.messages;
|
||||
|
||||
/**
|
||||
* @author Leo Siepel - Initial contribution
|
||||
*/
|
||||
|
||||
public class BaseMessage {
|
||||
public String messageId;
|
||||
public String type;
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2025 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.zwavejs.internal.api.dto.messages;
|
||||
|
||||
import org.openhab.binding.zwavejs.internal.api.dto.Event;
|
||||
|
||||
/**
|
||||
* @author Leo Siepel - Initial contribution
|
||||
*/
|
||||
public class EventMessage extends BaseMessage {
|
||||
public Event event;
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2025 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.zwavejs.internal.api.dto.messages;
|
||||
|
||||
import org.openhab.binding.zwavejs.internal.api.dto.Result;
|
||||
|
||||
/**
|
||||
* @author Leo Siepel - Initial contribution
|
||||
*/
|
||||
public class ResultMessage extends BaseMessage {
|
||||
public boolean success;
|
||||
public Result result;
|
||||
public String errorCode;
|
||||
public String message;
|
||||
public String zwaveErrorMessage;
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2025 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.zwavejs.internal.api.dto.messages;
|
||||
|
||||
/**
|
||||
* @author Leo Siepel - Initial contribution
|
||||
*/
|
||||
public class VersionMessage extends BaseMessage {
|
||||
public String driverVersion;
|
||||
public String serverVersion;
|
||||
public int homeId;
|
||||
public int minSchemaVersion;
|
||||
public int maxSchemaVersion;
|
||||
}
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2025 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.zwavejs.internal.api.exception;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* Exception thrown when there is a communication error with the Z-Wave JS Webservice.
|
||||
* This exception can be used to indicate various communication issues such as
|
||||
* connection failures, timeouts, and protocol errors.
|
||||
*
|
||||
* <p>
|
||||
* This class provides constructors to include a message, a cause, and options
|
||||
* to enable suppression and writable stack trace.
|
||||
*
|
||||
* @see Exception
|
||||
* @see Throwable
|
||||
*
|
||||
* @author Leo Siepel - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class CommunicationException extends Exception {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/*
|
||||
* Constructs a new CommunicationException with the specified detail message.
|
||||
*
|
||||
* @param message the detail message
|
||||
*/
|
||||
public CommunicationException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
/*
|
||||
* Constructs a new CommunicationException with the specified detail message and cause.
|
||||
*
|
||||
* @param message the detail message
|
||||
*
|
||||
* @param cause the cause of the exception
|
||||
*/
|
||||
public CommunicationException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
/*
|
||||
* Constructs a new CommunicationException with the specified cause.
|
||||
*
|
||||
* @param cause the cause of the exception
|
||||
*/
|
||||
public CommunicationException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2025 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.zwavejs.internal.config;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.core.library.types.DecimalType;
|
||||
import org.openhab.core.library.types.HSBType;
|
||||
import org.openhab.core.library.types.PercentType;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
|
||||
/**
|
||||
* A class encapsulating the color capability of a light endpoint
|
||||
*
|
||||
* @author Andrew Fiddian-Green - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class ColorCapability {
|
||||
public HSBType cachedColor = new HSBType(DecimalType.ZERO, PercentType.ZERO, PercentType.HUNDRED);
|
||||
public Number cachedWarmWhite = DecimalType.ZERO;
|
||||
public Number cachedColdWhite = DecimalType.ZERO;
|
||||
public Set<ChannelUID> colorChannels = new HashSet<>();
|
||||
public @Nullable ChannelUID dimmerChannel = null;
|
||||
public @Nullable ChannelUID colorTempChannel = null;
|
||||
public @Nullable ChannelUID warmWhiteChannel = null;
|
||||
public @Nullable ChannelUID coldWhiteChannel = null;
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ColorCapability [cachedColor=" + cachedColor + ", cachedWarmWhite=" + cachedWarmWhite
|
||||
+ ", cachedColdWhite=" + cachedColdWhite + ", colorChannels=" + colorChannels + ", dimmerChannel="
|
||||
+ dimmerChannel + ", colorTempChannel=" + colorTempChannel + ", warmWhiteChannel=" + warmWhiteChannel
|
||||
+ ", coldWhiteChannel=" + coldWhiteChannel + "]";
|
||||
}
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2025 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.zwavejs.internal.config;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* The {@link ZwaveJSBridgeConfiguration} class contains fields mapping bridge configuration parameters.
|
||||
*
|
||||
* @author Leo Siepel - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class ZwaveJSBridgeConfiguration {
|
||||
|
||||
public String hostname = "";
|
||||
public int port = 3000;
|
||||
public int maxMessageSize = 2097152;
|
||||
public boolean configurationChannels = false;
|
||||
|
||||
public boolean isValid() {
|
||||
return port > 0 && !hostname.isBlank();
|
||||
}
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2025 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.zwavejs.internal.config;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* The {@link ZwaveJSChannelConfiguration} class contains fields mapping channel configuration parameters.
|
||||
*
|
||||
* @author Leo Siepel - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class ZwaveJSChannelConfiguration {
|
||||
public @Nullable String incomingUnit;
|
||||
public int commandClassId = 0;
|
||||
public String commandClassName = "";
|
||||
public int endpoint = 0;
|
||||
public @Nullable Integer propertyKeyInt;
|
||||
public @Nullable String propertyKeyStr;
|
||||
public @Nullable String readProperty;
|
||||
public @Nullable Integer writePropertyInt;
|
||||
public @Nullable String writePropertyStr;
|
||||
public boolean inverted = false;
|
||||
public double factor = 1.0;
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2025 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.zwavejs.internal.config;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* The {@link ZwaveJSNodeConfiguration} class contains fields mapping thing configuration parameters.
|
||||
*
|
||||
* @author Leo Siepel - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class ZwaveJSNodeConfiguration {
|
||||
|
||||
public int id = 0;
|
||||
|
||||
public boolean isValid() {
|
||||
return id > 0;
|
||||
}
|
||||
}
|
||||
+604
@@ -0,0 +1,604 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2025 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.zwavejs.internal.conversion;
|
||||
|
||||
import static org.openhab.binding.zwavejs.internal.BindingConstants.*;
|
||||
import static org.openhab.binding.zwavejs.internal.CommandClassConstants.*;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.measure.Unit;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.zwavejs.internal.api.dto.Event;
|
||||
import org.openhab.binding.zwavejs.internal.api.dto.MetadataType;
|
||||
import org.openhab.binding.zwavejs.internal.api.dto.Value;
|
||||
import org.openhab.core.library.CoreItemFactory;
|
||||
import org.openhab.core.library.types.DecimalType;
|
||||
import org.openhab.core.library.types.HSBType;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.openhab.core.library.types.PercentType;
|
||||
import org.openhab.core.library.types.QuantityType;
|
||||
import org.openhab.core.library.types.StringType;
|
||||
import org.openhab.core.types.State;
|
||||
import org.openhab.core.types.StateDescriptionFragment;
|
||||
import org.openhab.core.types.StateDescriptionFragmentBuilder;
|
||||
import org.openhab.core.types.StateOption;
|
||||
import org.openhab.core.types.UnDefType;
|
||||
import org.openhab.core.types.util.UnitUtils;
|
||||
import org.openhab.core.util.ColorUtil;
|
||||
import org.openhab.core.util.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The {@link BaseMetadata} class represents basic metadata information for a Z-Wave node.
|
||||
* It contains various properties and methods to handle metadata and state information.
|
||||
*
|
||||
* @author Leo Siepel - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public abstract class BaseMetadata {
|
||||
|
||||
private Logger logger = LoggerFactory.getLogger(BaseMetadata.class);
|
||||
private static final String DEFAULT_LABEL = "Unknown Label";
|
||||
private static final Map<String, String> UNIT_REPLACEMENTS = Map.of("lux", "lx", //
|
||||
"Lux", "lx", //
|
||||
"KwH", "kWh", //
|
||||
"minutes", "min", //
|
||||
"Minutes", "min", //
|
||||
"seconds", "s", //
|
||||
"Seconds", "s", //
|
||||
"°(C/F)", "", // special case where Zwave JS sends °F/C as unit, but is actually dimensionless
|
||||
"°F/C", ""); // special case where Zwave JS sends °F/C as unit, but is actually dimensionless
|
||||
private static final Map<String, String> CHANNEL_ID_PROPERTY_NAME_REPLACEMENTS = Map.of("currentValue", "value", //
|
||||
"targetValue", "value", "currentColor", "color", "targetColor", "color", //
|
||||
"targetMode", "mode", "currentMode", "mode"); //
|
||||
private static final List<Integer> COMMAND_CLASSES_ADVANCED = List.of(44, 117);
|
||||
private static final List<Integer> SWITCH_STATES_OFF_CLOSED = List.of(-1, 0, 23);
|
||||
|
||||
public final int nodeId;
|
||||
public final String id;
|
||||
public final String label;
|
||||
public final boolean writable;
|
||||
public final String itemType;
|
||||
public final boolean isAdvanced;
|
||||
public final int commandClassId;
|
||||
public final int endpoint;
|
||||
public final Double factor;
|
||||
|
||||
public @Nullable String description;
|
||||
public @Nullable String unitSymbol;
|
||||
public @Nullable Unit<?> unit;
|
||||
public @Nullable Object readProperty;
|
||||
public @Nullable Object writeProperty;
|
||||
public @Nullable Map<String, String> optionList;
|
||||
public @Nullable String commandClassName;
|
||||
public @Nullable String propertyKeyName;
|
||||
public @Nullable Object propertyKey;
|
||||
|
||||
public final Object value;
|
||||
protected final @Nullable Integer min;
|
||||
protected @Nullable Long max;
|
||||
|
||||
protected BaseMetadata(int nodeId, Value value) {
|
||||
this.nodeId = nodeId;
|
||||
this.commandClassName = value.commandClassName;
|
||||
this.commandClassId = value.commandClass;
|
||||
this.endpoint = value.endpoint;
|
||||
this.propertyKey = value.propertyKey;
|
||||
this.writable = value.metadata.writeable;
|
||||
this.min = value.metadata.min;
|
||||
this.max = value.metadata.max;
|
||||
this.id = generateChannelId(value);
|
||||
|
||||
this.label = normalizeLabel(value.metadata.label, value.endpoint, value.propertyName);
|
||||
this.description = value.metadata.description != null ? value.metadata.description : null;
|
||||
this.unitSymbol = normalizeUnit(value.metadata.unit, value.value);
|
||||
this.factor = determineFactor(value.metadata.unit);
|
||||
this.unit = UnitUtils.parseUnit(this.unitSymbol);
|
||||
this.itemType = itemTypeFromMetadata(value.metadata.type, value.value, value.commandClass,
|
||||
value.metadata.states);
|
||||
if (unitSymbol != null && unit == null) {
|
||||
logger.warn("Node {}, unable to parse unitSymbol '{}', please file a bug report", nodeId, unitSymbol);
|
||||
}
|
||||
this.optionList = value.metadata.states;
|
||||
this.value = value.value;
|
||||
this.isAdvanced = isAdvanced(value.commandClass, value.propertyName, value.propertyKey);
|
||||
|
||||
if (writable) {
|
||||
this.writeProperty = value.property;
|
||||
} else {
|
||||
this.readProperty = value.property;
|
||||
}
|
||||
}
|
||||
|
||||
public BaseMetadata(int nodeId, Event data) {
|
||||
this.nodeId = nodeId;
|
||||
this.id = generateId(data);
|
||||
this.value = data.args.newValue;
|
||||
|
||||
this.min = null;
|
||||
this.max = null;
|
||||
this.commandClassId = 0;
|
||||
this.endpoint = 0;
|
||||
this.writable = false;
|
||||
this.label = DEFAULT_LABEL;
|
||||
this.itemType = CoreItemFactory.STRING;
|
||||
this.isAdvanced = false;
|
||||
this.factor = 1.0;
|
||||
}
|
||||
|
||||
/*
|
||||
* Determines the factor based on the unit string.
|
||||
*
|
||||
* @param unitString the unit string
|
||||
*
|
||||
* @return the factor
|
||||
*/
|
||||
protected Double determineFactor(@Nullable String unitString) {
|
||||
if (unitString == null && value instanceof Map<?, ?> treeMap) {
|
||||
if (treeMap.containsKey("unit")) {
|
||||
unitString = (String) treeMap.get("unit");
|
||||
}
|
||||
}
|
||||
if (unitString == null) {
|
||||
return 1.0;
|
||||
}
|
||||
|
||||
Pattern pattern = Pattern.compile("[0-9]*\\.?[0-9]+|[^0-9]+");
|
||||
Matcher matcher = pattern.matcher(unitString.trim());
|
||||
|
||||
String[] splitted = matcher.results().map(m -> m.group()).toArray(String[]::new);
|
||||
if (splitted.length < 2) {
|
||||
return 1.0;
|
||||
}
|
||||
|
||||
try {
|
||||
return Double.parseDouble(splitted[0]);
|
||||
} catch (NumberFormatException e) {
|
||||
return 1.0;
|
||||
}
|
||||
}
|
||||
|
||||
protected boolean isAdvanced(int commandClassId, String propertyName, @Nullable Object propertyKey) {
|
||||
return COMMAND_CLASSES_ADVANCED.contains(commandClassId);
|
||||
}
|
||||
|
||||
/*
|
||||
* Normalizes the label based on the provided parameters.
|
||||
*
|
||||
* @param label the label
|
||||
*
|
||||
* @param endpoint the endpoint
|
||||
*
|
||||
* @param propertyName the property name
|
||||
*
|
||||
* @return the normalized label
|
||||
*/
|
||||
private String normalizeLabel(@Nullable String label, int endpoint, String propertyName) {
|
||||
String output = "";
|
||||
if (label == null || label.isBlank()) {
|
||||
return propertyName;
|
||||
}
|
||||
output = label.replaceAll("\s\\[.*\\]", "");
|
||||
output = capitalize(output);
|
||||
if (endpoint > 0) {
|
||||
output = String.format("EP%s %s", endpoint, output);
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
/*
|
||||
* Capitalizes the input string by splitting camelCase words.
|
||||
*
|
||||
* @param input the input string
|
||||
*
|
||||
* @return the capitalized string
|
||||
*/
|
||||
private String capitalize(@Nullable String input) {
|
||||
if (input == null || input.isBlank()) {
|
||||
return DEFAULT_LABEL;
|
||||
}
|
||||
|
||||
return Objects
|
||||
.requireNonNullElse(
|
||||
Arrays.stream(StringUtils.splitByCharacterType(input)).filter(f -> !f.isBlank())
|
||||
.map(word -> StringUtils.capitalize(word)).collect(Collectors.joining(" ")),
|
||||
DEFAULT_LABEL)
|
||||
.replace(" - ", "-").replace("( ", "(").replace(" )", ")");
|
||||
}
|
||||
|
||||
private String normalizeString(@Nullable Object input) {
|
||||
if (input instanceof Number numberInput) {
|
||||
return "-" + numberInput.toString();
|
||||
} else if (input instanceof String strInput) {
|
||||
return "-" + strInput.trim().toLowerCase().replaceAll(" ", "-").replaceAll("[^a-zA-Z0-9\\-]", "");
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
private String generateId(String commandClassName, int endpoint, @Nullable String propertyName,
|
||||
@Nullable Object propertyKey) {
|
||||
String id = normalizeString(commandClassName).replaceFirst("-", "");
|
||||
String[] splitted;
|
||||
if (propertyName != null && !propertyName.contains("unknown")) {
|
||||
propertyName = CHANNEL_ID_PROPERTY_NAME_REPLACEMENTS.getOrDefault(propertyName, propertyName);
|
||||
splitted = StringUtils.splitByCharacterType(propertyName);
|
||||
List<String> result = Arrays.asList(splitted).stream().filter(s -> s.matches("^[a-zA-Z0-9]+$")).toList();
|
||||
if (!result.isEmpty()) {
|
||||
id += normalizeString(String.join("-", result));
|
||||
}
|
||||
}
|
||||
if (propertyKey != null) {
|
||||
id += normalizeString(propertyKey);
|
||||
}
|
||||
if (endpoint > 0) {
|
||||
id += "-" + endpoint;
|
||||
return id;
|
||||
}
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
private String generateId(Event event) {
|
||||
return generateId(event.args.commandClassName, event.args.endpoint, event.args.propertyName,
|
||||
event.args.propertyKey);
|
||||
}
|
||||
|
||||
private String generateChannelId(Value value) {
|
||||
return generateId(value.commandClassName, value.endpoint, value.propertyName, value.propertyKey);
|
||||
}
|
||||
|
||||
/*
|
||||
* Converts the given value to a State object based on the item type and unit.
|
||||
*
|
||||
* @param value the value to convert
|
||||
*
|
||||
* @param itemType the item type
|
||||
*
|
||||
* @param unit the unit of the value
|
||||
*
|
||||
* @param inverted whether the value should be inverted
|
||||
*
|
||||
* @param factor the factor to apply to the value
|
||||
*
|
||||
* @return the converted State object, or UnDefType.NULL if the value is null
|
||||
*/
|
||||
protected @Nullable State toState(@Nullable Object value, String itemType, @Nullable Unit<?> unit, boolean inverted,
|
||||
Double factor) {
|
||||
if (value == null) {
|
||||
return UnDefType.NULL;
|
||||
}
|
||||
|
||||
if (value instanceof Map<?, ?> treeMap) {
|
||||
if (treeMap.containsKey("value")) {
|
||||
value = Objects.requireNonNull(treeMap.get("value"));
|
||||
}
|
||||
}
|
||||
|
||||
String itemTypeSplitted[] = itemType.split(":");
|
||||
switch (itemTypeSplitted[0]) {
|
||||
case CoreItemFactory.NUMBER:
|
||||
return handleNumberType(value, unit, factor);
|
||||
case CoreItemFactory.DIMMER:
|
||||
return handleDimmerType(value, inverted);
|
||||
case CoreItemFactory.SWITCH:
|
||||
return handleSwitchType(value, inverted);
|
||||
case CoreItemFactory.COLOR:
|
||||
return handleColorType(value);
|
||||
case CoreItemFactory.STRING:
|
||||
return StringType.valueOf(Objects.requireNonNull(value).toString());
|
||||
default:
|
||||
logger.warn("Node {}, unexpected item type: {}, please file a bug report", nodeId, itemType);
|
||||
return UnDefType.UNDEF;
|
||||
}
|
||||
}
|
||||
|
||||
private @Nullable State handleNumberType(Object value, @Nullable Unit<?> unit, Double factor) {
|
||||
if (!(value instanceof Number numberVal)) {
|
||||
if (value instanceof String strVal) {
|
||||
if ("unknown".equalsIgnoreCase(strVal)) {
|
||||
return UnDefType.UNDEF;
|
||||
}
|
||||
}
|
||||
logger.warn("Node {}, unexpected value type for number: {}, please file a bug report", nodeId,
|
||||
value.getClass().getName());
|
||||
return UnDefType.UNDEF;
|
||||
}
|
||||
|
||||
numberVal = factor == 1.0 ? numberVal : numberVal.doubleValue() * factor;
|
||||
if (unit != null) {
|
||||
return new QuantityType<>(numberVal, unit);
|
||||
} else {
|
||||
return new DecimalType(numberVal);
|
||||
}
|
||||
}
|
||||
|
||||
private @Nullable State handleDimmerType(Object value, boolean inverted) {
|
||||
if (value instanceof Number numberValue) {
|
||||
try {
|
||||
return new PercentType(inverted ? 100 - numberValue.intValue() : numberValue.intValue());
|
||||
} catch (IllegalArgumentException e) {
|
||||
logger.warn("Node {}, invalid PercentType value provided: {}", nodeId, numberValue);
|
||||
return UnDefType.UNDEF;
|
||||
}
|
||||
} else {
|
||||
logger.warn("Node {}, unexpected value type for dimmer: {}, please file a bug report", nodeId,
|
||||
value.getClass().getName());
|
||||
return UnDefType.UNDEF;
|
||||
}
|
||||
}
|
||||
|
||||
private @Nullable State handleSwitchType(Object value, boolean inverted) {
|
||||
if (value instanceof Number numberValue) {
|
||||
boolean offOrClosedState = SWITCH_STATES_OFF_CLOSED.contains(numberValue.intValue());
|
||||
return OnOffType.from(inverted ? offOrClosedState : !offOrClosedState);
|
||||
}
|
||||
if (!(value instanceof Boolean boolVal)) {
|
||||
logger.warn("Node {}, unexpected value type for switch: {}, please file a bug report", nodeId,
|
||||
value.getClass().getName());
|
||||
return UnDefType.UNDEF;
|
||||
}
|
||||
|
||||
return inverted ? OnOffType.from(!boolVal) : OnOffType.from(boolVal);
|
||||
}
|
||||
|
||||
private @Nullable State handleColorType(Object value) {
|
||||
if (value instanceof String colorStr) {
|
||||
try {
|
||||
colorStr = colorStr.startsWith("#") ? colorStr : "#" + colorStr;
|
||||
int red = Integer.valueOf(colorStr.substring(1, 3), 16);
|
||||
int green = Integer.valueOf(colorStr.substring(3, 5), 16);
|
||||
int blue = Integer.valueOf(colorStr.substring(5, 7), 16);
|
||||
return HSBType.fromRGB(red, green, blue);
|
||||
} catch (NumberFormatException | IndexOutOfBoundsException e) {
|
||||
logger.warn("Node {}, invalid color string provided: {}", nodeId, colorStr, e);
|
||||
return UnDefType.UNDEF;
|
||||
}
|
||||
} else if (value instanceof Map<?, ?> map && isColorMap(map)) {
|
||||
int red = map.get(RED) instanceof Number n ? n.intValue() : -1;
|
||||
int green = map.get(GREEN) instanceof Number n ? n.intValue() : -1;
|
||||
int blue = map.get(BLUE) instanceof Number n ? n.intValue() : -1;
|
||||
int warm = map.get(WARM_WHITE) instanceof Number n ? n.intValue() : -1;
|
||||
int cold = map.get(COLD_WHITE) instanceof Number n ? n.intValue() : -1;
|
||||
if (red >= 0 && green >= 0 && blue >= 0 && warm <= 0 && cold <= 0) {
|
||||
return HSBType.fromRGB(red, green, blue);
|
||||
} else if (warm > 0 && cold > 0) {
|
||||
return ColorUtil.xyToHsb(ColorUtil.kelvinToXY(6500 - (4000 * warm / (warm + cold))));
|
||||
} else if (warm > 0) {
|
||||
return ColorUtil.xyToHsb(ColorUtil.kelvinToXY(6500 - (4000 * warm / 255)));
|
||||
} else if (cold > 0) {
|
||||
return ColorUtil.xyToHsb(ColorUtil.kelvinToXY(2500 + (4000 * cold / 255)));
|
||||
}
|
||||
return UnDefType.UNDEF;
|
||||
} else {
|
||||
logger.warn("Node {}, unexpected value type for color: {}, please file a bug report", nodeId,
|
||||
value.getClass().getName());
|
||||
return UnDefType.UNDEF;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Corrects the metadata type based on the provided value, command class name, and optional list of options.
|
||||
*
|
||||
* @param type The original metadata type.
|
||||
*
|
||||
* @param value The value to determine the type from if the original type is ANY.
|
||||
*
|
||||
* @param commandClassName The name of the command class.
|
||||
*
|
||||
* @param optionList An optional list of options that may influence the type correction.
|
||||
*
|
||||
* @return The corrected metadata type.
|
||||
*/
|
||||
protected MetadataType correctedType(MetadataType type, @Nullable Object value, int commandClass,
|
||||
@Nullable Map<String, String> optionList) {
|
||||
switch (type) {
|
||||
case ANY:
|
||||
return determineTypeFromValue(value, commandClass);
|
||||
case DURATION:
|
||||
return MetadataType.NUMBER;
|
||||
case NUMBER:
|
||||
if (COMMAND_CLASS_ALARM == commandClass && optionList != null && optionList.size() == 2) {
|
||||
return MetadataType.BOOLEAN;
|
||||
}
|
||||
default:
|
||||
return type;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Determines the metadata type based on the provided value and command class.
|
||||
*
|
||||
* @param value The value to determine the metadata type from. Can be null.
|
||||
*
|
||||
* @param commandClass The Z-Wave command class identifier used for additional type determination in specific cases.
|
||||
*
|
||||
* @return The determined metadata type.
|
||||
*/
|
||||
private MetadataType determineTypeFromValue(@Nullable Object value, int commandClass) {
|
||||
if (value instanceof Number) {
|
||||
return MetadataType.NUMBER;
|
||||
} else if (value instanceof Boolean) {
|
||||
return MetadataType.BOOLEAN;
|
||||
} else if (value instanceof Map<?, ?> treeMap) {
|
||||
if (isColorMap(treeMap)) {
|
||||
return MetadataType.COLOR;
|
||||
} else if (treeMap.containsKey("value")) {
|
||||
return determineTypeFromValue(treeMap.get("value"), commandClass);
|
||||
}
|
||||
} else if (value instanceof String) {
|
||||
return MetadataType.STRING;
|
||||
}
|
||||
|
||||
if (commandClass == COMMAND_CLASS_DOOR_LOCK) {
|
||||
if (value instanceof ArrayList) {
|
||||
return MetadataType.STRING;
|
||||
}
|
||||
return MetadataType.BOOLEAN; // Notification CC can be boolean or string, but we default to boolean
|
||||
}
|
||||
|
||||
logger.warn("Node {}, unexpected value type: {}, please file a bug report", nodeId,
|
||||
value != null ? value.getClass().getName() : "null");
|
||||
return MetadataType.STRING;
|
||||
}
|
||||
|
||||
/*
|
||||
* Checks if the given map represents an RGB Color or Color Temperature map.
|
||||
* The map should have either all the three keys "red", "green", and "blue",
|
||||
* and/or one or two of the keys "warmWhite" and "coldWhite".
|
||||
*
|
||||
* @param map the map to check
|
||||
*
|
||||
* @return true if the map represents an RGB color map, false otherwise
|
||||
*/
|
||||
private boolean isColorMap(Map<?, ?> map) {
|
||||
return (map.containsKey("red") && map.containsKey("green") && map.containsKey("blue"))
|
||||
|| map.containsKey("warmWhite") || map.containsKey("coldWhite");
|
||||
}
|
||||
|
||||
protected String itemTypeFromMetadata(MetadataType type, @Nullable Object value, int commandClass,
|
||||
@Nullable Map<String, String> optionList) {
|
||||
type = correctedType(type, value, commandClass, optionList);
|
||||
|
||||
switch (type) {
|
||||
case NUMBER:
|
||||
Unit<?> unit = this.unit;
|
||||
if (unit != null) {
|
||||
String dimension = UnitUtils.getDimensionName(unit);
|
||||
if (dimension == null) {
|
||||
logger.warn("Node {}. Could not parse '{}' as a unit, fallback to 'Number' itemType", nodeId,
|
||||
unitSymbol);
|
||||
return CoreItemFactory.NUMBER;
|
||||
}
|
||||
return CoreItemFactory.NUMBER + ":" + dimension;
|
||||
}
|
||||
|
||||
return CoreItemFactory.NUMBER;
|
||||
case BOOLEAN:
|
||||
// switch (or contact ?)
|
||||
return CoreItemFactory.SWITCH;
|
||||
case COLOR:
|
||||
return CoreItemFactory.COLOR;
|
||||
case STRING:
|
||||
case STRING_ARRAY:
|
||||
return CoreItemFactory.STRING;
|
||||
default:
|
||||
logger.warn(
|
||||
"Node {}. Unable to determine item type based on metadata.type: {}, fallback to 'String' please file a bug report",
|
||||
nodeId, type);
|
||||
return CoreItemFactory.STRING;
|
||||
}
|
||||
}
|
||||
|
||||
protected @Nullable StateDescriptionFragment createStatePattern(boolean writeable, @Nullable Integer min,
|
||||
@Nullable Long max, @Nullable Integer step, @Nullable Object value) {
|
||||
String pattern = null;
|
||||
String itemTypeSplitted[] = itemType.split(":");
|
||||
switch (itemTypeSplitted[0]) {
|
||||
case CoreItemFactory.NUMBER:
|
||||
String numberFormat = "%d";
|
||||
if (value instanceof Double) {
|
||||
// TODO: how to properly determine the decimals?
|
||||
numberFormat = "%.2f";
|
||||
}
|
||||
if (itemTypeSplitted.length > 1) {
|
||||
pattern = numberFormat + " %unit%";
|
||||
} else {
|
||||
pattern = numberFormat;
|
||||
}
|
||||
break;
|
||||
case CoreItemFactory.DIMMER:
|
||||
pattern = "%1d %%";
|
||||
break;
|
||||
case CoreItemFactory.COLOR:
|
||||
break;
|
||||
case CoreItemFactory.STRING:
|
||||
case CoreItemFactory.SWITCH:
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
var fragment = StateDescriptionFragmentBuilder.create();
|
||||
if (pattern != null) {
|
||||
fragment.withPattern(pattern);
|
||||
}
|
||||
fragment.withReadOnly(!writeable);
|
||||
if (min != null) {
|
||||
fragment.withMinimum(BigDecimal.valueOf(min));
|
||||
}
|
||||
if (max != null) {
|
||||
fragment.withMaximum(BigDecimal.valueOf(max));
|
||||
}
|
||||
Map<String, String> optionList = this.optionList;
|
||||
if (optionList != null) {
|
||||
List<StateOption> options = new ArrayList<>();
|
||||
optionList.forEach((k, v) -> options.add(new StateOption(k, v)));
|
||||
fragment.withOptions(options);
|
||||
}
|
||||
if (step != null && step > 0) {
|
||||
fragment.withStep(BigDecimal.valueOf(step));
|
||||
}
|
||||
return fragment.build();
|
||||
}
|
||||
|
||||
protected @Nullable String normalizeUnit(@Nullable String unitString, @Nullable Object value) {
|
||||
if (unitString == null && value instanceof Map<?, ?> treeMap) {
|
||||
if (treeMap.containsKey("unit")) {
|
||||
unitString = (String) treeMap.get("unit");
|
||||
}
|
||||
}
|
||||
if (unitString == null) {
|
||||
return null;
|
||||
}
|
||||
unitString = unitString.trim();
|
||||
Pattern pattern = Pattern.compile("[0-9]*\\.?[0-9]+|[^0-9]+");
|
||||
Matcher matcher = pattern.matcher(unitString);
|
||||
String[] splitted = matcher.results().map(m -> m.group()).toArray(String[]::new);
|
||||
String lastPart = splitted.length > 0 ? splitted[splitted.length - 1].trim() : unitString;
|
||||
String output = Objects
|
||||
.requireNonNull(UNIT_REPLACEMENTS.getOrDefault(lastPart, Objects.requireNonNull(lastPart)));
|
||||
|
||||
return !output.isBlank() ? output : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("BaseMetadata [");
|
||||
sb.append(", nodeId=" + nodeId);
|
||||
sb.append(", Id=" + id);
|
||||
sb.append(", label=" + label);
|
||||
sb.append(", description=" + description);
|
||||
sb.append(", unitSymbol=" + unitSymbol);
|
||||
sb.append(", value=" + value);
|
||||
sb.append(", itemType=" + itemType);
|
||||
sb.append(", writable=" + writable);
|
||||
sb.append(", propertyKey=" + propertyKey);
|
||||
sb.append(", readProperty=" + readProperty);
|
||||
sb.append(", writeProperty=" + writeProperty);
|
||||
sb.append(", itemType=" + itemType);
|
||||
sb.append("]");
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
+146
@@ -0,0 +1,146 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2025 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.zwavejs.internal.conversion;
|
||||
|
||||
import static org.openhab.binding.zwavejs.internal.CommandClassConstants.COMMAND_CLASS_SWITCH_COLOR;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.zwavejs.internal.api.dto.Event;
|
||||
import org.openhab.binding.zwavejs.internal.api.dto.MetadataType;
|
||||
import org.openhab.binding.zwavejs.internal.api.dto.Value;
|
||||
import org.openhab.core.library.CoreItemFactory;
|
||||
import org.openhab.core.types.State;
|
||||
import org.openhab.core.types.StateDescriptionFragment;
|
||||
import org.openhab.core.types.util.UnitUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The {@link ChannelMetadata} class represents channel metadata information for a Z-Wave node.
|
||||
* It contains various properties and methods to handle metadata and state information.
|
||||
*
|
||||
* @author Leo Siepel - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class ChannelMetadata extends BaseMetadata {
|
||||
|
||||
private Logger logger = LoggerFactory.getLogger(ChannelMetadata.class);
|
||||
private static final List<String> IGNORED_COMMANDCLASSES = List.of("Manufacturer Specific", "Version");
|
||||
private static final List<String> ADVANCED_CHANNELS = List.of( //
|
||||
"32-restorePrevious", //
|
||||
"32-duration", //
|
||||
"38-On", //
|
||||
"38-Off", //
|
||||
"38-duration", //
|
||||
"38-restorePrevious", //
|
||||
"51-duration", //
|
||||
"113-alarmType", //
|
||||
"113-alarmLevel", //
|
||||
"113-System"); //
|
||||
private static final List<String> INVERTIBLE_ITEM_TYPES = List.of(CoreItemFactory.DIMMER, CoreItemFactory.CONTACT,
|
||||
CoreItemFactory.SWITCH);
|
||||
|
||||
public @Nullable State state;
|
||||
public @Nullable StateDescriptionFragment statePattern;
|
||||
|
||||
public ChannelMetadata(int nodeId, Value data) {
|
||||
super(nodeId, data);
|
||||
|
||||
this.statePattern = createStatePattern(data.metadata.writeable, min, max, data.metadata.steps, data.value);
|
||||
this.state = toState(data.value, itemType, unit, false, factor);
|
||||
}
|
||||
|
||||
public ChannelMetadata(int nodeId, Event data) {
|
||||
super(nodeId, data);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String itemTypeFromMetadata(MetadataType type, @Nullable Object value, int commandClass,
|
||||
@Nullable Map<String, String> optionList) {
|
||||
String baseItemType = super.itemTypeFromMetadata(type, value, commandClass, optionList);
|
||||
if (CoreItemFactory.NUMBER.equals(baseItemType) && writable && min != null && max != null) {
|
||||
if (min == 0 && max == 99) {
|
||||
this.max = 100L; // ZUI uses 0-99, but openHAB uses 0-100
|
||||
return CoreItemFactory.DIMMER;
|
||||
}
|
||||
}
|
||||
|
||||
return baseItemType;
|
||||
}
|
||||
|
||||
public boolean isInvertible() {
|
||||
return INVERTIBLE_ITEM_TYPES.contains(itemType);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isAdvanced(int commandClassId, String propertyName, @Nullable Object propertyKey) {
|
||||
return super.isAdvanced(commandClassId, propertyName, propertyKey)
|
||||
|| ADVANCED_CHANNELS.contains(commandClassId + "-" + propertyName)
|
||||
|| (commandClassId == COMMAND_CLASS_SWITCH_COLOR && propertyKey != null);
|
||||
}
|
||||
|
||||
public boolean isIgnoredCommandClass(@Nullable String commandClassName) {
|
||||
return commandClassName != null && IGNORED_COMMANDCLASSES.contains(commandClassName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the state for this channel metadata based on the provided value and configuration.
|
||||
*
|
||||
* @param value the raw value to convert to a {@link State}
|
||||
* @param itemType the openHAB item type (e.g., "Switch", "Number", "Color")
|
||||
* @param unitSymbol the unit symbol of the incoming value, or {@code null} if not applicable
|
||||
* @param inverted {@code true} if the value should be logically inverted; {@code false} otherwise
|
||||
* @return the corresponding {@link State} for the given value and configuration, or {@code null} if conversion is
|
||||
* not possible
|
||||
*/
|
||||
public @Nullable State setState(Object value, String itemType, @Nullable String unitSymbol, boolean inverted) {
|
||||
this.unitSymbol = normalizeUnit(unitSymbol, value);
|
||||
Double factor = determineFactor(unitSymbol);
|
||||
this.unit = UnitUtils.parseUnit(this.unitSymbol);
|
||||
if (unitSymbol != null && this.unit == null) {
|
||||
logger.warn("Node {}. Unable to parse unitSymbol '{}' from channel config, this is a bug", nodeId,
|
||||
unitSymbol);
|
||||
}
|
||||
if (CoreItemFactory.DIMMER.equals(itemType) && value instanceof Number numberValue) {
|
||||
value = numberValue.intValue() >= 99 ? 100 : value;
|
||||
}
|
||||
return this.state = toState(value, itemType, this.unit, inverted, factor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("ChannelMetadata [");
|
||||
sb.append("nodeId=" + nodeId);
|
||||
sb.append(", Id=" + id);
|
||||
sb.append(", label=" + label);
|
||||
sb.append(", description=" + description);
|
||||
sb.append(", unitSymbol=" + unitSymbol);
|
||||
sb.append(", value=" + value);
|
||||
sb.append(", itemType=" + itemType);
|
||||
sb.append(", writable=" + writable);
|
||||
sb.append(", readProperty=" + readProperty);
|
||||
sb.append(", writeProperty=" + writeProperty);
|
||||
sb.append(", state=" + state);
|
||||
sb.append(", statePattern=" + statePattern);
|
||||
sb.append(", commandClassName=" + commandClassName);
|
||||
sb.append(", commandClassId=" + commandClassId);
|
||||
sb.append(", endpoint=" + endpoint);
|
||||
sb.append("]");
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2025 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.zwavejs.internal.conversion;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.zwavejs.internal.api.dto.Event;
|
||||
import org.openhab.binding.zwavejs.internal.api.dto.MetadataType;
|
||||
import org.openhab.binding.zwavejs.internal.api.dto.Value;
|
||||
import org.openhab.core.config.core.ConfigDescriptionParameter.Type;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The {@link ConfigMetadata} class represents configuration metadata information for a Z-Wave node.
|
||||
* It contains various properties and methods to handle metadata and state information.
|
||||
*
|
||||
* @author Leo Siepel - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class ConfigMetadata extends BaseMetadata {
|
||||
|
||||
private Logger logger = LoggerFactory.getLogger(ConfigMetadata.class);
|
||||
|
||||
public Type configType = Type.TEXT;
|
||||
|
||||
public ConfigMetadata(int nodeId, Value data) {
|
||||
super(nodeId, data);
|
||||
|
||||
this.configType = configTypeFromMetadata(data.metadata.type, data.value, data.commandClass);
|
||||
}
|
||||
|
||||
public ConfigMetadata(int nodeId, Event data) {
|
||||
super(nodeId, data);
|
||||
}
|
||||
|
||||
private Type configTypeFromMetadata(MetadataType type, Object value, int commandClass) {
|
||||
type = correctedType(type, value, commandClass, null);
|
||||
switch (type) {
|
||||
case NUMBER:
|
||||
return Type.INTEGER;
|
||||
// Might be future cases that require DECIMAL, might depend on scale?
|
||||
case COLOR:
|
||||
return Type.TEXT;
|
||||
case BOOLEAN:
|
||||
// switch (or contact ?)
|
||||
return Type.BOOLEAN;
|
||||
case STRING:
|
||||
case STRING_ARRAY:
|
||||
return Type.TEXT;
|
||||
default:
|
||||
logger.warn(
|
||||
"Node {}. Could not determine config type based on metadata.type: {}, fallback to 'Text' please file a bug report",
|
||||
this.nodeId, type);
|
||||
return Type.TEXT;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("ConfigMetadata [");
|
||||
sb.append(", nodeId=" + nodeId);
|
||||
sb.append(", Id=" + id);
|
||||
sb.append(", label=" + label);
|
||||
sb.append(", description=" + description);
|
||||
sb.append(", unitSymbol=" + unitSymbol);
|
||||
sb.append(", value=" + value);
|
||||
sb.append(", itemType=" + itemType);
|
||||
sb.append(", writable=" + writable);
|
||||
sb.append(", readProperty=" + readProperty);
|
||||
sb.append(", writeProperty=" + writeProperty);
|
||||
sb.append(", configType=" + configType);
|
||||
sb.append(", commandClassName=" + commandClassName);
|
||||
sb.append(", commandClassId=" + commandClassId);
|
||||
sb.append(", endpoint=" + endpoint);
|
||||
sb.append("]");
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2025 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.zwavejs.internal.discovery;
|
||||
|
||||
import static org.openhab.binding.zwavejs.internal.BindingConstants.*;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
import javax.jmdns.ServiceInfo;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.core.config.discovery.DiscoveryResult;
|
||||
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
|
||||
import org.openhab.core.config.discovery.mdns.MDNSDiscoveryParticipant;
|
||||
import org.openhab.core.i18n.TranslationProvider;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
import org.openhab.core.thing.ThingUID;
|
||||
import org.osgi.framework.FrameworkUtil;
|
||||
import org.osgi.service.component.annotations.Activate;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
import org.osgi.service.component.annotations.Reference;
|
||||
|
||||
/**
|
||||
* The {@link BridgeMDNSDiscoveryParticipant} is responsible for discovering new and removed Zwave JS servers. It uses
|
||||
* the
|
||||
* central {@link org.openhab.core.config.discovery.mdns.internal.MDNSDiscoveryService}.
|
||||
*
|
||||
* @author Leo Siepel - Initial contribution
|
||||
*/
|
||||
@Component(configurationPid = "discovery.zwavejs")
|
||||
@NonNullByDefault
|
||||
public class BridgeMDNSDiscoveryParticipant implements MDNSDiscoveryParticipant {
|
||||
|
||||
private static final String SERVICE_TYPE = "_zwave-js-server._tcp.local.";
|
||||
private static final String MDNS_PROPERTY_HOME_ID = "homeId";
|
||||
|
||||
private final TranslationProvider translationProvider;
|
||||
|
||||
@Activate
|
||||
public BridgeMDNSDiscoveryParticipant(@Reference TranslationProvider translationProvider) {
|
||||
this.translationProvider = translationProvider;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<ThingTypeUID> getSupportedThingTypeUIDs() {
|
||||
return Set.of(THING_TYPE_GATEWAY);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getServiceType() {
|
||||
return SERVICE_TYPE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable DiscoveryResult createResult(ServiceInfo service) {
|
||||
ThingUID uid = getThingUID(service);
|
||||
|
||||
if (uid != null) {
|
||||
String host = service.getHostAddresses()[0];
|
||||
String homeId = service.getPropertyString(MDNS_PROPERTY_HOME_ID);
|
||||
String thingTypeId = uid.getAsString().split(ThingUID.SEPARATOR)[1];
|
||||
String label = translationProvider.getText(FrameworkUtil.getBundle(getClass()),
|
||||
"discovery.%s.label".formatted(thingTypeId), null, null, host);
|
||||
|
||||
DiscoveryResultBuilder builder = DiscoveryResultBuilder.create(uid) //
|
||||
.withLabel(label) //
|
||||
.withProperty(CONFIG_HOSTNAME, host) //
|
||||
.withProperty(CONFIG_PORT, service.getPort()) //
|
||||
.withProperty(PROPERTY_HOME_ID, homeId) //
|
||||
.withRepresentationProperty(MDNS_PROPERTY_HOME_ID);
|
||||
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable ThingUID getThingUID(ServiceInfo service) {
|
||||
String homeId = service.getPropertyString(MDNS_PROPERTY_HOME_ID);
|
||||
|
||||
if (homeId != null) {
|
||||
return new ThingUID(THING_TYPE_GATEWAY, homeId);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
+162
@@ -0,0 +1,162 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2025 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.zwavejs.internal.discovery;
|
||||
|
||||
import static org.openhab.binding.zwavejs.internal.BindingConstants.*;
|
||||
import static org.openhab.core.thing.Thing.*;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.zwavejs.internal.BindingConstants;
|
||||
import org.openhab.binding.zwavejs.internal.api.dto.Node;
|
||||
import org.openhab.binding.zwavejs.internal.handler.ZwaveJSBridgeHandler;
|
||||
import org.openhab.core.config.discovery.AbstractThingHandlerDiscoveryService;
|
||||
import org.openhab.core.config.discovery.DiscoveryResult;
|
||||
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
|
||||
import org.openhab.core.i18n.LocaleProvider;
|
||||
import org.openhab.core.i18n.TranslationProvider;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
import org.openhab.core.thing.ThingUID;
|
||||
import org.osgi.service.component.annotations.Activate;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
import org.osgi.service.component.annotations.Reference;
|
||||
import org.osgi.service.component.annotations.ServiceScope;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The {@link NodeDiscoveryService} tracks for Z-Wave nodes which are connected
|
||||
* to a Z-Wave JS webservice.
|
||||
*
|
||||
* @author Leo Siepel - Initial contribution
|
||||
*/
|
||||
@Component(scope = ServiceScope.PROTOTYPE, service = NodeDiscoveryService.class)
|
||||
@NonNullByDefault
|
||||
public class NodeDiscoveryService extends AbstractThingHandlerDiscoveryService<ZwaveJSBridgeHandler> {
|
||||
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Set.of(THING_TYPE_NODE);
|
||||
|
||||
private static final int SEARCH_TIME = 10;
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(NodeDiscoveryService.class);
|
||||
|
||||
/*
|
||||
* Creates an NodeDiscoveryService with enabled autostart.
|
||||
*/
|
||||
@Activate
|
||||
public NodeDiscoveryService() {
|
||||
super(ZwaveJSBridgeHandler.class, SUPPORTED_THING_TYPES, SEARCH_TIME);
|
||||
}
|
||||
|
||||
@Reference(unbind = "-")
|
||||
public void bindTranslationProvider(TranslationProvider translationProvider) {
|
||||
this.i18nProvider = translationProvider;
|
||||
}
|
||||
|
||||
@Reference(unbind = "-")
|
||||
public void bindLocaleProvider(LocaleProvider localeProvider) {
|
||||
this.localeProvider = localeProvider;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
thingHandler.registerDiscoveryListener(this);
|
||||
super.initialize();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
super.dispose();
|
||||
removeOlderResults(Instant.now(), getBridgeUID());
|
||||
thingHandler.unregisterDiscoveryListener();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<ThingTypeUID> getSupportedThingTypes() {
|
||||
return SUPPORTED_THING_TYPES;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void startScan() {
|
||||
thingHandler.getFullState();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected synchronized void stopScan() {
|
||||
super.stopScan();
|
||||
removeOlderResults(getTimestampOfLastScan(), getBridgeUID());
|
||||
}
|
||||
|
||||
public void addNodeDiscovery(Node node) {
|
||||
ThingUID thingUID = getThingUID(node.nodeId);
|
||||
ThingTypeUID thingTypeUID = THING_TYPE_NODE;
|
||||
logger.trace("Node {}. addNodeDiscovery", node.nodeId);
|
||||
|
||||
if (thingUID != null) {
|
||||
String manufacturer = node.deviceConfig != null ? node.deviceConfig.manufacturer : "Unknown";
|
||||
String product = node.deviceConfig != null ? node.deviceConfig.label : "";
|
||||
String discoveryLabel = String.format(DISCOVERY_NODE_LABEL_PATTERN, manufacturer, product, node.nodeId);
|
||||
|
||||
Map<String, Object> properties = new HashMap<>();
|
||||
|
||||
properties.put(CONFIG_NODE_ID, node.nodeId);
|
||||
|
||||
properties.put(PROPERTY_NODE_IS_LISTENING, node.isListening);
|
||||
properties.put(PROPERTY_NODE_IS_ROUTING, node.isRouting);
|
||||
properties.put(PROPERTY_NODE_IS_SECURE, node.isSecure);
|
||||
properties.put(PROPERTY_VENDOR, manufacturer);
|
||||
properties.put(PROPERTY_MODEL_ID, product);
|
||||
properties.put(PROPERTY_NODE_LASTSEEN, node.lastSeen);
|
||||
properties.put(PROPERTY_NODE_FREQ_LISTENING, node.isFrequentListening);
|
||||
properties.put(PROPERTY_FIRMWARE_VERSION, node.firmwareVersion);
|
||||
|
||||
DiscoveryResult discoveryResult = DiscoveryResultBuilder.create(thingUID).withThingType(thingTypeUID)
|
||||
.withProperties(properties).withBridge(getBridgeUID()).withRepresentationProperty(CONFIG_NODE_ID)
|
||||
.withLabel(discoveryLabel).build();
|
||||
|
||||
thingDiscovered(discoveryResult);
|
||||
} else {
|
||||
logger.warn("Node {}. Discovered unsupported device.", node.nodeId);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void thingDiscovered(DiscoveryResult discoveryResult) {
|
||||
super.thingDiscovered(discoveryResult);
|
||||
}
|
||||
|
||||
public void removeNodeDiscovery(int nodeId) {
|
||||
ThingUID thingUID = getThingUID(nodeId);
|
||||
|
||||
if (thingUID != null) {
|
||||
thingRemoved(thingUID);
|
||||
logger.debug("Node {}. removeNodeDiscovery", nodeId);
|
||||
}
|
||||
}
|
||||
|
||||
private @Nullable ThingUID getBridgeUID() {
|
||||
return thingHandler.getThing().getUID();
|
||||
}
|
||||
|
||||
private @Nullable ThingUID getThingUID(int nodeId) {
|
||||
ThingUID localBridgeUID = getBridgeUID();
|
||||
if (localBridgeUID != null) {
|
||||
return new ThingUID(BindingConstants.THING_TYPE_NODE, localBridgeUID, "node" + nodeId);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2025 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.zwavejs.internal.handler;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.zwavejs.internal.api.dto.messages.BaseMessage;
|
||||
import org.openhab.binding.zwavejs.internal.discovery.NodeDiscoveryService;
|
||||
|
||||
/**
|
||||
* Implementations of this {@link ZwaveEventListener} interface can be registered with the {@link ZWaveJSClient}
|
||||
* to receive notifications about various Z-Wave events such as node updates, value changes,
|
||||
* and other Z-Wave network events.
|
||||
*
|
||||
*
|
||||
* @author Leo Siepel - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public interface ZwaveEventListener {
|
||||
|
||||
/*
|
||||
* Register {@link NodeDiscoveryService} to bridge handler
|
||||
*
|
||||
* @param listener the discovery service
|
||||
*
|
||||
* @return {@code true} if the new discovery service is accepted
|
||||
*/
|
||||
boolean registerDiscoveryListener(NodeDiscoveryService listener);
|
||||
|
||||
/*
|
||||
* Unregister {@link NodeDiscoveryService} from bridge handler
|
||||
*
|
||||
* @return {@code true} if the discovery service was removed
|
||||
*/
|
||||
boolean unregisterDiscoveryListener();
|
||||
|
||||
/*
|
||||
* Registers a listener for node events.
|
||||
*
|
||||
* @param nodeListener the listener to be registered
|
||||
*
|
||||
*/
|
||||
void registerNodeListener(ZwaveNodeListener nodeListener);
|
||||
|
||||
/*
|
||||
* Unregisters a previously registered node listener.
|
||||
*
|
||||
* @param nodeListener the node listener to unregister
|
||||
*
|
||||
* @return true if the listener was successfully unregistered, false otherwise
|
||||
*/
|
||||
boolean unregisterNodeListener(ZwaveNodeListener nodeListener);
|
||||
|
||||
/*
|
||||
* Handles an event when a message is received.
|
||||
*
|
||||
* @param message the message that was received
|
||||
*/
|
||||
void onEvent(BaseMessage message);
|
||||
|
||||
/*
|
||||
* This method is called when there is a connection error.
|
||||
*
|
||||
* @param message the error message describing the connection issue
|
||||
*/
|
||||
void onConnectionError(String message);
|
||||
}
|
||||
+375
@@ -0,0 +1,375 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2025 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.zwavejs.internal.handler;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import javax.naming.CommunicationException;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.zwavejs.internal.BindingConstants;
|
||||
import org.openhab.binding.zwavejs.internal.action.ZwaveJSActions;
|
||||
import org.openhab.binding.zwavejs.internal.api.ZWaveJSClient;
|
||||
import org.openhab.binding.zwavejs.internal.api.dto.Args;
|
||||
import org.openhab.binding.zwavejs.internal.api.dto.Event;
|
||||
import org.openhab.binding.zwavejs.internal.api.dto.Node;
|
||||
import org.openhab.binding.zwavejs.internal.api.dto.State;
|
||||
import org.openhab.binding.zwavejs.internal.api.dto.Status;
|
||||
import org.openhab.binding.zwavejs.internal.api.dto.commands.BaseCommand;
|
||||
import org.openhab.binding.zwavejs.internal.api.dto.commands.ControllerExclusionCommand;
|
||||
import org.openhab.binding.zwavejs.internal.api.dto.commands.ControllerInclusionCommand;
|
||||
import org.openhab.binding.zwavejs.internal.api.dto.commands.MulticastSetValueCommand;
|
||||
import org.openhab.binding.zwavejs.internal.api.dto.commands.ServerListeningCommand;
|
||||
import org.openhab.binding.zwavejs.internal.api.dto.messages.BaseMessage;
|
||||
import org.openhab.binding.zwavejs.internal.api.dto.messages.EventMessage;
|
||||
import org.openhab.binding.zwavejs.internal.api.dto.messages.ResultMessage;
|
||||
import org.openhab.binding.zwavejs.internal.api.dto.messages.VersionMessage;
|
||||
import org.openhab.binding.zwavejs.internal.config.ZwaveJSBridgeConfiguration;
|
||||
import org.openhab.binding.zwavejs.internal.discovery.NodeDiscoveryService;
|
||||
import org.openhab.core.io.net.http.WebSocketFactory;
|
||||
import org.openhab.core.thing.Bridge;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
import org.openhab.core.thing.ThingStatus;
|
||||
import org.openhab.core.thing.ThingStatusDetail;
|
||||
import org.openhab.core.thing.binding.BaseBridgeHandler;
|
||||
import org.openhab.core.thing.binding.ThingHandlerService;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The {@link ZwaveJSBridgeHandler} is responsible for handling communication between the
|
||||
* {@link ZwaveJSNodeHandler} 's and the {@link ZWaveJSClient} This handler also manages node discovery
|
||||
* and provides controller-level operations like inclusion and exclusion.
|
||||
*
|
||||
* @author Leo Siepel - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class ZwaveJSBridgeHandler extends BaseBridgeHandler implements ZwaveEventListener {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(ZwaveJSBridgeHandler.class);
|
||||
private final Map<Integer, ZwaveNodeListener> nodeListeners = new ConcurrentHashMap<>();
|
||||
private final Map<Integer, Node> lastNodeStates = new ConcurrentHashMap<>();
|
||||
|
||||
protected ScheduledExecutorService executorService = scheduler;
|
||||
private @Nullable NodeDiscoveryService discoveryService;
|
||||
private @Nullable ScheduledFuture<?> initialConnection;
|
||||
private ZWaveJSClient client;
|
||||
|
||||
public ZwaveJSBridgeHandler(Bridge bridge, WebSocketFactory wsFactory) {
|
||||
super(bridge);
|
||||
this.client = new ZWaveJSClient(wsFactory.getCommonWebSocketClient());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommand(ChannelUID channelUID, Command command) {
|
||||
// The bridge does not support any commands
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
ZwaveJSBridgeConfiguration config = getConfigAs(ZwaveJSBridgeConfiguration.class);
|
||||
|
||||
if (!config.isValid()) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
|
||||
"@text/offline.conf-error.hostname-or-port");
|
||||
return;
|
||||
}
|
||||
|
||||
updateStatus(ThingStatus.UNKNOWN);
|
||||
|
||||
initialConnection = scheduler.scheduleWithFixedDelay(new Runnable() {
|
||||
public void run() {
|
||||
startClient(config);
|
||||
}
|
||||
}, 0, 120, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
protected void startClient(ZwaveJSBridgeConfiguration config) {
|
||||
try {
|
||||
client.setBufferSize(config.maxMessageSize);
|
||||
client.start("ws://" + config.hostname + ":" + config.port);
|
||||
client.addEventListener(this);
|
||||
// the thing is set to online when the response/events are received
|
||||
stopInitialConnectionJob();
|
||||
} catch (CommunicationException e) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
|
||||
} catch (InterruptedException e) {
|
||||
updateStatus(ThingStatus.OFFLINE);
|
||||
}
|
||||
}
|
||||
|
||||
private void stopInitialConnectionJob() {
|
||||
ScheduledFuture<?> initialConnection = this.initialConnection;
|
||||
if (initialConnection != null) {
|
||||
initialConnection.cancel(false);
|
||||
this.initialConnection = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEvent(BaseMessage message) {
|
||||
if (message instanceof VersionMessage event) {
|
||||
Map<String, String> properties = new HashMap<>();
|
||||
properties.put(BindingConstants.PROPERTY_DRIVER_VERSION, event.driverVersion);
|
||||
properties.put(BindingConstants.PROPERTY_SERVER_VERSION, event.serverVersion);
|
||||
properties.put(BindingConstants.PROPERTY_SCHEMA_MIN, String.valueOf(event.minSchemaVersion));
|
||||
properties.put(BindingConstants.PROPERTY_SCHEMA_MAX, String.valueOf(event.maxSchemaVersion));
|
||||
properties.put(BindingConstants.PROPERTY_HOME_ID, String.valueOf(event.homeId));
|
||||
this.getThing().setProperties(properties);
|
||||
return;
|
||||
}
|
||||
|
||||
if (message instanceof ResultMessage result) {
|
||||
if (result.messageId != null && result.messageId.startsWith("getvalue|")) {
|
||||
Event event = createEventFromMessageId(result.messageId,
|
||||
result.result != null ? result.result.value : null);
|
||||
if (event == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
ZwaveNodeListener nodeListener = nodeListeners.get(event.nodeId);
|
||||
if (nodeListener != null) {
|
||||
nodeListener.onNodeStateChanged(event);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (result.result == null || result.result.state == null) {
|
||||
logger.debug("ResultMessage missing result or state, ignoring.");
|
||||
return;
|
||||
}
|
||||
procesStateUpdate(result.result.state);
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
return;
|
||||
}
|
||||
|
||||
if (message instanceof EventMessage eventMsg && eventMsg.event != null) {
|
||||
String eventType = eventMsg.event.event;
|
||||
ZwaveNodeListener nodeListener = nodeListeners.get(eventMsg.event.nodeId);
|
||||
switch (eventType) {
|
||||
case "value updated":
|
||||
case "value notification":
|
||||
if (nodeListener != null) {
|
||||
nodeListener.onNodeStateChanged(eventMsg.event);
|
||||
}
|
||||
break;
|
||||
case "alive":
|
||||
if (nodeListener != null) {
|
||||
nodeListener.onNodeAlive(eventMsg.event);
|
||||
}
|
||||
break;
|
||||
case "dead":
|
||||
if (nodeListener != null) {
|
||||
nodeListener.onNodeDead(eventMsg.event);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
logger.trace("Unhandled event type: {}", eventType);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private @Nullable Event createEventFromMessageId(String messageId, @Nullable Object value) {
|
||||
// Example messageId: getvalue|0|51|Color Switch|2|currentColor|44|2466
|
||||
String[] parts = messageId.split("\\|");
|
||||
if (parts.length < 7) {
|
||||
logger.warn("Invalid messageId format: {}", messageId);
|
||||
return null;
|
||||
}
|
||||
Event event = new Event();
|
||||
event.args = new Args();
|
||||
event.args.newValue = value;
|
||||
try {
|
||||
event.args.endpoint = Integer.parseInt(parts[1]);
|
||||
event.args.commandClass = Integer.parseInt(parts[2]);
|
||||
event.args.commandClassName = parts[3];
|
||||
event.args.propertyKey = parts[4];
|
||||
event.args.propertyName = parts[5];
|
||||
event.nodeId = Integer.parseInt(parts[6]);
|
||||
} catch (NumberFormatException e) {
|
||||
logger.warn("Error parsing messageId '{}': {}", messageId, e.getMessage());
|
||||
return null;
|
||||
}
|
||||
return event;
|
||||
}
|
||||
|
||||
private void procesStateUpdate(State state) {
|
||||
logger.debug("Processing state update with {} nodes", state.nodes.size());
|
||||
|
||||
Map<Integer, Node> lastNodeStatesCopy = new HashMap<>(lastNodeStates);
|
||||
final NodeDiscoveryService discovery = discoveryService;
|
||||
for (Node node : state.nodes) {
|
||||
logger.debug("Node {}. Processing with label: {}", node.nodeId, node.label);
|
||||
|
||||
final int nodeId = node.nodeId;
|
||||
|
||||
final @Nullable ZwaveNodeListener nodeListener = nodeListeners.get(nodeId);
|
||||
if (nodeListener == null) {
|
||||
if (Status.DEAD == node.status) {
|
||||
logger.warn("Node {}. Ignored due to state: {}", nodeId, node.status);
|
||||
continue;
|
||||
}
|
||||
logger.trace("Node {}. No listener, pass to discovery", nodeId);
|
||||
|
||||
if (discovery != null) {
|
||||
discovery.addNodeDiscovery(node);
|
||||
}
|
||||
}
|
||||
lastNodeStates.put(nodeId, node);
|
||||
lastNodeStatesCopy.remove(nodeId);
|
||||
}
|
||||
|
||||
// Check for removed nodes
|
||||
lastNodeStatesCopy.forEach((nodeId, node) -> {
|
||||
logger.trace("Node {}. Removed state is missing update", nodeId);
|
||||
lastNodeStates.remove(nodeId);
|
||||
|
||||
final ZwaveNodeListener nodeListener = nodeListeners.get(nodeId);
|
||||
if (nodeListener != null) {
|
||||
nodeListener.onNodeRemoved();
|
||||
}
|
||||
|
||||
if (discovery != null) {
|
||||
discovery.removeNodeDiscovery(nodeId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
* Initiates a full refresh of all data from the remote service.
|
||||
*
|
||||
*/
|
||||
public void getFullState() {
|
||||
if (getThing().getStatus().equals(ThingStatus.ONLINE)) {
|
||||
client.sendCommand(new ServerListeningCommand());
|
||||
}
|
||||
}
|
||||
|
||||
public void sendCommand(BaseCommand command) {
|
||||
if (getThing().getStatus().equals(ThingStatus.ONLINE)) {
|
||||
client.sendCommand(command);
|
||||
}
|
||||
}
|
||||
|
||||
public @Nullable Node requestNodeDetails(int nodeId) {
|
||||
Node node = lastNodeStates.get(nodeId);
|
||||
logger.debug("Node {}. Details requested, provided: {}", nodeId, node != null);
|
||||
return node;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerNodeListener(ZwaveNodeListener nodeListener) {
|
||||
final Integer id = nodeListener.getId();
|
||||
if (nodeListeners.put(id, nodeListener) != null) {
|
||||
logger.debug("Node {}. Registering listener", id);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean unregisterNodeListener(ZwaveNodeListener nodeListener) {
|
||||
logger.debug("Node {}. Unregistering listener", nodeListener.getId());
|
||||
return nodeListeners.remove(nodeListener.getId()) != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean registerDiscoveryListener(NodeDiscoveryService listener) {
|
||||
logger.debug("Registering Z-Wave discovery listener");
|
||||
if (discoveryService == null) {
|
||||
discoveryService = listener;
|
||||
getFullState();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean unregisterDiscoveryListener() {
|
||||
logger.debug("Unregistering Z-Wave discovery listener");
|
||||
if (discoveryService != null) {
|
||||
discoveryService = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Class<? extends ThingHandlerService>> getServices() {
|
||||
return Set.of(NodeDiscoveryService.class, ZwaveJSActions.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
stopInitialConnectionJob();
|
||||
client.stop();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConnectionError(String message) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, message);
|
||||
}
|
||||
|
||||
public void startInclusion() {
|
||||
sendCommand(new ControllerInclusionCommand(false));
|
||||
}
|
||||
|
||||
public void stopInclusion() {
|
||||
sendCommand(new ControllerInclusionCommand(true));
|
||||
}
|
||||
|
||||
public void startExclusion() {
|
||||
sendCommand(new ControllerExclusionCommand(false));
|
||||
}
|
||||
|
||||
public void stopExclusion() {
|
||||
sendCommand(new ControllerExclusionCommand(true));
|
||||
}
|
||||
|
||||
public void sendMulticastCommand(String nodeIDs, Integer commandClass, Integer endpoint, String property,
|
||||
String value) {
|
||||
sendCommand(new MulticastSetValueCommand(parseNodeIDs(nodeIDs), commandClass, endpoint, property,
|
||||
convertValueType(value)));
|
||||
}
|
||||
|
||||
private int[] parseNodeIDs(String nodeIDs) {
|
||||
return Arrays.stream(nodeIDs.split(",")).map(String::trim).filter(s -> !s.isEmpty()).mapToInt(s -> {
|
||||
try {
|
||||
return Integer.parseInt(s);
|
||||
} catch (NumberFormatException e) {
|
||||
logger.warn("Invalid node ID '{}' - skipping", s);
|
||||
return -1; // Use -1 as invalid marker
|
||||
}
|
||||
}).filter(id -> id > 0) // Filter out invalid IDs (-1 and 0)
|
||||
.toArray();
|
||||
}
|
||||
|
||||
private static Object convertValueType(String value) {
|
||||
try {
|
||||
return Double.parseDouble(value.trim());
|
||||
} catch (NumberFormatException e) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2025 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.zwavejs.internal.handler;
|
||||
|
||||
import static org.openhab.binding.zwavejs.internal.BindingConstants.*;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.zwavejs.internal.type.ZwaveJSTypeGenerator;
|
||||
import org.openhab.core.io.net.http.WebSocketFactory;
|
||||
import org.openhab.core.thing.Bridge;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
|
||||
import org.openhab.core.thing.binding.ThingHandler;
|
||||
import org.openhab.core.thing.binding.ThingHandlerFactory;
|
||||
import org.osgi.service.component.annotations.Activate;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
import org.osgi.service.component.annotations.Reference;
|
||||
|
||||
/**
|
||||
* The {@link ZwaveJSHandlerFactory} is responsible for creating things and thing
|
||||
* handlers.
|
||||
*
|
||||
* @author Leo Siepel - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
@Component(configurationPid = "binding.zwavejs", service = ThingHandlerFactory.class)
|
||||
public class ZwaveJSHandlerFactory extends BaseThingHandlerFactory {
|
||||
|
||||
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_GATEWAY, THING_TYPE_NODE);
|
||||
|
||||
private WebSocketFactory webSocketFactory;
|
||||
private ZwaveJSTypeGenerator typeGenerator;
|
||||
|
||||
@Activate
|
||||
public ZwaveJSHandlerFactory(final @Reference WebSocketFactory webSocketFactory,
|
||||
final @Reference ZwaveJSTypeGenerator typeGenerator) {
|
||||
this.webSocketFactory = webSocketFactory;
|
||||
this.typeGenerator = typeGenerator;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
|
||||
return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @Nullable ThingHandler createHandler(Thing thing) {
|
||||
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
|
||||
|
||||
if (THING_TYPE_GATEWAY.equals(thingTypeUID)) {
|
||||
return new ZwaveJSBridgeHandler((Bridge) thing, webSocketFactory);
|
||||
} else if (THING_TYPE_NODE.equals(thingTypeUID)) {
|
||||
return new ZwaveJSNodeHandler(thing, typeGenerator);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
+716
@@ -0,0 +1,716 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2025 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.zwavejs.internal.handler;
|
||||
|
||||
import static org.openhab.binding.zwavejs.internal.BindingConstants.*;
|
||||
import static org.openhab.binding.zwavejs.internal.CommandClassConstants.EQUIPMENT_MAP;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.measure.Unit;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.zwavejs.internal.api.dto.Event;
|
||||
import org.openhab.binding.zwavejs.internal.api.dto.Node;
|
||||
import org.openhab.binding.zwavejs.internal.api.dto.Status;
|
||||
import org.openhab.binding.zwavejs.internal.api.dto.commands.NodeGetValueCommand;
|
||||
import org.openhab.binding.zwavejs.internal.api.dto.commands.NodeSetValueCommand;
|
||||
import org.openhab.binding.zwavejs.internal.config.ColorCapability;
|
||||
import org.openhab.binding.zwavejs.internal.config.ZwaveJSBridgeConfiguration;
|
||||
import org.openhab.binding.zwavejs.internal.config.ZwaveJSChannelConfiguration;
|
||||
import org.openhab.binding.zwavejs.internal.config.ZwaveJSNodeConfiguration;
|
||||
import org.openhab.binding.zwavejs.internal.conversion.ChannelMetadata;
|
||||
import org.openhab.binding.zwavejs.internal.conversion.ConfigMetadata;
|
||||
import org.openhab.binding.zwavejs.internal.type.ZwaveJSTypeGenerator;
|
||||
import org.openhab.binding.zwavejs.internal.type.ZwaveJSTypeGeneratorResult;
|
||||
import org.openhab.core.config.core.Configuration;
|
||||
import org.openhab.core.config.core.validation.ConfigValidationException;
|
||||
import org.openhab.core.library.CoreItemFactory;
|
||||
import org.openhab.core.library.types.DateTimeType;
|
||||
import org.openhab.core.library.types.DecimalType;
|
||||
import org.openhab.core.library.types.HSBType;
|
||||
import org.openhab.core.library.types.IncreaseDecreaseType;
|
||||
import org.openhab.core.library.types.NextPreviousType;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.openhab.core.library.types.OpenClosedType;
|
||||
import org.openhab.core.library.types.PercentType;
|
||||
import org.openhab.core.library.types.PlayPauseType;
|
||||
import org.openhab.core.library.types.PointType;
|
||||
import org.openhab.core.library.types.QuantityType;
|
||||
import org.openhab.core.library.types.RewindFastforwardType;
|
||||
import org.openhab.core.library.types.StopMoveType;
|
||||
import org.openhab.core.library.types.StringListType;
|
||||
import org.openhab.core.library.types.StringType;
|
||||
import org.openhab.core.library.types.UpDownType;
|
||||
import org.openhab.core.semantics.SemanticTag;
|
||||
import org.openhab.core.thing.Bridge;
|
||||
import org.openhab.core.thing.Channel;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.thing.ThingStatus;
|
||||
import org.openhab.core.thing.ThingStatusDetail;
|
||||
import org.openhab.core.thing.ThingStatusInfo;
|
||||
import org.openhab.core.thing.binding.BaseThingHandler;
|
||||
import org.openhab.core.thing.binding.builder.ThingBuilder;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.core.types.RefreshType;
|
||||
import org.openhab.core.types.State;
|
||||
import org.openhab.core.types.UnDefType;
|
||||
import org.openhab.core.types.util.UnitUtils;
|
||||
import org.openhab.core.util.ColorUtil;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The {@link ZwaveJSNodeHandler} is responsible for handling commands, which are
|
||||
* sent to one of the channels.
|
||||
*
|
||||
* @author Leo Siepel - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class ZwaveJSNodeHandler extends BaseThingHandler implements ZwaveNodeListener {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(ZwaveJSNodeHandler.class);
|
||||
private final ZwaveJSTypeGenerator typeGenerator;
|
||||
private ZwaveJSNodeConfiguration config = new ZwaveJSNodeConfiguration();
|
||||
private boolean configurationAsChannels = false;
|
||||
protected ScheduledExecutorService executorService = scheduler;
|
||||
|
||||
// Nodes may contain multiple lighting endpoints; this map holds each one's ColorCapability.
|
||||
private Map<Integer, ColorCapability> colorCapabilities = new HashMap<>();
|
||||
|
||||
public ZwaveJSNodeHandler(final Thing thing, final ZwaveJSTypeGenerator typeGenerator) {
|
||||
super(thing);
|
||||
this.typeGenerator = typeGenerator;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleConfigurationUpdate(Map<String, Object> configurationParameters)
|
||||
throws ConfigValidationException {
|
||||
super.handleConfigurationUpdate(configurationParameters);
|
||||
logger.debug("Node {}. Configuration update: {}", config.id, configurationParameters.keySet());
|
||||
|
||||
ZwaveJSBridgeHandler handler = getBridgeHandler();
|
||||
if (handler == null || !handler.getThing().getStatus().equals(ThingStatus.ONLINE)) {
|
||||
logger.warn("Node {}. Bridge handler is null or Bridge is offline, cannot process configuration update",
|
||||
config.id);
|
||||
return;
|
||||
}
|
||||
|
||||
Node node = handler.requestNodeDetails(config.id);
|
||||
if (node == null) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
|
||||
"@text/offline.conf-error.no-node-details");
|
||||
return;
|
||||
}
|
||||
|
||||
ZwaveJSTypeGeneratorResult result;
|
||||
try {
|
||||
result = typeGenerator.generate(thing.getUID(), node, true);
|
||||
} catch (Exception e) {
|
||||
logger.warn("Node {}. Error generating type information during configuration update", config.id, e);
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
|
||||
"@text/offline.conf-error.build-channels-failed");
|
||||
return;
|
||||
}
|
||||
|
||||
// Process each configuration parameter update
|
||||
// TODO are we able to determine the changed parameters? The UI has a hold of 'dirty' parameters
|
||||
for (Entry<String, Object> configurationParameter : configurationParameters.entrySet()) {
|
||||
String key = configurationParameter.getKey();
|
||||
Object newValue = configurationParameter.getValue();
|
||||
|
||||
if (result.channels.containsKey(key)) {
|
||||
ZwaveJSChannelConfiguration channelConfig = Objects.requireNonNull(result.channels.get(key))
|
||||
.getConfiguration().as(ZwaveJSChannelConfiguration.class);
|
||||
NodeSetValueCommand zwaveCommand = new NodeSetValueCommand(config.id, channelConfig);
|
||||
zwaveCommand.value = newValue;
|
||||
handler.sendCommand(zwaveCommand);
|
||||
} else {
|
||||
logger.debug("Node {}. Configuration key '{}' not found in generated channels, skipping.", config.id,
|
||||
key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommand(ChannelUID channelUID, Command command) {
|
||||
logger.debug("Node {}. Processing command {} type {} for channel {}", config.id, command,
|
||||
command.getClass().getSimpleName(), channelUID);
|
||||
ZwaveJSBridgeHandler handler = getBridgeHandler();
|
||||
if (handler == null || !handler.getThing().getStatus().equals(ThingStatus.ONLINE)) {
|
||||
logger.warn("Node {}. Bridge handler is null or Bridge is offline, cannot process command", config.id);
|
||||
return;
|
||||
}
|
||||
|
||||
Channel channel = thing.getChannel(channelUID);
|
||||
if (channel == null) {
|
||||
logger.debug("Channel {} not found", channelUID);
|
||||
return;
|
||||
}
|
||||
|
||||
ZwaveJSChannelConfiguration channelConfig = channel.getConfiguration().as(ZwaveJSChannelConfiguration.class);
|
||||
// Handle RefreshType
|
||||
if (command instanceof RefreshType) {
|
||||
NodeGetValueCommand zwaveCommand = new NodeGetValueCommand(config.id, channelConfig);
|
||||
handler.sendCommand(zwaveCommand);
|
||||
return;
|
||||
}
|
||||
|
||||
NodeSetValueCommand zwaveCommand = new NodeSetValueCommand(config.id, channelConfig);
|
||||
|
||||
ColorCapability colorCap = colorCapabilities.get(channelConfig.endpoint);
|
||||
boolean isColorChannelCmd = colorCap != null && colorCap.colorChannels.contains(channelUID);
|
||||
boolean isColorTempChannelCmd = colorCap != null && channelUID.equals(colorCap.colorTempChannel);
|
||||
|
||||
if (command instanceof OnOffType onOffCommand) {
|
||||
zwaveCommand.value = handleOnOffTypeCommand(channelConfig, channel, colorCap, isColorChannelCmd,
|
||||
onOffCommand);
|
||||
} else if (command instanceof HSBType hsbTypeCommand) {
|
||||
zwaveCommand.value = handleHSBTypeCommand(channelConfig, channel, colorCap, isColorChannelCmd,
|
||||
hsbTypeCommand);
|
||||
} else if (command instanceof QuantityType<?> quantityCommand) {
|
||||
zwaveCommand.value = handleQuantityTypeCommand(channelConfig, quantityCommand);
|
||||
} else if (command instanceof PercentType percentTypeCommand) {
|
||||
zwaveCommand.value = handlePercentTypeCommand(channelConfig, channel, colorCap, isColorChannelCmd,
|
||||
isColorTempChannelCmd, percentTypeCommand);
|
||||
} else if (command instanceof DecimalType decimalCommand) {
|
||||
zwaveCommand.value = decimalCommand.doubleValue();
|
||||
} else if (command instanceof DateTimeType dateTimeCommand) {
|
||||
throw new UnsupportedOperationException(dateTimeCommand.toString() + " is currently not supported");
|
||||
} else if (command instanceof IncreaseDecreaseType increaseDecreaseCommand) {
|
||||
throw new UnsupportedOperationException(increaseDecreaseCommand.toString() + " is currently not supported");
|
||||
} else if (command instanceof NextPreviousType nextPreviousCommand) {
|
||||
throw new UnsupportedOperationException(nextPreviousCommand.toString() + " is currently not supported");
|
||||
} else if (command instanceof OpenClosedType openClosedCommand) {
|
||||
zwaveCommand.value = openClosedCommand == (channelConfig.inverted ? OpenClosedType.CLOSED
|
||||
: OpenClosedType.OPEN);
|
||||
} else if (command instanceof PlayPauseType playPauseCommand) {
|
||||
throw new UnsupportedOperationException(playPauseCommand.toString() + " is currently not supported");
|
||||
} else if (command instanceof PointType pointCommand) {
|
||||
throw new UnsupportedOperationException(pointCommand.toString() + " is currently not supported");
|
||||
} else if (command instanceof RewindFastforwardType rewindFastforwardCommand) {
|
||||
throw new UnsupportedOperationException(
|
||||
rewindFastforwardCommand.toString() + " is currently not supported");
|
||||
} else if (command instanceof StopMoveType stopMoveCommand) {
|
||||
throw new UnsupportedOperationException(stopMoveCommand.toString() + " is currently not supported");
|
||||
} else if (command instanceof StringListType stringListCommand) {
|
||||
throw new UnsupportedOperationException(stringListCommand.toString() + " is currently not supported");
|
||||
} else if (command instanceof UpDownType upDownCommand) {
|
||||
throw new UnsupportedOperationException(upDownCommand.toString() + " is currently not supported");
|
||||
} else if (command instanceof StringType stringCommand) {
|
||||
zwaveCommand.value = stringCommand.toString();
|
||||
}
|
||||
if (zwaveCommand.value != null) {
|
||||
handler.sendCommand(zwaveCommand);
|
||||
}
|
||||
}
|
||||
|
||||
private @Nullable Object handleOnOffTypeCommand(ZwaveJSChannelConfiguration channelConfig, Channel channel,
|
||||
@Nullable ColorCapability colorCap, boolean isColorChannelCommand, OnOffType onOffCommand) {
|
||||
// If this is a color channel, delegate to percent type logic (0% or 100%)
|
||||
if (isColorChannelCommand) {
|
||||
PercentType percent = (OnOffType.OFF == onOffCommand) ? PercentType.ZERO : PercentType.HUNDRED;
|
||||
return handlePercentTypeCommand(channelConfig, channel, colorCap, true, false, percent);
|
||||
}
|
||||
|
||||
// For dimmer channels, ON is mapped to 255, which means restore to the last brightness.
|
||||
if (CoreItemFactory.DIMMER.equals(channel.getAcceptedItemType())) {
|
||||
return (OnOffType.ON == onOffCommand) ? 255 : 0;
|
||||
}
|
||||
|
||||
// For other types, handle inversion if needed.
|
||||
return (onOffCommand == (channelConfig.inverted ? OnOffType.OFF : OnOffType.ON));
|
||||
}
|
||||
|
||||
private @Nullable Object handlePercentTypeCommand(ZwaveJSChannelConfiguration channelConfig, Channel channel,
|
||||
@Nullable ColorCapability colorCap, boolean isColorChannelCmd, boolean isColorTempChannelCmd,
|
||||
PercentType percentTypeCommand) {
|
||||
if (isColorChannelCmd && colorCap != null) {
|
||||
HSBType hsb = new HSBType(colorCap.cachedColor.getHue(), colorCap.cachedColor.getSaturation(),
|
||||
percentTypeCommand);
|
||||
return handleHSBTypeCommand(channelConfig, channel, colorCap, true, hsb);
|
||||
}
|
||||
|
||||
if (isColorTempChannelCmd && colorCap != null) {
|
||||
int byteValue = Math.max(0, Math.min(255, percentTypeCommand.intValue() * 255 / 100));
|
||||
if (colorCap.warmWhiteChannel instanceof ChannelUID warmWhiteChannel) {
|
||||
DecimalType warm = new DecimalType(byteValue);
|
||||
scheduler.submit(() -> handleCommand(warmWhiteChannel, warm));
|
||||
}
|
||||
if (colorCap.coldWhiteChannel instanceof ChannelUID coldWhiteChannel) {
|
||||
DecimalType cold = new DecimalType(255 - byteValue);
|
||||
scheduler.submit(() -> handleCommand(coldWhiteChannel, cold));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// For non-color channels, handle inversion and the dimmer 100% edge case.
|
||||
int value = percentTypeCommand.intValue();
|
||||
if (channelConfig.inverted) {
|
||||
value = 100 - value;
|
||||
}
|
||||
|
||||
// For dimmers, 100% is represented as 99 (zero based value).
|
||||
if (CoreItemFactory.DIMMER.equals(channel.getAcceptedItemType()) && value == 100) {
|
||||
value = 99;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private @Nullable Object handleHSBTypeCommand(ZwaveJSChannelConfiguration channelConfig, Channel channel,
|
||||
@Nullable ColorCapability colorCap, boolean isColorChannelCommand, HSBType hsbTypeCommand) {
|
||||
if (isColorChannelCommand && colorCap != null) {
|
||||
colorCap.cachedColor = hsbTypeCommand;
|
||||
}
|
||||
|
||||
HSBType hsbFull = new HSBType(hsbTypeCommand.getHue(), hsbTypeCommand.getSaturation(), PercentType.HUNDRED);
|
||||
int[] rgb = ColorUtil.hsbToRgb(hsbFull);
|
||||
|
||||
if (channel.getUID().getId().contains(HEX)) {
|
||||
return "%02X%02X%02X".formatted(rgb[0], rgb[1], rgb[2]);
|
||||
} else {
|
||||
Map<String, Integer> colorMap = new HashMap<>();
|
||||
colorMap.put(RED, rgb[0]);
|
||||
colorMap.put(GREEN, rgb[1]);
|
||||
colorMap.put(BLUE, rgb[2]);
|
||||
|
||||
if (colorCap != null) {
|
||||
if (colorCap.coldWhiteChannel != null) {
|
||||
colorMap.put(COLD_WHITE, 0);
|
||||
}
|
||||
if (colorCap.warmWhiteChannel != null) {
|
||||
colorMap.put(WARM_WHITE, 0);
|
||||
}
|
||||
}
|
||||
|
||||
if (isColorChannelCommand && colorCap != null
|
||||
&& colorCap.dimmerChannel instanceof ChannelUID dimmerChannel) {
|
||||
// Schedule brightness command(s) for dimmer channel(s)
|
||||
scheduler.submit(() -> handleCommand(dimmerChannel, hsbTypeCommand.getBrightness()));
|
||||
}
|
||||
return colorMap;
|
||||
}
|
||||
}
|
||||
|
||||
private @Nullable Object handleQuantityTypeCommand(ZwaveJSChannelConfiguration channelConfig,
|
||||
QuantityType<?> quantityCommand) {
|
||||
Unit<?> unit = UnitUtils.parseUnit(channelConfig.incomingUnit);
|
||||
if (unit == null) {
|
||||
logger.warn("Could not parse '{}' as a unit, this is a bug.", channelConfig.incomingUnit);
|
||||
return null;
|
||||
}
|
||||
|
||||
Double value = Objects.requireNonNull(quantityCommand.toUnit(unit)).doubleValue();
|
||||
if (channelConfig.factor != 1.0) {
|
||||
value = value / channelConfig.factor;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
ZwaveJSNodeConfiguration config = this.config = getConfigAs(ZwaveJSNodeConfiguration.class);
|
||||
|
||||
if (!config.isValid()) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
|
||||
"@text/offline.conf-error.id-invalid");
|
||||
return;
|
||||
}
|
||||
|
||||
updateStatus(ThingStatus.UNKNOWN);
|
||||
|
||||
executorService.execute(() -> {
|
||||
internalInitialize();
|
||||
});
|
||||
}
|
||||
|
||||
private @Nullable ZwaveJSBridgeHandler getBridgeHandler() {
|
||||
Bridge bridge = getBridge();
|
||||
if (bridge == null) {
|
||||
logger.trace("Node {}. Prevented initialisation as bridge is null", config.id);
|
||||
return null;
|
||||
}
|
||||
if (bridge.getHandler() instanceof ZwaveJSBridgeHandler handler) {
|
||||
return handler;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void internalInitialize() {
|
||||
ZwaveJSBridgeHandler handler = getBridgeHandler();
|
||||
if (handler == null) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!handler.getThing().getStatus().equals(ThingStatus.ONLINE)) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
|
||||
return;
|
||||
}
|
||||
handler.registerNodeListener(this);
|
||||
Node nodeDetails = handler.requestNodeDetails(config.id);
|
||||
if (nodeDetails == null) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
|
||||
"@text/offline.conf-error.no-node-details");
|
||||
return;
|
||||
}
|
||||
if (Status.DEAD == nodeDetails.status) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
|
||||
"@text/offline.comm-error.dead-node");
|
||||
return;
|
||||
}
|
||||
if (!setupThing(nodeDetails)) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
|
||||
"@text/offline.conf-error.build-channels-failed");
|
||||
return;
|
||||
}
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
|
||||
internalInitialize();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNodeRemoved() {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.GONE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNodeAdded(Node node) {
|
||||
// not supported, as this is a handler for an existing node
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onNodeStateChanged(Event event) {
|
||||
logger.trace("Node {}. State changed event", config.id);
|
||||
|
||||
// Handle configuration value updates
|
||||
if (!configurationAsChannels && CONFIGURATION_COMMAND_CLASSES.contains(event.args.commandClass)) {
|
||||
if (event.args.newValue == null) {
|
||||
logger.debug("Node {}. Configuration value not set, because it is null.", config.id);
|
||||
return false;
|
||||
}
|
||||
ConfigMetadata details = new ConfigMetadata(getId(), event);
|
||||
Configuration configuration = editConfiguration();
|
||||
configuration.put(details.id, event.args.newValue);
|
||||
updateConfiguration(configuration);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Handle channel state updates
|
||||
ChannelMetadata metadata = new ChannelMetadata(getId(), event);
|
||||
if (metadata.isIgnoredCommandClass(event.args.commandClassName) || !isLinked(metadata.id)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
logger.trace("Getting the configuration for linked channel {}", metadata.id);
|
||||
Channel channel = thing.getChannel(metadata.id);
|
||||
if (channel == null) {
|
||||
logger.debug("Node {}. Channel {} not found, ignoring event", config.id, metadata.id);
|
||||
return false;
|
||||
}
|
||||
|
||||
ZwaveJSChannelConfiguration channelConfig = channel.getConfiguration().as(ZwaveJSChannelConfiguration.class);
|
||||
|
||||
State state = metadata.setState(event.args.newValue, Objects.requireNonNull(channel.getAcceptedItemType()),
|
||||
channelConfig.incomingUnit, channelConfig.inverted);
|
||||
|
||||
if (state == null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Handle color and color temperature state updates
|
||||
ColorCapability colorCap = colorCapabilities.get(channelConfig.endpoint);
|
||||
state = handleColorUpdate(colorCap, state, channel, channelConfig);
|
||||
state = handleColorTemperatureUpdate(colorCap, state, channel, channelConfig);
|
||||
|
||||
try {
|
||||
updateState(metadata.id, state);
|
||||
} catch (IllegalArgumentException e) {
|
||||
logger.warn("Node {}. Error updating state for channel {} with value {}:{}. {}", event.nodeId, metadata.id,
|
||||
state.getClass().getSimpleName(), state.toFullString(), e.getMessage());
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* If the channel has a matching {@link ColorCapability} that supports color channel then either:
|
||||
*
|
||||
* <li>If the new {@link State} is an {@link HSBType} and the target channel is in the {@link ColorCapability}'s set
|
||||
* of color channels then return a new {@link HSBType} derived from the new HS parts plus the cached B part, or</li>
|
||||
*
|
||||
* <li>If the new {@link State} is a {@link PercentType} and the channel's accepted item type is 'Dimmer' then
|
||||
* update the {@link ColorCapability}'s cached B part, and notify all other channel's whose accepted item type is
|
||||
* 'Color' with the new HSB value.</li>
|
||||
* <p>
|
||||
*
|
||||
* @param colorCapability the colorCapability for this endpoint, which may be null
|
||||
* @param newState the incoming state from the web socket
|
||||
* @param targetChannel the target channel
|
||||
* @param channelConfig the target channelconfiguration
|
||||
*
|
||||
* @return either the incoming state or the newly updated one
|
||||
*/
|
||||
private State handleColorUpdate(@Nullable ColorCapability colorCapability, State newState, Channel targetChannel,
|
||||
ZwaveJSChannelConfiguration channelConfig) {
|
||||
if (colorCapability == null || colorCapability.colorChannels.isEmpty()) {
|
||||
return newState;
|
||||
}
|
||||
|
||||
ChannelUID targetUID = targetChannel.getUID();
|
||||
|
||||
if (colorCapability.colorChannels.contains(targetUID) && newState instanceof HSBType color) {
|
||||
colorCapability.cachedColor = new HSBType(color.getHue(), color.getSaturation(),
|
||||
colorCapability.cachedColor.getBrightness());
|
||||
return colorCapability.cachedColor;
|
||||
}
|
||||
|
||||
if (targetUID.equals(colorCapability.dimmerChannel) && newState instanceof Number brightness) {
|
||||
colorCapability.cachedColor = new HSBType(colorCapability.cachedColor.getHue(),
|
||||
colorCapability.cachedColor.getSaturation(), new PercentType(brightness.intValue()));
|
||||
colorCapability.colorChannels.forEach(c -> updateState(c, colorCapability.cachedColor));
|
||||
}
|
||||
|
||||
return newState;
|
||||
}
|
||||
|
||||
/**
|
||||
* If the channel has a matching {@link ColorCapability} that supports color temperature commands, the target
|
||||
* channel UID matches the ColorCapability's warm or cold white UID, and new {@link State} is a {@link Number}
|
||||
* then update the color temperature cache with the appropriate new value, and update the color temperature channel
|
||||
* respectively.
|
||||
*
|
||||
* @param colorCapability the colorCapability for this endpoint, which may be null
|
||||
* @param newState the incoming state from the web socket
|
||||
* @param targetChannel the target channel
|
||||
* @param channelConfig the target channelconfiguration
|
||||
*
|
||||
* @return the incoming state
|
||||
*/
|
||||
private State handleColorTemperatureUpdate(@Nullable ColorCapability colorCapability, State newState,
|
||||
Channel targetChannel, ZwaveJSChannelConfiguration channelConfig) {
|
||||
if (colorCapability == null || colorCapability.colorTempChannel == null) {
|
||||
return newState;
|
||||
}
|
||||
|
||||
boolean isWarmTarget = targetChannel.getUID().equals(colorCapability.warmWhiteChannel);
|
||||
boolean isColdTarget = targetChannel.getUID().equals(colorCapability.coldWhiteChannel);
|
||||
|
||||
if ((isWarmTarget || isColdTarget) && newState instanceof Number number) {
|
||||
if (isWarmTarget) {
|
||||
colorCapability.cachedWarmWhite = number;
|
||||
}
|
||||
if (isColdTarget) {
|
||||
colorCapability.cachedColdWhite = number;
|
||||
}
|
||||
|
||||
State colorTemp = UnDefType.UNDEF;
|
||||
int warm = colorCapability.cachedWarmWhite.intValue();
|
||||
int cold = colorCapability.cachedColdWhite.intValue();
|
||||
|
||||
int colorTempPercent = -1;
|
||||
if (warm > 0 && cold > 0) {
|
||||
colorTempPercent = warm * 100 / (warm + cold);
|
||||
} else if (warm >= 0) {
|
||||
colorTempPercent = warm * 100 / 255;
|
||||
} else if (cold >= 0) {
|
||||
colorTempPercent = (255 - cold) * 100 / 255;
|
||||
}
|
||||
|
||||
if (colorTempPercent >= 0) {
|
||||
colorTemp = new PercentType(Math.max(0, Math.min(100, colorTempPercent)));
|
||||
updateState(Objects.requireNonNull(colorCapability.colorTempChannel), colorTemp);
|
||||
} else {
|
||||
updateState(Objects.requireNonNull(colorCapability.colorTempChannel), UnDefType.UNDEF);
|
||||
}
|
||||
}
|
||||
|
||||
return newState;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNodeDead(Event event) {
|
||||
logger.trace("Node {}. Dead", config.id);
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "@text/offline.comm-error.dead-node");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNodeAlive(Event event) {
|
||||
logger.trace("Node {}. Alive", config.id);
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer getId() {
|
||||
return this.config.id;
|
||||
}
|
||||
|
||||
private boolean setupThing(Node node) {
|
||||
logger.debug("Node {}. Building channels and configuration, containing {} values", node.nodeId,
|
||||
node.values.size());
|
||||
|
||||
configurationAsChannels = Objects.requireNonNull(getBridge()).getConfiguration()
|
||||
.as(ZwaveJSBridgeConfiguration.class).configurationChannels;
|
||||
|
||||
ZwaveJSTypeGeneratorResult result;
|
||||
try {
|
||||
result = typeGenerator.generate(thing.getUID(), node, configurationAsChannels);
|
||||
} catch (Exception e) {
|
||||
logger.warn("Node {}. Error generating type information", node.nodeId, e);
|
||||
return false;
|
||||
}
|
||||
|
||||
ThingBuilder builder = editThing();
|
||||
|
||||
// Update location if needed
|
||||
if (!result.location.equals(getThing().getLocation()) && !result.location.isBlank()) {
|
||||
builder.withLocation(result.location);
|
||||
}
|
||||
|
||||
// Update channels
|
||||
builder = updateChannels(builder, result);
|
||||
|
||||
colorCapabilities = result.colorCapabilities;
|
||||
if (logger.isDebugEnabled()) {
|
||||
colorCapabilities.forEach((e, c) -> logger.debug("Node {}. Endpoint {}, {}", node.nodeId, e, c));
|
||||
}
|
||||
|
||||
updateThing(builder.build());
|
||||
|
||||
// Initialize state for channels and configuration
|
||||
initializeChannelAndConfigState(node, result);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private ThingBuilder updateChannels(ThingBuilder builder, ZwaveJSTypeGeneratorResult result) {
|
||||
List<Channel> channelsToRemove = new ArrayList<>();
|
||||
for (Channel channel : thing.getChannels()) {
|
||||
if (!result.channels.containsKey(channel.getUID().getId())) {
|
||||
channelsToRemove.add(channel);
|
||||
} else {
|
||||
result.channels.remove(channel.getUID().getId());
|
||||
}
|
||||
}
|
||||
if (!channelsToRemove.isEmpty()) {
|
||||
logger.trace("Node {}. Removing {} channels", this.config.id, channelsToRemove.size());
|
||||
builder.withoutChannels(channelsToRemove);
|
||||
}
|
||||
if (!result.channels.isEmpty()) {
|
||||
List<Channel> channels = result.channels.entrySet().stream().sorted(Map.Entry.comparingByKey())
|
||||
.map(Map.Entry::getValue).toList();
|
||||
logger.trace("Node {}. Adding {} channels", this.config.id, channels.size());
|
||||
builder.withChannels(channels);
|
||||
}
|
||||
if (!channelsToRemove.isEmpty() || !result.channels.isEmpty()) {
|
||||
SemanticTag equipmentTag = getEquipmentTag(builder.build().getChannels());
|
||||
if (equipmentTag != null) {
|
||||
logger.debug("Node {}. Setting semantic equipment tag {}", this.config.id, equipmentTag);
|
||||
builder.withSemanticEquipmentTag(equipmentTag);
|
||||
} else {
|
||||
logger.debug("Node {}. No semantic equipment tag set", this.config.id);
|
||||
}
|
||||
}
|
||||
return builder;
|
||||
}
|
||||
|
||||
private void initializeChannelAndConfigState(Node node, ZwaveJSTypeGeneratorResult result) {
|
||||
// Set initial state for linked channels
|
||||
for (Channel channel : thing.getChannels()) {
|
||||
if (result.values.containsKey(channel.getUID().getId()) && isLinked(channel.getUID())) {
|
||||
ChannelMetadata dummy = new ChannelMetadata(getId(), node.values.get(0));
|
||||
ZwaveJSChannelConfiguration channelConfig = channel.getConfiguration()
|
||||
.as(ZwaveJSChannelConfiguration.class);
|
||||
State state = dummy.setState(Objects.requireNonNull(result.values.get(channel.getUID().getId())),
|
||||
Objects.requireNonNull(channel.getAcceptedItemType()), channelConfig.incomingUnit,
|
||||
channelConfig.inverted);
|
||||
if (state != null) {
|
||||
// Initialize color and color temperature channels
|
||||
ColorCapability colorCap = colorCapabilities.get(channelConfig.endpoint);
|
||||
state = handleColorUpdate(colorCap, state, channel, channelConfig);
|
||||
state = handleColorTemperatureUpdate(colorCap, state, channel, channelConfig);
|
||||
try {
|
||||
updateState(channel.getUID(), state);
|
||||
} catch (IllegalArgumentException e) {
|
||||
logger.warn("Node {}. Error initializing state for channel {} with value {}:{}. {}",
|
||||
node.nodeId, channel.getUID().getId(), state.getClass().getSimpleName(),
|
||||
state.toFullString(), e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Set configuration items if not using configurationAsChannels
|
||||
if (!configurationAsChannels) {
|
||||
Configuration configuration = editConfiguration();
|
||||
List<String> channelIds = thing.getChannels().stream().map(c -> c.getUID().getId()).toList();
|
||||
|
||||
for (Entry<String, Object> entry : result.values.entrySet()) {
|
||||
if (!channelIds.contains(entry.getKey())) {
|
||||
logger.trace("Node {}. Setting configuration item {} to {}", node.nodeId, entry.getKey(),
|
||||
entry.getValue());
|
||||
try {
|
||||
Object entryValue = entry.getValue();
|
||||
if (entryValue instanceof Map map) {
|
||||
entryValue = map.toString();
|
||||
}
|
||||
configuration.put(entry.getKey(), entryValue);
|
||||
} catch (IllegalArgumentException e) {
|
||||
logger.warn("Node {}. Error setting configuration item {} to {}: {}", node.nodeId,
|
||||
entry.getKey(), entry.getValue(), e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
updateConfiguration(configuration);
|
||||
logger.debug("Node {}. Done values to configuration items", node.nodeId);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
Bridge bridge = getBridge();
|
||||
if (bridge != null && bridge.getHandler() instanceof ZwaveJSBridgeHandler handler) {
|
||||
handler.unregisterNodeListener(this);
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
private @Nullable SemanticTag getEquipmentTag(Collection<Channel> channels) {
|
||||
Set<Integer> commandClassIds = channels.stream()
|
||||
.map(channel -> channel.getConfiguration().as(ZwaveJSChannelConfiguration.class))
|
||||
.filter(Objects::nonNull).map(config -> Integer.valueOf(config.commandClassId))
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
// Find the first matching equipment tag based on command class IDs
|
||||
for (Map.Entry<Set<Integer>, SemanticTag> entry : EQUIPMENT_MAP.entrySet()) {
|
||||
if (commandClassIds.removeAll(entry.getKey())) {
|
||||
return entry.getValue();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2025 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.zwavejs.internal.handler;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.zwavejs.internal.api.dto.Event;
|
||||
import org.openhab.binding.zwavejs.internal.api.dto.Node;
|
||||
|
||||
/**
|
||||
* The {@link ZwaveNodeListener} interface defines the methods that must be implemented by any class
|
||||
* that wishes to receive notifications about changes to the state of a node, the addition
|
||||
* or removal of nodes, and other node-related events.
|
||||
*
|
||||
* @author Leo Siepel - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public interface ZwaveNodeListener {
|
||||
|
||||
/*
|
||||
* Retrieves the identifier of the node.
|
||||
*
|
||||
* @return the identifier of the node as an Integer.
|
||||
*/
|
||||
Integer getId();
|
||||
|
||||
/*
|
||||
* This method is called when the state of a node changes.
|
||||
*
|
||||
* @param event the event that contains information about the state change
|
||||
*
|
||||
* @return true if the state change was handled successfully, false otherwise
|
||||
*/
|
||||
boolean onNodeStateChanged(Event event);
|
||||
|
||||
/*
|
||||
* This method is called when the node is dead
|
||||
*
|
||||
* @param event the event that contains information about the status change
|
||||
*/
|
||||
void onNodeDead(Event event);
|
||||
|
||||
/*
|
||||
* This method is called when the node is alive
|
||||
*
|
||||
* @param event the event that contains information about the status change
|
||||
*/
|
||||
void onNodeAlive(Event event);
|
||||
|
||||
/*
|
||||
* This method is called when a node is removed from the Z-Wave network.
|
||||
*/
|
||||
void onNodeRemoved();
|
||||
|
||||
/*
|
||||
* This method is called when a new node is added to the Z-Wave network.
|
||||
*
|
||||
* @param node the node that has been added
|
||||
*/
|
||||
void onNodeAdded(Node node);
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2025 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.zwavejs.internal.type;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.core.thing.type.ChannelType;
|
||||
import org.openhab.core.thing.type.ChannelTypeProvider;
|
||||
|
||||
/**
|
||||
* This interface represents a provider for Z-Wave JS channel types.
|
||||
* It extends the {@link ChannelTypeProvider} interface and provides
|
||||
* additional functionality specific to Z-Wave JS.
|
||||
*
|
||||
* @see ChannelTypeProvider
|
||||
*
|
||||
* @author Leo Siepel - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public interface ZwaveJSChannelTypeProvider extends ChannelTypeProvider {
|
||||
|
||||
/*
|
||||
* Adds a new channel type to the provider.
|
||||
*
|
||||
* @param channelType the channel type to be added
|
||||
*/
|
||||
void addChannelType(ChannelType channelType);
|
||||
}
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2025 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.zwavejs.internal.type;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.core.storage.StorageService;
|
||||
import org.openhab.core.thing.ThingUID;
|
||||
import org.openhab.core.thing.binding.AbstractStorageBasedTypeProvider;
|
||||
import org.openhab.core.thing.type.ChannelType;
|
||||
import org.openhab.core.thing.type.ChannelTypeProvider;
|
||||
import org.osgi.service.component.annotations.Activate;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
import org.osgi.service.component.annotations.Reference;
|
||||
|
||||
/**
|
||||
* Implementation of the {@link ZwaveJSChannelTypeProvider} interface that provides
|
||||
* channel types for Z-Wave JS devices. This class extends the {@link AbstractStorageBasedTypeProvider}
|
||||
* to leverage storage-based channel type management.
|
||||
*
|
||||
* <p>
|
||||
* This class is registered as an OSGi component and provides services for the
|
||||
* {@link ZwaveJSChannelTypeProvider} interface.
|
||||
*
|
||||
* @see ZwaveJSChannelTypeProvider
|
||||
* @see ChannelTypeProvider
|
||||
* @see AbstractStorageBasedTypeProvider
|
||||
*
|
||||
* @author Leo Siepel - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
@Component(service = { ZwaveJSChannelTypeProvider.class, ChannelTypeProvider.class })
|
||||
public class ZwaveJSChannelTypeProviderImpl extends AbstractStorageBasedTypeProvider
|
||||
implements ZwaveJSChannelTypeProvider {
|
||||
|
||||
@Activate
|
||||
public ZwaveJSChannelTypeProviderImpl(@Reference StorageService storageService) {
|
||||
super(storageService);
|
||||
}
|
||||
|
||||
/*
|
||||
* Removes all channel types associated with the specified ThingUID.
|
||||
*
|
||||
* @param uid the ThingUID for which the channel types should be removed
|
||||
*/
|
||||
public void removeChannelTypesForThing(ThingUID uid) {
|
||||
String thingUid = uid.getAsString() + ":";
|
||||
getChannelTypes(null).stream().map(ChannelType::getUID).filter(c -> c.getAsString().startsWith(thingUid))
|
||||
.forEach(this::removeChannelType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addChannelType(ChannelType channelType) {
|
||||
this.putChannelType(channelType);
|
||||
}
|
||||
}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2025 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.zwavejs.internal.type;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.Locale;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.core.config.core.ConfigDescription;
|
||||
import org.openhab.core.config.core.ConfigDescriptionProvider;
|
||||
|
||||
/**
|
||||
* The {@code ZwaveJSConfigDescriptionProvider} interface extends the {@link ConfigDescriptionProvider}
|
||||
* and provides methods to add and retrieve configuration descriptions specific to Z-Wave JS.
|
||||
*
|
||||
* @see ConfigDescriptionProvider
|
||||
*
|
||||
* @author Leo Siepel - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public interface ZwaveJSConfigDescriptionProvider extends ConfigDescriptionProvider {
|
||||
|
||||
/*
|
||||
* Adds a configuration description to the provider.
|
||||
*
|
||||
* @param configDescription the configuration description to be added
|
||||
*/
|
||||
void addConfigDescription(ConfigDescription configDescription);
|
||||
|
||||
/*
|
||||
* Provides a {@link ConfigDescription} for the given URI.
|
||||
*
|
||||
* @param uri uri of the config description
|
||||
*
|
||||
* @param locale locale
|
||||
*
|
||||
* @return config description or null if no config description could be found
|
||||
*/
|
||||
@Override
|
||||
@Nullable
|
||||
ConfigDescription getConfigDescription(URI uri, @Nullable Locale locale);
|
||||
}
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2025 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.zwavejs.internal.type;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.core.config.core.ConfigDescription;
|
||||
import org.openhab.core.config.core.ConfigDescriptionProvider;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
|
||||
/**
|
||||
* Implementation of the {@link ZwaveJSConfigDescriptionProvider} interface.
|
||||
* This class provides configuration descriptions for Z-Wave JS.
|
||||
*
|
||||
* <p>
|
||||
* It maintains a map of configuration descriptions indexed by their URI.
|
||||
* </p>
|
||||
*
|
||||
* @author Leo Siepel - Initial contribution
|
||||
*/
|
||||
@Component(service = { ZwaveJSConfigDescriptionProvider.class, ConfigDescriptionProvider.class })
|
||||
@NonNullByDefault
|
||||
public class ZwaveJSConfigDescriptionProviderImpl implements ZwaveJSConfigDescriptionProvider {
|
||||
private Map<URI, ConfigDescription> configDescriptionsByURI = new HashMap<>();
|
||||
|
||||
@Override
|
||||
public Collection<ConfigDescription> getConfigDescriptions(@Nullable Locale locale) {
|
||||
return new ArrayList<ConfigDescription>(configDescriptionsByURI.values());
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public ConfigDescription getConfigDescription(URI uri, @Nullable Locale locale) {
|
||||
return configDescriptionsByURI.get(uri);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addConfigDescription(ConfigDescription configDescription) {
|
||||
configDescriptionsByURI.put(configDescription.getUID(), configDescription);
|
||||
}
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2025 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.zwavejs.internal.type;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.zwavejs.internal.api.dto.Node;
|
||||
import org.openhab.core.thing.ThingUID;
|
||||
|
||||
/**
|
||||
* Interface for generating ChannelTypes and ConfigDescriptions for a given Z-Wave JS node.
|
||||
*
|
||||
* @author Leo Siepel - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public interface ZwaveJSTypeGenerator {
|
||||
|
||||
/*
|
||||
* Generates a ZwaveJSTypeGeneratorResult based on the provided ThingUID, Node, and configurationAsChannels flag.
|
||||
*
|
||||
* @param thingUID the unique identifier of the thing
|
||||
*
|
||||
* @param node the node for which the type is being generated
|
||||
*
|
||||
* @param configurationAsChannels a flag indicating whether the configuration should be treated as channels
|
||||
*
|
||||
* @return a ZwaveJSTypeGeneratorResult containing the generated type information
|
||||
*/
|
||||
ZwaveJSTypeGeneratorResult generate(ThingUID thingUID, Node node, boolean configurationAsChannels);
|
||||
}
|
||||
+734
@@ -0,0 +1,734 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2025 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.zwavejs.internal.type;
|
||||
|
||||
import static org.openhab.binding.zwavejs.internal.BindingConstants.*;
|
||||
import static org.openhab.binding.zwavejs.internal.CommandClassConstants.COMMAND_CLASS_SWITCH_COLOR;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.zwavejs.internal.BindingConstants;
|
||||
import org.openhab.binding.zwavejs.internal.api.dto.Metadata;
|
||||
import org.openhab.binding.zwavejs.internal.api.dto.MetadataType;
|
||||
import org.openhab.binding.zwavejs.internal.api.dto.Node;
|
||||
import org.openhab.binding.zwavejs.internal.api.dto.Value;
|
||||
import org.openhab.binding.zwavejs.internal.config.ColorCapability;
|
||||
import org.openhab.binding.zwavejs.internal.config.ZwaveJSChannelConfiguration;
|
||||
import org.openhab.binding.zwavejs.internal.conversion.ChannelMetadata;
|
||||
import org.openhab.binding.zwavejs.internal.conversion.ConfigMetadata;
|
||||
import org.openhab.core.config.core.ConfigDescriptionBuilder;
|
||||
import org.openhab.core.config.core.ConfigDescriptionParameter;
|
||||
import org.openhab.core.config.core.ConfigDescriptionParameterBuilder;
|
||||
import org.openhab.core.config.core.Configuration;
|
||||
import org.openhab.core.config.core.ParameterOption;
|
||||
import org.openhab.core.library.CoreItemFactory;
|
||||
import org.openhab.core.semantics.SemanticTag;
|
||||
import org.openhab.core.semantics.model.DefaultSemanticTags.Point;
|
||||
import org.openhab.core.semantics.model.DefaultSemanticTags.Property;
|
||||
import org.openhab.core.thing.Channel;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
import org.openhab.core.thing.DefaultSystemChannelTypeProvider;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.thing.ThingRegistry;
|
||||
import org.openhab.core.thing.ThingUID;
|
||||
import org.openhab.core.thing.binding.builder.ChannelBuilder;
|
||||
import org.openhab.core.thing.type.ChannelType;
|
||||
import org.openhab.core.thing.type.ChannelTypeBuilder;
|
||||
import org.openhab.core.thing.type.ChannelTypeUID;
|
||||
import org.openhab.core.thing.type.StateChannelTypeBuilder;
|
||||
import org.openhab.core.types.StateDescriptionFragment;
|
||||
import org.osgi.service.component.annotations.Activate;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
import org.osgi.service.component.annotations.Reference;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Generates openHAB entities (Channel, ChannelType and ConfigDescription) based on the Z-Wave JS data.
|
||||
*
|
||||
* @see ChannelType
|
||||
* @see ChannelTypeBuilder
|
||||
* @see ChannelTypeUID
|
||||
* @see StateChannelTypeBuilder
|
||||
* @see ThingRegistry
|
||||
* @see ZwaveJSChannelTypeProvider
|
||||
* @see ZwaveJSConfigDescriptionProvider
|
||||
*
|
||||
* @author Leo Siepel - Initial contribution
|
||||
*/
|
||||
@Component
|
||||
@NonNullByDefault
|
||||
public class ZwaveJSTypeGeneratorImpl implements ZwaveJSTypeGenerator {
|
||||
|
||||
private static final Object CHANNEL_TYPE_VERSION = "5"; // when static configuration is changed, the version must be
|
||||
// changed as well to force new channel type generation
|
||||
private static final Map<String, SemanticTag> ITEM_TYPES_TO_PROPERTY_TAGS = new HashMap<>();
|
||||
static {
|
||||
ITEM_TYPES_TO_PROPERTY_TAGS.put("Number:ElectricCurrent", Property.CURRENT);
|
||||
ITEM_TYPES_TO_PROPERTY_TAGS.put("Number:ElectricPotential", Property.VOLTAGE);
|
||||
ITEM_TYPES_TO_PROPERTY_TAGS.put("Number:Energy", Property.ENERGY);
|
||||
ITEM_TYPES_TO_PROPERTY_TAGS.put("Number:Frequency", Property.FREQUENCY);
|
||||
ITEM_TYPES_TO_PROPERTY_TAGS.put("Number:Illuminance", Property.ILLUMINANCE);
|
||||
ITEM_TYPES_TO_PROPERTY_TAGS.put("Number:Power", Property.POWER);
|
||||
ITEM_TYPES_TO_PROPERTY_TAGS.put("Number:Pressure", Property.PRESSURE);
|
||||
ITEM_TYPES_TO_PROPERTY_TAGS.put("Number:Speed", Property.SPEED);
|
||||
ITEM_TYPES_TO_PROPERTY_TAGS.put("Number:Temperature", Property.TEMPERATURE);
|
||||
ITEM_TYPES_TO_PROPERTY_TAGS.put("Number:Time", Property.DURATION);
|
||||
}
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(ZwaveJSTypeGeneratorImpl.class);
|
||||
private final ThingRegistry thingRegistry;
|
||||
private final ZwaveJSChannelTypeProvider channelTypeProvider;
|
||||
private final ZwaveJSConfigDescriptionProvider configDescriptionProvider;
|
||||
|
||||
@Activate
|
||||
public ZwaveJSTypeGeneratorImpl(@Reference ZwaveJSChannelTypeProvider channelTypeProvider,
|
||||
@Reference ZwaveJSConfigDescriptionProvider configDescriptionProvider,
|
||||
@Reference ThingRegistry thingRegistry) {
|
||||
this.channelTypeProvider = channelTypeProvider;
|
||||
this.configDescriptionProvider = configDescriptionProvider;
|
||||
this.thingRegistry = thingRegistry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a Thing by its UID.
|
||||
*
|
||||
* @param thingUID the UID of the Thing
|
||||
* @return the Thing, or {@code null} if not found
|
||||
*/
|
||||
public @Nullable Thing getThing(ThingUID thingUID) {
|
||||
return thingRegistry.get(thingUID);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a ZwaveJSTypeGeneratorResult for the given ThingUID and Node.
|
||||
*
|
||||
* @param thingUID the ThingUID of the device
|
||||
* @param node the Node containing the values to be processed
|
||||
* @param configurationAsChannels flag indicating whether to treat configuration as channels
|
||||
* @return a ZwaveJSTypeGeneratorResult containing the generated channels and configuration descriptions
|
||||
*/
|
||||
@Override
|
||||
public ZwaveJSTypeGeneratorResult generate(ThingUID thingUID, Node node, boolean configurationAsChannels) {
|
||||
ZwaveJSTypeGeneratorResult result = new ZwaveJSTypeGeneratorResult();
|
||||
List<ConfigDescriptionParameter> configDescriptions = new ArrayList<>();
|
||||
URI uri = Objects.requireNonNull(getConfigDescriptionURI(thingUID, node));
|
||||
|
||||
for (Value value : node.values) {
|
||||
if (!configurationAsChannels && CONFIGURATION_COMMAND_CLASSES.contains(value.commandClass)) {
|
||||
ConfigMetadata metadata = new ConfigMetadata(node.nodeId, value);
|
||||
configDescriptions.add(createConfigDescription(metadata));
|
||||
if (!result.values.containsKey(metadata.id) && value.value != null) {
|
||||
result.values.put(metadata.id, value.value);
|
||||
}
|
||||
}
|
||||
ChannelMetadata metadata = new ChannelMetadata(node.nodeId, value);
|
||||
if (configurationAsChannels || !CONFIGURATION_COMMAND_CLASSES.contains(value.commandClass)) {
|
||||
result.channels = createChannel(thingUID, result, metadata, configDescriptionProvider);
|
||||
if (!metadata.isIgnoredCommandClass(value.commandClassName) && !result.values.containsKey(metadata.id)
|
||||
&& value.value != null) {
|
||||
result.values.put(metadata.id, value.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// cross link the ColorCapability dimmer channels to Dimmer type channels withing the same endpoint
|
||||
mapDimmerChannelsToColorCapabilities(result);
|
||||
|
||||
// add a color temperature channel if necessary
|
||||
addColorTemperatureChannel(thingUID, node, result);
|
||||
|
||||
logger.debug("Node {}. Generated {} channels and {} configDescriptions with URI {}", node.nodeId,
|
||||
result.channels.size(), configDescriptions.size(), uri);
|
||||
|
||||
configDescriptionProvider
|
||||
.addConfigDescription(ConfigDescriptionBuilder.create(uri).withParameters(configDescriptions).build());
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private ConfigDescriptionParameter createConfigDescription(ConfigMetadata details) {
|
||||
logger.trace("Node {}. createConfigDescriptions with Id: {}", details.nodeId, details.id);
|
||||
|
||||
ConfigDescriptionParameterBuilder parameterBuilder = ConfigDescriptionParameterBuilder
|
||||
.create(details.id, details.configType) //
|
||||
.withRequired(false) //
|
||||
.withLabel(details.label) //
|
||||
.withVerify(false) //
|
||||
.withUnit(null) //
|
||||
.withDescription(details.description);
|
||||
|
||||
if (details.unitSymbol != null) {
|
||||
parameterBuilder.withUnit(details.unitSymbol);
|
||||
}
|
||||
Map<String, String> optionList = details.optionList;
|
||||
if (optionList != null) {
|
||||
List<ParameterOption> options = new ArrayList<>();
|
||||
optionList.forEach((k, v) -> options.add(new ParameterOption(k, v)));
|
||||
parameterBuilder.withLimitToOptions(true);
|
||||
parameterBuilder.withMultiple(false);
|
||||
parameterBuilder.withContext("item");
|
||||
parameterBuilder.withOptions(options);
|
||||
}
|
||||
|
||||
return parameterBuilder.build();
|
||||
}
|
||||
|
||||
private Map<String, Channel> createChannel(ThingUID thingUID, ZwaveJSTypeGeneratorResult result,
|
||||
ChannelMetadata details, ZwaveJSConfigDescriptionProvider configDescriptionProvider) {
|
||||
if (details.isIgnoredCommandClass(details.commandClassName)) {
|
||||
logger.trace("Node {}. Ignoring channel with Id: {} (ignored command class)", details.nodeId, details.id);
|
||||
return result.channels;
|
||||
}
|
||||
logger.trace("Node {}. building channel with Id: {}", details.nodeId, details.id);
|
||||
logger.trace(" >> {}", details);
|
||||
|
||||
ChannelUID channelUID = new ChannelUID(thingUID, details.id);
|
||||
Configuration channelConfiguration = buildChannelConfiguration(details);
|
||||
|
||||
// Try to reuse or update an existing channel
|
||||
Channel existingChannel = result.channels.get(channelUID.getId());
|
||||
ChannelTypeUID channelTypeUID = generateChannelTypeUID(details);
|
||||
ChannelType channelType = getOrGenerate(channelTypeUID, details);
|
||||
|
||||
String label = details.label;
|
||||
String itemType = details.itemType;
|
||||
if (existingChannel != null) {
|
||||
// Update configuration and label if needed
|
||||
Configuration existingConfig = existingChannel.getConfiguration();
|
||||
updateReadWriteProperties(existingConfig, details);
|
||||
|
||||
if (details.writable) {
|
||||
label = existingChannel.getLabel() != null ? existingChannel.getLabel() : label;
|
||||
} else {
|
||||
// If the channel is not writable, we keep the existing item type
|
||||
itemType = existingChannel.getAcceptedItemType();
|
||||
}
|
||||
// If the channel type UID has changed and the channel is not writable, keep the old type UID
|
||||
if (!channelTypeUID.equals(existingChannel.getChannelTypeUID()) && !details.writable) {
|
||||
channelTypeUID = existingChannel.getChannelTypeUID();
|
||||
}
|
||||
channelConfiguration = existingConfig;
|
||||
logger.debug("Node {}. Channel {}: existing channel updated", details.nodeId, details.id);
|
||||
}
|
||||
|
||||
if (label == null || label.isBlank()) {
|
||||
label = "Unknown Channel";
|
||||
}
|
||||
|
||||
if (channelType == null) {
|
||||
logger.warn("Node {} Channel {}, ChannelType could not be found or generated, please report, this is a bug",
|
||||
details.nodeId, details.id);
|
||||
return result.channels;
|
||||
}
|
||||
|
||||
// Build the new or updated channel
|
||||
ChannelBuilder builder = ChannelBuilder.create(channelUID, itemType).withLabel(label)
|
||||
.withConfiguration(channelConfiguration).withType(channelTypeUID);
|
||||
|
||||
if (details.writable) {
|
||||
builder.withAcceptedItemType(channelType.getItemType());
|
||||
}
|
||||
|
||||
result.channels.put(details.id, builder.build());
|
||||
|
||||
// if necessary add or update the entry in our ZwaveJSTypeGeneratorResult's map of ColorCapabilities
|
||||
updateColorCapabilities(thingUID, details, result);
|
||||
|
||||
return result.channels;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the Configuration object for a channel based on ChannelMetadata.
|
||||
*
|
||||
* @param details the channel metadata
|
||||
* @return the Configuration object for the channel
|
||||
*/
|
||||
private Configuration buildChannelConfiguration(ChannelMetadata details) {
|
||||
Configuration config = new Configuration();
|
||||
config.put(BindingConstants.CONFIG_CHANNEL_INCOMING_UNIT, details.unitSymbol);
|
||||
config.put(BindingConstants.CONFIG_CHANNEL_COMMANDCLASS_ID, details.commandClassId);
|
||||
config.put(BindingConstants.CONFIG_CHANNEL_COMMANDCLASS_NAME, details.commandClassName);
|
||||
config.put(BindingConstants.CONFIG_CHANNEL_ENDPOINT, details.endpoint);
|
||||
if (details.propertyKey instanceof Number propertyInteger) {
|
||||
config.put(BindingConstants.CONFIG_CHANNEL_PROPERTY_KEY_INT, propertyInteger);
|
||||
} else if (details.propertyKey instanceof String propertyString) {
|
||||
config.put(BindingConstants.CONFIG_CHANNEL_PROPERTY_KEY_STR, propertyString);
|
||||
}
|
||||
config.put(BindingConstants.CONFIG_CHANNEL_FACTOR, details.factor);
|
||||
config.put(BindingConstants.CONFIG_CHANNEL_INVERTED, false);
|
||||
|
||||
updateReadWriteProperties(config, details);
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the read/write properties of an existing channel configuration.
|
||||
*
|
||||
* @param config the Configuration object to update
|
||||
* @param details the channel metadata containing the new properties
|
||||
*/
|
||||
private void updateReadWriteProperties(Configuration config, ChannelMetadata details) {
|
||||
if (details.writeProperty instanceof Number writePropertyInteger) {
|
||||
config.put(BindingConstants.CONFIG_CHANNEL_WRITE_PROPERTY_INT, writePropertyInteger);
|
||||
} else if (details.writeProperty instanceof String writePropertyString) {
|
||||
config.put(BindingConstants.CONFIG_CHANNEL_WRITE_PROPERTY_STR, writePropertyString);
|
||||
}
|
||||
if (details.readProperty != null) {
|
||||
config.put(BindingConstants.CONFIG_CHANNEL_READ_PROPERTY, String.valueOf(details.readProperty));
|
||||
}
|
||||
}
|
||||
|
||||
private @Nullable ChannelType getOrGenerate(ChannelTypeUID channelTypeUID, ChannelMetadata details) {
|
||||
ChannelType channelType = channelTypeProvider.getChannelType(channelTypeUID, null);
|
||||
if (channelType == null) {
|
||||
channelType = generateChannelType(details);
|
||||
if (channelType != null) {
|
||||
channelTypeProvider.addChannelType(channelType);
|
||||
}
|
||||
}
|
||||
if (channelType == null) {
|
||||
logger.warn("Node {} Channel {}, ChannelType could not be found or generated, please report, this is a bug",
|
||||
details.nodeId, details.id);
|
||||
return null;
|
||||
}
|
||||
return channelType;
|
||||
}
|
||||
|
||||
private ChannelTypeUID generateChannelTypeUID(ChannelMetadata details) {
|
||||
StringBuilder parts = new StringBuilder();
|
||||
parts.append(CHANNEL_TYPE_VERSION);
|
||||
parts.append(details.itemType);
|
||||
parts.append(details.unitSymbol);
|
||||
parts.append(details.writable);
|
||||
parts.append(details.isAdvanced);
|
||||
StateDescriptionFragment statePattern = details.statePattern;
|
||||
if (statePattern != null) {
|
||||
parts.append(statePattern.hashCode());
|
||||
}
|
||||
|
||||
try {
|
||||
MessageDigest messageDigest = MessageDigest.getInstance("MD5");
|
||||
byte[] array = messageDigest.digest(parts.toString().getBytes());
|
||||
StringBuilder stringBuilder = new StringBuilder();
|
||||
for (int i = 0; i < array.length; ++i) {
|
||||
stringBuilder.append(Integer.toHexString((array[i] & 0xFF) | 0x100).substring(1, 3));
|
||||
}
|
||||
return new ChannelTypeUID(BindingConstants.BINDING_ID, stringBuilder.toString());
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
logger.warn("NoSuchAlgorithmException error when calculating MD5 hash");
|
||||
}
|
||||
return new ChannelTypeUID(BindingConstants.BINDING_ID, "unknown");
|
||||
}
|
||||
|
||||
private @Nullable ChannelType generateChannelType(ChannelMetadata details) {
|
||||
final ChannelTypeUID channelTypeUID = generateChannelTypeUID(details);
|
||||
return generateChannelType(channelTypeUID, details);
|
||||
}
|
||||
|
||||
private ChannelType generateChannelType(ChannelTypeUID channelTypeUID, ChannelMetadata details) {
|
||||
StateChannelTypeBuilder builder = ChannelTypeBuilder.state(channelTypeUID, details.label, details.itemType);
|
||||
|
||||
if (details.description != null) {
|
||||
builder.withDescription(Objects.requireNonNull(details.description));
|
||||
}
|
||||
|
||||
if (details.statePattern != null) {
|
||||
builder.withStateDescriptionFragment(details.statePattern);
|
||||
}
|
||||
|
||||
if (details.unitSymbol != null) {
|
||||
builder.withUnitHint(details.unitSymbol);
|
||||
}
|
||||
|
||||
if (details.isInvertible()) {
|
||||
builder.withConfigDescriptionURI(URI.create("channel-type:zwavejs:invertible-channel"));
|
||||
} else {
|
||||
builder.withConfigDescriptionURI(URI.create("channel-type:zwavejs:base-channel"));
|
||||
}
|
||||
builder = setSemanticTags(builder, details);
|
||||
return builder.isAdvanced(details.isAdvanced).build();
|
||||
}
|
||||
|
||||
private @Nullable URI getConfigDescriptionURI(ThingUID thingUID, Node node) {
|
||||
Thing thing = getThing(thingUID);
|
||||
if (thing == null) {
|
||||
logger.debug("Thing '{}'' not found in registry for getConfigDescriptionURI", thingUID);
|
||||
return null;
|
||||
}
|
||||
ThingUID bridgeUID = thing.getBridgeUID();
|
||||
if (bridgeUID == null) {
|
||||
logger.debug("No bridgeUID found for Thing '{}'' in getConfigDescriptionURI", thingUID);
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return new URI(String.format("thing:%s:node:%s:node%s", BindingConstants.BINDING_ID, bridgeUID.getId(),
|
||||
node.nodeId));
|
||||
} catch (URISyntaxException ex) {
|
||||
logger.warn("Can't create configDescriptionURI for node {}", node.nodeId);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method for setSemanticTags(). Matches information in the {@link ChannelMetadata} against the given
|
||||
* keywords.
|
||||
*
|
||||
* @param keyWords the list of keywords to match
|
||||
* @param details the channel metadata to check
|
||||
* @return true if any keyword matches, false otherwise
|
||||
*/
|
||||
private static boolean match(List<String> keyWords, ChannelMetadata details) {
|
||||
List<String> sourceTexts = details.description instanceof String description
|
||||
? List.of(details.label, description)
|
||||
: List.of(details.label);
|
||||
for (String keyWord : keyWords) {
|
||||
String keyWordLowercase = keyWord.toLowerCase();
|
||||
for (String sourceText : sourceTexts) {
|
||||
String sourceTextLowercase = sourceText.toLowerCase();
|
||||
if (sourceTextLowercase.contains(keyWordLowercase)) {
|
||||
return true;
|
||||
}
|
||||
if (sourceTextLowercase.endsWith(keyWordLowercase.stripTrailing())) {
|
||||
return true;
|
||||
}
|
||||
if (sourceTextLowercase.startsWith(keyWordLowercase.stripLeading())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the channel's Point and Property tags.
|
||||
*
|
||||
* @param builder the StateChannelTypeBuilder to update
|
||||
* @param details the channel metadata
|
||||
* @return the updated StateChannelTypeBuilder
|
||||
*/
|
||||
private StateChannelTypeBuilder setSemanticTags(StateChannelTypeBuilder builder, ChannelMetadata details) {
|
||||
SemanticTag point = null;
|
||||
SemanticTag property = null;
|
||||
switch (details.itemType) {
|
||||
case CoreItemFactory.COLOR:
|
||||
point = details.writable ? Point.CONTROL : Point.STATUS;
|
||||
property = Property.COLOR;
|
||||
case CoreItemFactory.DIMMER:
|
||||
point = Point.CONTROL;
|
||||
break;
|
||||
case CoreItemFactory.STRING:
|
||||
point = details.writable ? Point.CONTROL : Point.STATUS;
|
||||
break;
|
||||
case CoreItemFactory.SWITCH:
|
||||
point = details.writable ? Point.SWITCH
|
||||
: match(List.of("alarm", "error", "warning", "fault"), details) ? Point.ALARM : Point.STATUS;
|
||||
break;
|
||||
case CoreItemFactory.CONTACT:
|
||||
point = match(List.of("alarm", "error", "warning", "fault"), details) ? Point.ALARM : Point.STATUS;
|
||||
property = Property.OPEN_STATE;
|
||||
break;
|
||||
case CoreItemFactory.DATETIME:
|
||||
point = details.writable ? Point.CONTROL : Point.STATUS;
|
||||
property = Property.TIMESTAMP;
|
||||
break;
|
||||
case CoreItemFactory.LOCATION:
|
||||
point = details.writable ? Point.CONTROL : Point.STATUS;
|
||||
property = Property.GEO_LOCATION;
|
||||
break;
|
||||
case CoreItemFactory.PLAYER:
|
||||
point = Point.CONTROL;
|
||||
property = Property.MEDIA_CONTROL;
|
||||
break;
|
||||
case CoreItemFactory.ROLLERSHUTTER:
|
||||
point = Point.CONTROL;
|
||||
property = Property.OPEN_LEVEL;
|
||||
break;
|
||||
case CoreItemFactory.NUMBER:
|
||||
point = details.writable ? Point.CONTROL : Point.MEASUREMENT;
|
||||
break;
|
||||
default:
|
||||
// this handles the case of Number:Dimension
|
||||
if (details.itemType.startsWith(CoreItemFactory.NUMBER)) {
|
||||
point = details.writable ? Point.SETPOINT : Point.MEASUREMENT;
|
||||
property = ITEM_TYPES_TO_PROPERTY_TAGS.get(details.itemType);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// estimate property tag if missing
|
||||
if (point != null && property == null) {
|
||||
if (Point.SWITCH == point && match(List.of("mode ", "auto", "manual", "enable", "disable", "lock", " day ",
|
||||
"night", "standby", "lock", "away"), details)) {
|
||||
property = Property.MODE;
|
||||
} else if (Point.STATUS == point && match(List.of("mode ", "auto", "manual", "enable", "disable", "lock",
|
||||
" day ", "night", "standby", "lock", "away"), details)) {
|
||||
property = Property.MODE;
|
||||
} else if (match(List.of("air"), details) && match(List.of("flow"), details)) {
|
||||
property = Property.AIRFLOW;
|
||||
} else if (match(List.of("air"), details) && match(List.of("quality"), details)) {
|
||||
property = Property.AIR_QUALITY;
|
||||
} else if (match(List.of(" nox ", "no2", "no₂"), details)) {
|
||||
property = Property.AIR_QUALITY;
|
||||
} else if (match(List.of("aqi"), details)) {
|
||||
property = Property.AQI;
|
||||
} else if (match(List.of("bright", "dimm"), details)) {
|
||||
property = Property.BRIGHTNESS;
|
||||
} else if (match(List.of(" co ", "(co)", "monoxide"), details)) {
|
||||
property = Property.CO;
|
||||
} else if (match(List.of("co2", "co₂", "dioxide"), details)) {
|
||||
property = Property.CO2;
|
||||
} else if (match(List.of("color", "colour"), details) && match(List.of("temp"), details)) {
|
||||
property = Property.COLOR_TEMPERATURE;
|
||||
} else if (match(List.of("color", "colour"), details)) {
|
||||
property = Property.COLOR;
|
||||
} else if (match(List.of("kelvin", "mirek", "mired"), details)) {
|
||||
property = Property.COLOR_TEMPERATURE;
|
||||
} else if (match(List.of("duration"), details)) {
|
||||
property = Property.DURATION;
|
||||
} else if (match(List.of("energy"), details)) {
|
||||
property = Property.ENERGY;
|
||||
} else if (match(List.of("fan"), details)) {
|
||||
property = Property.SPEED;
|
||||
} else if (match(List.of("freq"), details)) {
|
||||
property = Property.FREQUENCY;
|
||||
} else if (match(List.of("gas"), details)) {
|
||||
property = Property.GAS;
|
||||
} else if (match(List.of("location"), details)) {
|
||||
property = Property.GEO_LOCATION;
|
||||
} else if (match(List.of("heat"), details)) {
|
||||
property = Property.HEATING;
|
||||
} else if (match(List.of("conditioning", "cooling", " ac "), details)) {
|
||||
property = Property.AIRCONDITIONING;
|
||||
} else if (match(List.of("humidity"), details) && !match(List.of("soil"), details)) {
|
||||
property = Property.HUMIDITY;
|
||||
} else if (match(List.of("light"), details) && match(List.of("level"), details)) {
|
||||
property = Property.ILLUMINANCE;
|
||||
} else if (match(List.of("illuminance", " lux "), details)) {
|
||||
property = Property.ILLUMINANCE;
|
||||
} else if (match(List.of("battery"), details) && match(List.of("low", "alarm"), details)) {
|
||||
property = Property.LOW_BATTERY;
|
||||
} else if (Point.ALARM == point && match(List.of("battery"), details)) {
|
||||
property = Property.LOW_BATTERY;
|
||||
} else if (match(List.of("battery"), details)) {
|
||||
property = Property.ENERGY;
|
||||
} else if (match(List.of("media", "player", "television", " tv ", "receiver"), details)) {
|
||||
property = Property.INFO;
|
||||
} else if (match(List.of("soil"), details) && match(List.of("humidity"), details)) {
|
||||
property = Property.MOISTURE;
|
||||
} else if (match(List.of("moisture"), details)) {
|
||||
property = Property.MOISTURE;
|
||||
} else if (match(List.of("motion", " pir "), details)) {
|
||||
property = Property.MOTION;
|
||||
} else if (match(List.of("noise"), details)) {
|
||||
property = Property.NOISE;
|
||||
} else if (match(List.of(" oil "), details)) {
|
||||
property = Property.OIL;
|
||||
} else if (match(List.of("open", "close", " shut "), details)
|
||||
&& (match(List.of("state", "status", "level", "position"), details))) {
|
||||
property = Property.OPEN_LEVEL;
|
||||
} else if (match(List.of("open", "close", " shut "), details)) {
|
||||
property = Property.OPENING;
|
||||
} else if (match(List.of("ozone", "o3", "o₃"), details)) {
|
||||
property = Property.OZONE;
|
||||
} else if (match(List.of("partic", " pm ", "dust"), details)) {
|
||||
property = Property.PARTICULATE_MATTER;
|
||||
} else if (match(List.of("pollen"), details)) {
|
||||
property = Property.POLLEN;
|
||||
} else if (match(List.of("position"), details)) {
|
||||
property = Property.POSITION;
|
||||
} else if (match(List.of("wall"), details) && !match(List.of("socket", "outlet", "plug "), details)) {
|
||||
property = Property.LIGHT;
|
||||
} else if (match(List.of("light", "luminaire", "lamp", "bulb", " led "), details)) {
|
||||
property = Property.LIGHT;
|
||||
} else if (match(List.of("power", "on-off", " off ", "on/off", "relay", "outlet", "plug "), details)) {
|
||||
property = Property.POWER;
|
||||
} else if (match(List.of("presence", " occup"), details)) {
|
||||
property = Property.PRESENCE;
|
||||
} else if (match(List.of("pressure"), details)) {
|
||||
property = Property.PRESSURE;
|
||||
} else if (match(List.of("quality "), details)) {
|
||||
property = Property.QUALITY_OF_SERVICE;
|
||||
} else if (match(List.of("radon"), details)) {
|
||||
property = Property.RADON;
|
||||
} else if (match(List.of(" rain", "precip"), details)) {
|
||||
property = Property.RAIN;
|
||||
} else if (match(List.of("rssi"), details)) {
|
||||
property = Property.RSSI;
|
||||
} else if (match(List.of("signal"), details) && match(List.of("strength"), details)) {
|
||||
property = Property.SIGNAL_STRENGTH;
|
||||
} else if (match(List.of(" rf ", " dbw "), details)) {
|
||||
property = Property.SIGNAL_STRENGTH;
|
||||
} else if (match(List.of("smoke", "fire"), details)) {
|
||||
property = Property.SMOKE;
|
||||
} else if (match(List.of("sound", "audio", "noise", "decibel", " db ", "mute", " dba "), details)) {
|
||||
property = Property.SOUND_VOLUME;
|
||||
} else if (match(List.of("speed", "velocity"), details)) {
|
||||
property = Property.SPEED;
|
||||
} else if (match(List.of("tamper"), details)) {
|
||||
property = Property.TAMPERED;
|
||||
} else if (match(List.of("tilt", "vane", "slat"), details)) {
|
||||
property = Property.TILT;
|
||||
} else if (match(List.of("stamp "), details)) {
|
||||
property = Property.TIMESTAMP;
|
||||
} else if (match(List.of("violet", " uv "), details)) {
|
||||
property = Property.ULTRAVIOLET;
|
||||
} else if (match(List.of("ventilat", " fan "), details)) {
|
||||
property = Property.VENTILATION;
|
||||
} else if (match(List.of("vibration"), details)) {
|
||||
property = Property.VIBRATION;
|
||||
} else if (match(List.of("volatile", " voc "), details)) {
|
||||
property = Property.VOC;
|
||||
} else if (match(List.of("water", "leak", "flood"), details)) {
|
||||
property = Property.WATER;
|
||||
} else if (match(List.of("wind"), details)) {
|
||||
property = Property.WIND;
|
||||
} else if (match(List.of("level"), details)) {
|
||||
property = Property.LEVEL;
|
||||
} else if (Point.CONTROL == point && match(List.of("application", " app "), details)) {
|
||||
property = Property.APP;
|
||||
} else if (Point.CONTROL == point && match(List.of("channel"), details)) {
|
||||
property = Property.CHANNEL;
|
||||
}
|
||||
}
|
||||
|
||||
if (point != null) {
|
||||
if (property == null) {
|
||||
builder.withTags(point);
|
||||
} else {
|
||||
builder.withTags(point, property);
|
||||
}
|
||||
}
|
||||
return builder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterates over the {@link ZwaveJSTypeGeneratorResult}'s map of {@link ColorCapability} to find endpoints which
|
||||
* support color temperature, and if found, adds a respective color temperature channel to the
|
||||
* {@link ZwaveJSTypeGeneratorResult}'s map of {@link Channel}.
|
||||
*
|
||||
* @param thingUID the ThingUID
|
||||
* @param node the Node
|
||||
* @param result the ZwaveJSTypeGeneratorResult that provides the inputs and receives the results
|
||||
*/
|
||||
private void addColorTemperatureChannel(ThingUID thingUID, Node node, ZwaveJSTypeGeneratorResult result) {
|
||||
result.colorCapabilities.forEach((endPoint, colorCapability) -> {
|
||||
if (colorCapability.colorTempChannel != null
|
||||
|| (colorCapability.coldWhiteChannel == null && colorCapability.warmWhiteChannel == null)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Value value = new Value();
|
||||
// populate minimum required fields; the system channel type provides the rest
|
||||
value.endpoint = endPoint;
|
||||
value.commandClass = COMMAND_CLASS_SWITCH_COLOR;
|
||||
value.commandClassName = COLOR_TEMP_CHANNEL_COMMAND_CLASS_NAME;
|
||||
value.propertyName = COLOR_TEMP_CHANNEL_PROPERTY_NAME;
|
||||
value.metadata = new Metadata();
|
||||
value.metadata.type = MetadataType.NUMBER;
|
||||
value.metadata.writeable = true;
|
||||
value.metadata.readable = true;
|
||||
value.value = 0;
|
||||
|
||||
ChannelMetadata details = new ChannelMetadata(node.nodeId, value);
|
||||
logger.trace("Node {} building channel with Id: {}", details.nodeId, details.id);
|
||||
logger.trace(" >> {}", details);
|
||||
|
||||
ChannelType type = DefaultSystemChannelTypeProvider.SYSTEM_COLOR_TEMPERATURE;
|
||||
Configuration config = buildChannelConfiguration(details);
|
||||
|
||||
Channel channel = ChannelBuilder.create(new ChannelUID(thingUID, details.id), type.getItemType())
|
||||
.withType(type.getUID()) //
|
||||
.withDefaultTags(type.getTags()) //
|
||||
.withKind(type.getKind()) //
|
||||
.withLabel(type.getLabel()) //
|
||||
.withDescription(Objects.requireNonNull(type.getDescription())) //
|
||||
.withAutoUpdatePolicy(type.getAutoUpdatePolicy()) //
|
||||
.withConfiguration(config) //
|
||||
.build();
|
||||
|
||||
result.channels.put(details.id, channel);
|
||||
colorCapability.colorTempChannel = channel.getUID();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterates over the {@link ZwaveJSTypeGeneratorResult}'s map of {@link Channel} to find ones with the accepted Item
|
||||
* type 'Dimmer' and, if such a channel's endpoint has an entry in the {@link ZwaveJSTypeGeneratorResult}'s map of
|
||||
* {@link ColorCapability}, adds that channel to the respective ColorCapability's set of dimmer channels.
|
||||
*
|
||||
* @param result ZwaveJSTypeGeneratorResult that provides the inputs and receives the results
|
||||
*/
|
||||
private void mapDimmerChannelsToColorCapabilities(ZwaveJSTypeGeneratorResult result) {
|
||||
if (!result.colorCapabilities.isEmpty()) {
|
||||
result.channels.values().stream() //
|
||||
.filter(c -> CoreItemFactory.DIMMER.equals(c.getAcceptedItemType())) //
|
||||
.forEach(c -> {
|
||||
ZwaveJSChannelConfiguration config = c.getConfiguration().as(ZwaveJSChannelConfiguration.class);
|
||||
if (result.colorCapabilities.get(config.endpoint) instanceof ColorCapability colorCapability) {
|
||||
colorCapability.dimmerChannel = c.getUID();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the {@link ChannelMetadata} to determine the {@link ColorCapability}, if any, and updates the given
|
||||
* {@link ZwaveJSTypeGeneratorResult}'s colorCapabilities map accordingly.
|
||||
*
|
||||
* @param thingUID the ThingUID
|
||||
* @param details the channel creation metadata
|
||||
* @param result the ZwaveJSTypeGeneratorResult that receives the results
|
||||
*/
|
||||
private void updateColorCapabilities(ThingUID thingUID, ChannelMetadata details,
|
||||
ZwaveJSTypeGeneratorResult result) {
|
||||
if (details.commandClassId != COMMAND_CLASS_SWITCH_COLOR) {
|
||||
return;
|
||||
}
|
||||
|
||||
boolean isColor = (details.value instanceof Map map && map.containsKey(GREEN)) //
|
||||
|| details.id.contains(HEX);
|
||||
|
||||
int propertyKey = details.propertyKey instanceof Number n ? n.intValue()
|
||||
: details.propertyKey instanceof String s ? Integer.valueOf(s) : -1;
|
||||
|
||||
boolean isColdWhite = propertyKey == COLD_PROPERTY_KEY;
|
||||
boolean isWarmWhite = propertyKey == WARM_PROPERTY_KEY;
|
||||
|
||||
if (isColor || isColdWhite || isWarmWhite) {
|
||||
ColorCapability colorCapability = result.colorCapabilities.getOrDefault(details.endpoint,
|
||||
new ColorCapability());
|
||||
if (isColor) {
|
||||
colorCapability.colorChannels.add(new ChannelUID(thingUID, details.id));
|
||||
}
|
||||
if (isColdWhite) {
|
||||
colorCapability.coldWhiteChannel = new ChannelUID(thingUID, details.id);
|
||||
}
|
||||
if (isWarmWhite) {
|
||||
colorCapability.warmWhiteChannel = new ChannelUID(thingUID, details.id);
|
||||
}
|
||||
result.colorCapabilities.put(details.endpoint, colorCapability);
|
||||
}
|
||||
}
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2025 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.zwavejs.internal.type;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.zwavejs.internal.config.ColorCapability;
|
||||
import org.openhab.core.thing.Channel;
|
||||
|
||||
/**
|
||||
* This class represents the result of the ZwaveJS type generator.
|
||||
* It contains a map of channels, where the key is a string and the value is a Channel object.
|
||||
*
|
||||
* @see Channel
|
||||
*
|
||||
* @author Leo Siepel - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class ZwaveJSTypeGeneratorResult {
|
||||
|
||||
public Map<String, Channel> channels = new HashMap<>();
|
||||
public Map<String, Object> values = new HashMap<>();
|
||||
public Map<Integer, ColorCapability> colorCapabilities = new HashMap<>();
|
||||
public String location = "";
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<addon:addon id="zwavejs" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:addon="https://openhab.org/schemas/addon/v1.0.0"
|
||||
xsi:schemaLocation="https://openhab.org/schemas/addon/v1.0.0 https://openhab.org/schemas/addon-1.0.0.xsd">
|
||||
|
||||
<type>binding</type>
|
||||
<name>Z-Wave JS Binding</name>
|
||||
<description>This is the binding for Z-Wave JS.</description>
|
||||
<connection>local</connection>
|
||||
|
||||
<discovery-methods>
|
||||
<discovery-method>
|
||||
<service-type>mdns</service-type>
|
||||
<discovery-parameters>
|
||||
<discovery-parameter>
|
||||
<name>mdnsServiceType</name>
|
||||
<value>_zwave-js-server._tcp.local.</value>
|
||||
</discovery-parameter>
|
||||
</discovery-parameters>
|
||||
</discovery-method>
|
||||
</discovery-methods>
|
||||
</addon:addon>
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<config-description:config-descriptions
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0"
|
||||
xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0 https://openhab.org/schemas/config-description-1.0.0.xsd">
|
||||
|
||||
<config-description uri="channel-type:zwavejs:base-channel">
|
||||
<parameter name="incomingUnit" type="text">
|
||||
<label>Incoming Unit</label>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
<parameter name="commandClassId" type="integer">
|
||||
<label>Command Class Id</label>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
<parameter name="commandClassName" type="text">
|
||||
<label>Command Class Name</label>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
<parameter name="endpoint" type="integer">
|
||||
<label>Endpoint Index</label>
|
||||
<advanced>true</advanced>
|
||||
<default>0</default>
|
||||
</parameter>
|
||||
<parameter name="factor" type="decimal">
|
||||
<label>Factor</label>
|
||||
<advanced>true</advanced>
|
||||
<default>1</default>
|
||||
</parameter>
|
||||
<parameter name="propertyKeyStr" type="text">
|
||||
<label>Property Key Str</label>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
<parameter name="propertyKeyInt" type="integer">
|
||||
<label>Property Key Int</label>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
<parameter name="readProperty" type="text">
|
||||
<label>Read Property</label>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
<parameter name="writePropertyStr" type="text">
|
||||
<label>Write Property Str</label>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
<parameter name="writePropertyInt" type="integer">
|
||||
<label>Write Property Int</label>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
</config-description>
|
||||
|
||||
<config-description uri="channel-type:zwavejs:invertible-channel">
|
||||
<parameter name="incomingUnit" type="text">
|
||||
<label>Incoming Unit</label>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
<parameter name="commandClassId" type="integer">
|
||||
<label>Command Class Id</label>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
<parameter name="commandClassName" type="text">
|
||||
<label>Command Class Name</label>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
<parameter name="endpoint" type="integer">
|
||||
<label>Endpoint Index</label>
|
||||
<advanced>true</advanced>
|
||||
<default>0</default>
|
||||
</parameter>
|
||||
<parameter name="factor" type="decimal">
|
||||
<label>Factor</label>
|
||||
<advanced>true</advanced>
|
||||
<default>1</default>
|
||||
</parameter>
|
||||
<parameter name="propertyKeyStr" type="text">
|
||||
<label>Property Key Str</label>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
<parameter name="propertyKeyInt" type="integer">
|
||||
<label>Property Key Int</label>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
<parameter name="readProperty" type="text">
|
||||
<label>Read Property</label>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
<parameter name="writePropertyStr" type="text">
|
||||
<label>Write Property Str</label>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
<parameter name="writePropertyInt" type="integer">
|
||||
<label>Write Property Int</label>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
<parameter name="inverted" type="boolean">
|
||||
<label>Inverted</label>
|
||||
<description>When True the value will be inverted for OpenClose, OnOff and Percentage types.</description>
|
||||
<default>false</default>
|
||||
</parameter>
|
||||
</config-description>
|
||||
</config-description:config-descriptions>
|
||||
@@ -0,0 +1,66 @@
|
||||
# add-on
|
||||
|
||||
addon.zwavejs.name = Z-Wave JS Binding
|
||||
addon.zwavejs.description = This is the binding for Z-Wave JS.
|
||||
|
||||
# thing types
|
||||
|
||||
thing-type.zwavejs.gateway.label = Z-Wave JS Gateway
|
||||
thing-type.zwavejs.gateway.description = The Z-Wave JS server that acts as a bridge to the Z-Wave network
|
||||
thing-type.zwavejs.node.label = Z-Wave node
|
||||
thing-type.zwavejs.node.description = A Z-Wave node provided by Zwave JS
|
||||
|
||||
# thing types config
|
||||
|
||||
thing-type.config.zwavejs.gateway.configurationChannels.label = Configuration As Channels
|
||||
thing-type.config.zwavejs.gateway.configurationChannels.description = When set to `true`, the Z-Wave CommandClass "Configuration" is exposed in openHAB as channels instead of Thing configuration. This allows you to modify the configuration in rules, such as changing codes for locks.
|
||||
thing-type.config.zwavejs.gateway.hostname.label = Hostname
|
||||
thing-type.config.zwavejs.gateway.hostname.description = Hostname or IP address of Z-Wave JS server
|
||||
thing-type.config.zwavejs.gateway.maxMessageSize.label = Maximum Message Size
|
||||
thing-type.config.zwavejs.gateway.maxMessageSize.description = The maximum size of the message (in bytes) that the ZWave-JS server can send
|
||||
thing-type.config.zwavejs.gateway.port.label = Port
|
||||
thing-type.config.zwavejs.gateway.port.description = Port the remote Z-Wave JS server listens on
|
||||
thing-type.config.zwavejs.node.id.label = ID
|
||||
|
||||
channel-type.config.zwavejs.base-channel.commandClassId.label = Command Class Id
|
||||
channel-type.config.zwavejs.base-channel.commandClassName.label = Command Class Name
|
||||
channel-type.config.zwavejs.base-channel.endpoint.label = Endpoint Index
|
||||
channel-type.config.zwavejs.base-channel.factor.label = Factor
|
||||
channel-type.config.zwavejs.base-channel.incomingUnit.label = Incoming Unit
|
||||
channel-type.config.zwavejs.base-channel.itemType.label = Item Type
|
||||
channel-type.config.zwavejs.base-channel.propertyKeyInt.label = Property Key Int
|
||||
channel-type.config.zwavejs.base-channel.propertyKeyStr.label = Property Key Str
|
||||
channel-type.config.zwavejs.base-channel.readProperty.label = Read Property
|
||||
channel-type.config.zwavejs.base-channel.writePropertyInt.label = Write Property Int
|
||||
channel-type.config.zwavejs.base-channel.writePropertyStr.label = Write Property Str
|
||||
channel-type.config.zwavejs.invertible-channel.commandClassId.label = Command Class Id
|
||||
channel-type.config.zwavejs.invertible-channel.commandClassName.label = Command Class Name
|
||||
channel-type.config.zwavejs.invertible-channel.endpoint.label = Endpoint Index
|
||||
channel-type.config.zwavejs.invertible-channel.factor.label = Factor
|
||||
channel-type.config.zwavejs.invertible-channel.incomingUnit.label = Incoming Unit
|
||||
channel-type.config.zwavejs.invertible-channel.inverted.label = Inverted
|
||||
channel-type.config.zwavejs.invertible-channel.inverted.description = When True the value will be inverted for OpenClose, OnOff and Percentage types.
|
||||
channel-type.config.zwavejs.invertible-channel.itemType.label = Item Type
|
||||
channel-type.config.zwavejs.invertible-channel.propertyKeyInt.label = Property Key Int
|
||||
channel-type.config.zwavejs.invertible-channel.propertyKeyStr.label = Property Key Str
|
||||
channel-type.config.zwavejs.invertible-channel.readProperty.label = Read Property
|
||||
channel-type.config.zwavejs.invertible-channel.writePropertyInt.label = Write Property Int
|
||||
channel-type.config.zwavejs.invertible-channel.writePropertyStr.label = Write Property Str
|
||||
|
||||
# thing status descriptions
|
||||
|
||||
discovery.gateway.label = Z-Wave JS Gateway ({0})
|
||||
offline.conf-error.hostname-or-port = Hostname or port invalid
|
||||
offline.conf-error.id-invalid = id invalid
|
||||
offline.conf-error.no-node-details = Could not obtain node details
|
||||
offline.conf-error.build-channels-failed = Initialization or update failed, could not build channels
|
||||
offline.comm-error.dead-node = Z-Wave JS reported this node dead
|
||||
|
||||
# thing actions
|
||||
|
||||
action.start-exclusion.label = start exclusion
|
||||
action.start-exclusion.description = Puts the controller for 30s in network wide exclusion mode
|
||||
action.start-inclusion.label = start inclusion
|
||||
action.start-inclusion.description = Puts the controller for 30s in network wide inclusion mode
|
||||
action.send-multicast-command.label = send multicast command
|
||||
action.send-multicast-command.description = Sends command to multiple nodes
|
||||
@@ -0,0 +1,61 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<thing:thing-descriptions bindingId="zwavejs"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
|
||||
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
|
||||
|
||||
<bridge-type id="gateway">
|
||||
|
||||
<label>Z-Wave JS Gateway</label>
|
||||
<description>The Z-Wave JS server that acts as a bridge to the Z-Wave network</description>
|
||||
<semantic-equipment-tag>NetworkAppliance</semantic-equipment-tag>
|
||||
<representation-property>hostname</representation-property>
|
||||
|
||||
<config-description>
|
||||
<parameter name="hostname" type="text" required="true">
|
||||
<context>network-address</context>
|
||||
<label>Hostname</label>
|
||||
<description>Hostname or IP address of Z-Wave JS server</description>
|
||||
</parameter>
|
||||
<parameter name="port" type="integer" required="true">
|
||||
<label>Port</label>
|
||||
<description>Port the remote Z-Wave JS server listens on</description>
|
||||
<default>3000</default>
|
||||
</parameter>
|
||||
<parameter name="configurationChannels" type="boolean" required="true">
|
||||
<label>Configuration As Channels</label>
|
||||
<description>When set to `true`, the Z-Wave CommandClass "Configuration" is exposed in openHAB as channels instead
|
||||
of Thing configuration. This allows you to modify the configuration in rules, such as changing codes for locks.</description>
|
||||
<default>false</default>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
<parameter name="maxMessageSize" type="integer" required="true" min="1048576" max="33554432" step="262144">
|
||||
<label>Maximum Message Size</label>
|
||||
<description>The maximum size of the message (in bytes) that the ZWave-JS server can send</description>
|
||||
<default>2097152</default>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
</config-description>
|
||||
|
||||
</bridge-type>
|
||||
|
||||
<thing-type id="node" listed="false">
|
||||
<supported-bridge-type-refs>
|
||||
<bridge-type-ref id="gateway"/>
|
||||
</supported-bridge-type-refs>
|
||||
|
||||
<label>Z-Wave node</label>
|
||||
<description>A Z-Wave node provided by Zwave JS</description>
|
||||
|
||||
<representation-property>id</representation-property>
|
||||
|
||||
<config-description>
|
||||
<parameter name="id" type="integer" min="1" max="255" readOnly="true" required="true">
|
||||
<label>ID</label>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
</config-description>
|
||||
|
||||
</thing-type>
|
||||
|
||||
</thing:thing-descriptions>
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2025 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.zwavejs.internal;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.Reader;
|
||||
import java.lang.reflect.Type;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Instant;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.zwavejs.internal.api.adapter.InstantAdapter;
|
||||
import org.openhab.binding.zwavejs.internal.api.dto.Node;
|
||||
import org.openhab.binding.zwavejs.internal.api.dto.messages.ResultMessage;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.ToNumberPolicy;
|
||||
|
||||
/**
|
||||
* Utility class for working with test data in unit tests
|
||||
*
|
||||
* @author Leo Siepel - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class DataUtil {
|
||||
|
||||
public static Reader openDataReader(String fileName) throws FileNotFoundException {
|
||||
String filePath = "src/test/resources/json/" + fileName;
|
||||
|
||||
InputStream inputStream = new FileInputStream(filePath);
|
||||
return new InputStreamReader(inputStream, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
public static <T> T fromJson(String fileName, Type typeOfT) throws IOException {
|
||||
try (Reader reader = openDataReader(fileName)) {
|
||||
Gson gson = new GsonBuilder().setObjectToNumberStrategy(ToNumberPolicy.LONG_OR_DOUBLE)
|
||||
.registerTypeAdapter(Instant.class, new InstantAdapter()).create();
|
||||
|
||||
return gson.fromJson(reader, typeOfT);
|
||||
}
|
||||
}
|
||||
|
||||
public static String fromFile(String fileName) throws IOException {
|
||||
try (Reader reader = openDataReader(fileName)) {
|
||||
return Objects
|
||||
.requireNonNull(new BufferedReader(reader).lines().parallel().collect(Collectors.joining("\n")));
|
||||
}
|
||||
}
|
||||
|
||||
public static Node getNodeFromStore(String storeFileName, int nodeId) throws IOException {
|
||||
ResultMessage resultMessage = DataUtil.fromJson(storeFileName, ResultMessage.class);
|
||||
return Objects.requireNonNull(
|
||||
resultMessage.result.state.nodes.stream().filter(f -> f.nodeId == nodeId).findAny().orElse(null));
|
||||
}
|
||||
}
|
||||
+327
@@ -0,0 +1,327 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2025 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.zwavejs.internal.conversion;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
import javax.measure.quantity.Time;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.openhab.binding.zwavejs.internal.DataUtil;
|
||||
import org.openhab.binding.zwavejs.internal.api.dto.Node;
|
||||
import org.openhab.binding.zwavejs.internal.api.dto.messages.ResultMessage;
|
||||
import org.openhab.core.library.types.DecimalType;
|
||||
import org.openhab.core.library.types.HSBType;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.openhab.core.library.types.QuantityType;
|
||||
import org.openhab.core.library.types.StringType;
|
||||
import org.openhab.core.library.unit.Units;
|
||||
import org.openhab.core.types.StateDescriptionFragment;
|
||||
import org.openhab.core.types.StateDescriptionFragmentBuilder;
|
||||
import org.openhab.core.types.UnDefType;
|
||||
import org.openhab.core.util.ColorUtil;
|
||||
|
||||
/**
|
||||
* @author Leo Siepel - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class ChannelMetadataTest {
|
||||
|
||||
private List<Node> getNodesFromStore(String filename) throws IOException {
|
||||
ResultMessage resultMessage = DataUtil.fromJson(filename, ResultMessage.class);
|
||||
return resultMessage.result.state.nodes;
|
||||
}
|
||||
|
||||
private Node getNodeFromStore(String filename, int NodeId) throws IOException {
|
||||
return getNodesFromStore(filename).stream().filter(f -> f.nodeId == NodeId).findAny().get();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDetailsNode7Channel83() throws IOException {
|
||||
Node node = getNodeFromStore("store_4.json", 7);
|
||||
|
||||
ChannelMetadata details = new ChannelMetadata(7, node.values.get(83));
|
||||
|
||||
assertEquals("multilevel-sensor-power-1", details.id);
|
||||
assertEquals("Number:Power", details.itemType);
|
||||
assertEquals("EP1 Power", details.label);
|
||||
assertNull(details.description);
|
||||
assertEquals(new QuantityType<>(4.8, Units.WATT), details.state);
|
||||
assertEquals(false, details.writable);
|
||||
assertEquals(StateDescriptionFragmentBuilder.create().withPattern("%.2f %unit%").withReadOnly(true).build(),
|
||||
details.statePattern);
|
||||
assertEquals("W", details.unitSymbol);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDetailsNode7Channel84() throws IOException {
|
||||
Node node = getNodeFromStore("store_4.json", 7);
|
||||
|
||||
ChannelMetadata details = new ChannelMetadata(7, node.values.get(84));
|
||||
|
||||
assertEquals("meter-value-65537-1", details.id);
|
||||
assertEquals("Number:Energy", details.itemType);
|
||||
assertEquals("EP1 Electric Consumption", details.label);
|
||||
assertNull(details.description);
|
||||
assertEquals(new QuantityType<>(169.48, Units.KILOWATT_HOUR), details.state);
|
||||
assertEquals(false, details.writable);
|
||||
assertEquals(StateDescriptionFragmentBuilder.create().withPattern("%.2f %unit%").withReadOnly(true).build(),
|
||||
details.statePattern);
|
||||
assertEquals("kWh", details.unitSymbol);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDetailsNode7Channel85() throws IOException {
|
||||
Node node = getNodeFromStore("store_4.json", 7);
|
||||
|
||||
ChannelMetadata details = new ChannelMetadata(7, node.values.get(85));
|
||||
|
||||
assertEquals("meter-value-66049-1", details.id);
|
||||
assertEquals("Number:Power", details.itemType);
|
||||
assertEquals("EP1 Electric Consumption", details.label);
|
||||
assertNull(details.description);
|
||||
assertEquals(new QuantityType<>(4.8, Units.WATT), details.state);
|
||||
assertEquals(false, details.writable);
|
||||
assertEquals("W", details.unitSymbol);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDetailsNode7Channel86() throws IOException {
|
||||
Node node = getNodeFromStore("store_4.json", 7);
|
||||
|
||||
ChannelMetadata details = new ChannelMetadata(7, node.values.get(86));
|
||||
|
||||
assertEquals("meter-reset-1", details.id);
|
||||
assertEquals("Switch", details.itemType);
|
||||
assertEquals("EP1 Reset Accumulated Values", details.label);
|
||||
assertNull(details.description);
|
||||
assertEquals(UnDefType.NULL, details.state);
|
||||
assertEquals(true, details.writable);
|
||||
assertNull(details.statePattern);
|
||||
assertNull(details.unitSymbol);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDetailsNode7Channel1() throws IOException {
|
||||
Node node = getNodeFromStore("store_4.json", 5);
|
||||
|
||||
ChannelMetadata details = new ChannelMetadata(5, node.values.get(1));
|
||||
|
||||
assertEquals("configuration-total-alarm-duration", details.id);
|
||||
assertEquals("Number:Time", details.itemType);
|
||||
assertEquals("Total Alarm Duration", details.label);
|
||||
assertEquals("Total time the Leak Sensor will beep and light its LED in the event of a leak",
|
||||
details.description);
|
||||
assertEquals(new QuantityType<>(120, Units.MINUTE), details.state);
|
||||
assertEquals(true, details.writable);
|
||||
assertEquals("min", details.unitSymbol);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDetailsNode74Channel36() throws IOException {
|
||||
Node node = getNodeFromStore("store_4.json", 74);
|
||||
|
||||
ChannelMetadata details = new ChannelMetadata(74, node.values.get(36));
|
||||
|
||||
assertEquals("multilevel-sensor-humidity-2", details.id);
|
||||
assertEquals("Number:Dimensionless", details.itemType);
|
||||
assertEquals("EP2 Humidity", details.label);
|
||||
assertNull(details.description);
|
||||
assertEquals(new QuantityType<>(36, Units.PERCENT), details.state);
|
||||
assertEquals(false, details.writable);
|
||||
assertEquals(StateDescriptionFragmentBuilder.create().withPattern("%d %unit%").withReadOnly(true).build(),
|
||||
details.statePattern);
|
||||
assertEquals("%", details.unitSymbol);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDetailsNode74Channel7() throws IOException {
|
||||
Node node = getNodeFromStore("store_4.json", 74);
|
||||
|
||||
ChannelMetadata details = new ChannelMetadata(74, node.values.get(7));
|
||||
|
||||
assertEquals("thermostat-setpoint-setpoint-types-interpretation", details.id);
|
||||
assertEquals("String", details.itemType);
|
||||
assertEquals("setpointTypesInterpretation", details.label);
|
||||
assertNull(details.description);
|
||||
assertEquals(new StringType("B"), details.state);
|
||||
assertEquals(true, details.writable);
|
||||
assertNull(details.statePattern);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDetailsNode13Channel1() throws IOException {
|
||||
Node node = getNodeFromStore("store_4.json", 13);
|
||||
|
||||
ChannelMetadata details = new ChannelMetadata(13, node.values.get(1));
|
||||
|
||||
assertEquals("multilevel-switch-value", details.id);
|
||||
assertEquals("Number", details.itemType);
|
||||
assertEquals("Current Value", details.label);
|
||||
assertNull(details.description);
|
||||
assertEquals(new DecimalType(0.0), details.state);
|
||||
assertEquals(false, details.writable);
|
||||
|
||||
StateDescriptionFragment statePattern = details.statePattern;
|
||||
assertNotNull(statePattern);
|
||||
assertEquals(BigDecimal.valueOf(0), statePattern.getMinimum());
|
||||
assertEquals(BigDecimal.valueOf(99), statePattern.getMaximum());
|
||||
assertNull(statePattern.getStep());
|
||||
assertEquals("%d", statePattern.getPattern());
|
||||
|
||||
assertNull(details.unitSymbol);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDetailsNode35Channel5() throws IOException {
|
||||
Node node = getNodeFromStore("store_4.json", 35);
|
||||
|
||||
ChannelMetadata details = new ChannelMetadata(35, node.values.get(5));
|
||||
|
||||
assertEquals("configuration-transmission-retry-wait-time-255", details.id);
|
||||
assertEquals("Number:Time", details.itemType);
|
||||
assertEquals("Transmission Retry Wait Time", details.label);
|
||||
assertNull(details.description);
|
||||
assertEquals(new QuantityType<Time>("1400 ms"), details.state);
|
||||
assertEquals(true, details.writable);
|
||||
|
||||
StateDescriptionFragment statePattern = details.statePattern;
|
||||
assertNotNull(statePattern);
|
||||
assertEquals(BigDecimal.valueOf(0), statePattern.getMinimum());
|
||||
assertEquals(BigDecimal.valueOf(255), statePattern.getMaximum());
|
||||
assertNull(statePattern.getStep());
|
||||
assertEquals("%d %unit%", statePattern.getPattern());
|
||||
|
||||
assertEquals("ms", details.unitSymbol);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDetailsNode7Channel97() throws IOException {
|
||||
Node node = getNodeFromStore("store_4.json", 7);
|
||||
|
||||
ChannelMetadata details = new ChannelMetadata(7, node.values.get(97));
|
||||
|
||||
assertEquals("basic-restore-previous-2", details.id);
|
||||
assertEquals("Switch", details.itemType);
|
||||
assertEquals("EP2 Restore Previous Value", details.label);
|
||||
assertNull(details.description);
|
||||
assertEquals(UnDefType.NULL, details.state);
|
||||
assertEquals(true, details.writable);
|
||||
assertTrue(details.isAdvanced);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDetailsNode44Channel11() throws IOException {
|
||||
Node node = getNodeFromStore("store_4.json", 44);
|
||||
|
||||
ChannelMetadata details = new ChannelMetadata(44, node.values.get(11));
|
||||
|
||||
assertEquals("color-switch-color", details.id);
|
||||
assertEquals("Color", details.itemType);
|
||||
assertEquals("Current Color", details.label);
|
||||
assertNull(details.description);
|
||||
assertEquals(HSBType.fromRGB(0, 0, 0), details.state);
|
||||
assertEquals(false, details.writable);
|
||||
assertFalse(details.isAdvanced);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDetailsNode44Channel13() throws IOException {
|
||||
Node node = getNodeFromStore("store_4.json", 44);
|
||||
|
||||
ChannelMetadata details = new ChannelMetadata(44, node.values.get(13));
|
||||
|
||||
assertEquals("color-switch-hex-color", details.id);
|
||||
assertEquals("Color", details.itemType);
|
||||
assertEquals("RGB Color", details.label);
|
||||
assertNull(details.description);
|
||||
assertEquals(HSBType.fromRGB(0, 0, 0), details.state);
|
||||
assertEquals(true, details.writable);
|
||||
assertFalse(details.isAdvanced);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDetailsNode51Channel0() throws IOException {
|
||||
Node node = getNodeFromStore("store_4.json", 51);
|
||||
|
||||
ChannelMetadata details = new ChannelMetadata(51, node.values.get(0));
|
||||
|
||||
assertEquals("binary-switch-value", details.id);
|
||||
assertNull(details.description);
|
||||
assertEquals("Switch", details.itemType);
|
||||
assertEquals("Current Value", details.label);
|
||||
assertEquals(OnOffType.OFF, details.state);
|
||||
assertEquals(false, details.writable);
|
||||
assertNull(details.statePattern);
|
||||
assertNull(details.unitSymbol);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDetailsNode78Channel22() throws IOException {
|
||||
Node node = getNodeFromStore("store_4.json", 78);
|
||||
|
||||
ChannelMetadata details = new ChannelMetadata(78, node.values.get(22));
|
||||
|
||||
assertEquals("notification-access-control-door-state-simple", details.id);
|
||||
assertEquals("Switch", details.itemType);
|
||||
assertEquals("Door State (Simple)", details.label);
|
||||
assertNull(details.description);
|
||||
assertEquals(OnOffType.OFF, details.state);
|
||||
assertEquals(false, details.writable);
|
||||
assertFalse(details.isAdvanced);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDetailsNode66Channel43() throws IOException {
|
||||
Node node = getNodeFromStore("store_4.json", 66);
|
||||
|
||||
ChannelMetadata details = new ChannelMetadata(66, node.values.get(43));
|
||||
|
||||
assertEquals("notification-home-security-motion-sensor-status", details.id);
|
||||
assertEquals("Switch", details.itemType);
|
||||
assertEquals("Motion Sensor Status", details.label);
|
||||
assertNull(details.description);
|
||||
assertEquals(OnOffType.OFF, details.state);
|
||||
assertEquals(false, details.writable);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDetailsNode16Channel3() throws IOException {
|
||||
Node node = getNodeFromStore("store_4.json", 16);
|
||||
|
||||
ChannelMetadata details = new ChannelMetadata(16, node.values.get(13));
|
||||
|
||||
assertEquals(51, details.commandClassId);
|
||||
assertEquals("color-switch-color", details.id);
|
||||
assertEquals("Color", details.itemType);
|
||||
assertEquals("Target Color", details.label);
|
||||
assertNull(details.description);
|
||||
assertTrue(details.value instanceof Map);
|
||||
assertEquals(0L, ((Map<?, ?>) details.value).get("warmWhite"));
|
||||
assertEquals(0L, ((Map<?, ?>) details.value).get("coldWhite"));
|
||||
assertEquals(53L, ((Map<?, ?>) details.value).get("red"));
|
||||
assertEquals(3L, ((Map<?, ?>) details.value).get("green"));
|
||||
assertEquals(255L, ((Map<?, ?>) details.value).get("blue"));
|
||||
assertNotNull(details.state);
|
||||
assertEquals(HSBType.class, Objects.requireNonNull(details.state).getClass());
|
||||
assertEquals(ColorUtil.rgbToHsb(new int[] { 53, 3, 255 }), details.state);
|
||||
assertEquals(true, details.writable);
|
||||
}
|
||||
}
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2025 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.zwavejs.internal.conversion;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.openhab.binding.zwavejs.internal.DataUtil;
|
||||
import org.openhab.binding.zwavejs.internal.api.dto.Node;
|
||||
import org.openhab.binding.zwavejs.internal.api.dto.messages.ResultMessage;
|
||||
import org.openhab.core.config.core.ConfigDescriptionParameter.Type;
|
||||
|
||||
/**
|
||||
* @author Leo Siepel - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class ConfigMetadataTest {
|
||||
|
||||
private List<Node> getNodesFromStore(String filename) throws IOException {
|
||||
ResultMessage resultMessage = DataUtil.fromJson(filename, ResultMessage.class);
|
||||
return resultMessage.result.state.nodes;
|
||||
}
|
||||
|
||||
private Node getNodeFromStore(String filename, int NodeId) throws IOException {
|
||||
return getNodesFromStore(filename).stream().filter(f -> f.nodeId == NodeId).findAny().get();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testChannelDetailsStore4Node7Config1() throws IOException {
|
||||
Node node = getNodeFromStore("store_4.json", 7);
|
||||
|
||||
ConfigMetadata details = new ConfigMetadata(7, node.values.get(23));
|
||||
|
||||
assertEquals("configuration-key-s-1-associations-send-when-double-clicking-8", details.id);
|
||||
assertEquals(Type.INTEGER, details.configType);
|
||||
assertEquals("Key S 1 Associations : Send When Double Clicking", details.label);
|
||||
assertNull(details.description);
|
||||
assertEquals(true, details.writable);
|
||||
// assertEquals(BigDecimal.valueOf(0), details.statePattern.getMinimum());
|
||||
// assertEquals(BigDecimal.valueOf(1), details.statePattern.getMaximum());
|
||||
// assertEquals(BigDecimal.valueOf(1), details.statePattern.getStep());
|
||||
// assertEquals("%0.d", details.statePattern.getPattern());
|
||||
// assertEquals(new StateOption("0", "Activated"), details.statePattern.getOptions().get(0));
|
||||
// assertEquals(new StateOption("1", "Inactive"), details.statePattern.getOptions().get(1));
|
||||
|
||||
assertNull(details.unitSymbol);
|
||||
assertNotNull(details.optionList);
|
||||
Map<String, String> optionList = details.optionList;
|
||||
if (optionList != null) {
|
||||
assertEquals(2, optionList.size());
|
||||
assertEquals("Enable", optionList.get("0"));
|
||||
assertEquals("Disable", optionList.get("1"));
|
||||
}
|
||||
}
|
||||
}
|
||||
+99
@@ -0,0 +1,99 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2025 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.zwavejs.internal.discovery;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.mockito.Mockito.*;
|
||||
import static org.openhab.binding.zwavejs.internal.BindingConstants.PROPERTY_NODE_FREQ_LISTENING;
|
||||
import static org.openhab.binding.zwavejs.internal.BindingConstants.PROPERTY_NODE_IS_LISTENING;
|
||||
import static org.openhab.binding.zwavejs.internal.BindingConstants.PROPERTY_NODE_IS_ROUTING;
|
||||
import static org.openhab.binding.zwavejs.internal.BindingConstants.PROPERTY_NODE_IS_SECURE;
|
||||
import static org.openhab.binding.zwavejs.internal.BindingConstants.PROPERTY_NODE_LASTSEEN;
|
||||
import static org.openhab.core.thing.Thing.PROPERTY_FIRMWARE_VERSION;
|
||||
import static org.openhab.core.thing.Thing.PROPERTY_MODEL_ID;
|
||||
import static org.openhab.core.thing.Thing.PROPERTY_VENDOR;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
import org.openhab.binding.zwavejs.internal.BindingConstants;
|
||||
import org.openhab.binding.zwavejs.internal.api.dto.DeviceConfig;
|
||||
import org.openhab.binding.zwavejs.internal.api.dto.Node;
|
||||
import org.openhab.binding.zwavejs.internal.handler.ZwaveJSBridgeHandler;
|
||||
import org.openhab.core.config.discovery.DiscoveryResult;
|
||||
import org.openhab.core.thing.Bridge;
|
||||
import org.openhab.core.thing.ThingUID;
|
||||
|
||||
/**
|
||||
* @author Leo Siepel - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class NodeDiscoveryServiceTest {
|
||||
|
||||
private NodeDiscoveryService nodeDiscoveryService = spy(new NodeDiscoveryService());
|
||||
private ZwaveJSBridgeHandler thingHandler = mock(ZwaveJSBridgeHandler.class);
|
||||
private Bridge bridge = mock(Bridge.class);
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
MockitoAnnotations.openMocks(this);
|
||||
|
||||
when(bridge.getUID()).thenReturn(new ThingUID(BindingConstants.BINDING_ID, "test-bridge"));
|
||||
when(thingHandler.getThing()).thenReturn(bridge);
|
||||
nodeDiscoveryService.setThingHandler(thingHandler);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAddNodeDiscovery() {
|
||||
Node node = new Node();
|
||||
node.nodeId = 1;
|
||||
node.deviceConfig = new DeviceConfig();
|
||||
node.deviceConfig.label = "Test Device";
|
||||
node.deviceConfig.manufacturer = "Test Manufacturer";
|
||||
node.isListening = true;
|
||||
node.isRouting = true;
|
||||
node.isSecure = true;
|
||||
node.lastSeen = Instant.parse("2023-10-01T12:00:00Z");
|
||||
node.isFrequentListening = true;
|
||||
|
||||
ThingUID bridgeUID = new ThingUID("zwavejs", "bridge");
|
||||
when(thingHandler.getThing().getUID()).thenReturn(bridgeUID);
|
||||
|
||||
nodeDiscoveryService.addNodeDiscovery(node);
|
||||
|
||||
ArgumentCaptor<DiscoveryResult> captor = ArgumentCaptor.forClass(DiscoveryResult.class);
|
||||
verify(nodeDiscoveryService).thingDiscovered(captor.capture());
|
||||
|
||||
DiscoveryResult result = captor.getValue();
|
||||
Map<String, Object> expectedProperties = new HashMap<>();
|
||||
expectedProperties.put("id", node.nodeId);
|
||||
expectedProperties.put(PROPERTY_NODE_IS_LISTENING, node.isListening);
|
||||
expectedProperties.put(PROPERTY_NODE_IS_ROUTING, node.isRouting);
|
||||
expectedProperties.put(PROPERTY_NODE_IS_SECURE, node.isSecure);
|
||||
expectedProperties.put(PROPERTY_VENDOR, node.deviceConfig.manufacturer);
|
||||
expectedProperties.put(PROPERTY_MODEL_ID, node.deviceConfig.label);
|
||||
expectedProperties.put(PROPERTY_NODE_LASTSEEN, node.lastSeen);
|
||||
expectedProperties.put(PROPERTY_NODE_FREQ_LISTENING, node.isFrequentListening);
|
||||
expectedProperties.put(PROPERTY_FIRMWARE_VERSION, node.firmwareVersion);
|
||||
|
||||
assertEquals(expectedProperties, result.getProperties());
|
||||
assertEquals(bridgeUID, result.getBridgeUID());
|
||||
assertEquals("Test Manufacturer Test Device (node 1)", result.getLabel());
|
||||
}
|
||||
}
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2025 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.zwavejs.internal.handler;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.argThat;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.doNothing;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.openhab.binding.zwavejs.internal.DataUtil;
|
||||
import org.openhab.binding.zwavejs.internal.api.dto.messages.ResultMessage;
|
||||
import org.openhab.binding.zwavejs.internal.discovery.NodeDiscoveryService;
|
||||
import org.openhab.binding.zwavejs.internal.handler.mock.ZwaveJSBridgeHandlerMock;
|
||||
import org.openhab.core.thing.Bridge;
|
||||
import org.openhab.core.thing.ThingStatus;
|
||||
import org.openhab.core.thing.ThingStatusDetail;
|
||||
import org.openhab.core.thing.binding.ThingHandlerCallback;
|
||||
|
||||
/**
|
||||
* @author Leo Siepel - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class ZwaveJSBridgeHandlerTest {
|
||||
|
||||
@Test
|
||||
public void testInvalidConfiguration() {
|
||||
final Bridge thing = ZwaveJSBridgeHandlerMock.mockBridge("");
|
||||
final ThingHandlerCallback callback = mock(ThingHandlerCallback.class);
|
||||
final ZwaveJSBridgeHandler handler = ZwaveJSBridgeHandlerMock.createAndInitHandler(callback, thing);
|
||||
|
||||
try {
|
||||
verify(callback).statusUpdated(eq(thing), argThat(arg -> arg.getStatus().equals(ThingStatus.OFFLINE)
|
||||
&& arg.getStatusDetail().equals(ThingStatusDetail.CONFIGURATION_ERROR)));
|
||||
} finally {
|
||||
handler.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testValidConfiguration() {
|
||||
final Bridge thing = ZwaveJSBridgeHandlerMock.mockBridge("loclahost");
|
||||
final ThingHandlerCallback callback = mock(ThingHandlerCallback.class);
|
||||
final ZwaveJSBridgeHandler handler = ZwaveJSBridgeHandlerMock.createAndInitHandler(callback, thing);
|
||||
|
||||
try {
|
||||
verify(callback).statusUpdated(eq(thing), argThat(arg -> arg.getStatus().equals(ThingStatus.UNKNOWN)));
|
||||
} finally {
|
||||
handler.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDiscoveryForActiveNodes() throws IOException {
|
||||
final Bridge thing = ZwaveJSBridgeHandlerMock.mockBridge("localhost");
|
||||
final ThingHandlerCallback callback = mock(ThingHandlerCallback.class);
|
||||
final ZwaveJSBridgeHandler handler = ZwaveJSBridgeHandlerMock.createAndInitHandler(callback, thing);
|
||||
final NodeDiscoveryService discoveryService = mock(NodeDiscoveryService.class);
|
||||
doNothing().when(handler).getFullState();
|
||||
handler.registerDiscoveryListener(discoveryService);
|
||||
|
||||
ResultMessage resultMessage = DataUtil.fromJson("store_4.json", ResultMessage.class);
|
||||
|
||||
handler.onEvent(resultMessage);
|
||||
|
||||
try {
|
||||
verify(callback).statusUpdated(eq(thing), argThat(arg -> arg.getStatus().equals(ThingStatus.UNKNOWN)));
|
||||
verify(discoveryService, times(25)).addNodeDiscovery(any());
|
||||
} finally {
|
||||
handler.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
+253
@@ -0,0 +1,253 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2025 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.zwavejs.internal.handler;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import javax.measure.quantity.Power;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.openhab.binding.zwavejs.internal.DataUtil;
|
||||
import org.openhab.binding.zwavejs.internal.api.dto.messages.EventMessage;
|
||||
import org.openhab.binding.zwavejs.internal.handler.mock.ZwaveJSNodeHandlerMock;
|
||||
import org.openhab.core.config.core.Configuration;
|
||||
import org.openhab.core.library.types.HSBType;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.openhab.core.library.types.PercentType;
|
||||
import org.openhab.core.library.types.QuantityType;
|
||||
import org.openhab.core.library.unit.Units;
|
||||
import org.openhab.core.thing.Channel;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.thing.ThingStatus;
|
||||
import org.openhab.core.thing.ThingStatusDetail;
|
||||
import org.openhab.core.thing.binding.ThingHandlerCallback;
|
||||
|
||||
/**
|
||||
* @author Leo Siepel - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class ZwaveJSNodeHandlerTest {
|
||||
|
||||
@Test
|
||||
public void testInvalidConfiguration() {
|
||||
final Thing thing = ZwaveJSNodeHandlerMock.mockThing(0);
|
||||
final ThingHandlerCallback callback = mock(ThingHandlerCallback.class);
|
||||
final ZwaveJSNodeHandlerMock handler = ZwaveJSNodeHandlerMock.createAndInitHandler(callback, thing,
|
||||
"store_4.json");
|
||||
|
||||
try {
|
||||
verify(callback).statusUpdated(eq(thing), argThat(arg -> arg.getStatus().equals(ThingStatus.OFFLINE)
|
||||
&& arg.getStatusDetail().equals(ThingStatusDetail.CONFIGURATION_ERROR)));
|
||||
} finally {
|
||||
handler.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNode7ChannelsCreation() {
|
||||
final Thing thing = ZwaveJSNodeHandlerMock.mockThing(7);
|
||||
final ThingHandlerCallback callback = mock(ThingHandlerCallback.class);
|
||||
final ZwaveJSNodeHandlerMock handler = ZwaveJSNodeHandlerMock.createAndInitHandler(callback, thing,
|
||||
"store_4.json");
|
||||
|
||||
try {
|
||||
verify(callback).statusUpdated(eq(thing), argThat(arg -> arg.getStatus().equals(ThingStatus.UNKNOWN)));
|
||||
verify(callback).statusUpdated(argThat(arg -> arg.getUID().equals(thing.getUID())),
|
||||
argThat(arg -> arg.getStatus().equals(ThingStatus.ONLINE)));
|
||||
verify(callback, times(12)).stateUpdated(any(), any());
|
||||
} finally {
|
||||
handler.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNode7ChannelsCreationInclConfig() {
|
||||
final Thing thing = ZwaveJSNodeHandlerMock.mockThing(7);
|
||||
final ThingHandlerCallback callback = mock(ThingHandlerCallback.class);
|
||||
final ZwaveJSNodeHandler handler = ZwaveJSNodeHandlerMock.createAndInitHandler(callback, thing, "store_4.json",
|
||||
true);
|
||||
|
||||
try {
|
||||
verify(callback).statusUpdated(eq(thing), argThat(arg -> arg.getStatus().equals(ThingStatus.UNKNOWN)));
|
||||
verify(callback).statusUpdated(argThat(arg -> arg.getUID().equals(thing.getUID())),
|
||||
argThat(arg -> arg.getStatus().equals(ThingStatus.ONLINE)));
|
||||
verify(callback, times(74)).stateUpdated(any(), any());
|
||||
} finally {
|
||||
handler.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNode7ConfigCreation() {
|
||||
final Thing thing = ZwaveJSNodeHandlerMock.mockThing(7);
|
||||
final ThingHandlerCallback callback = mock(ThingHandlerCallback.class);
|
||||
final ZwaveJSNodeHandler handler = ZwaveJSNodeHandlerMock.createAndInitHandler(callback, thing, "store_4.json",
|
||||
false);
|
||||
Configuration configuration = handler.getThing().getConfiguration();
|
||||
try {
|
||||
verify(callback).statusUpdated(eq(thing), argThat(arg -> arg.getStatus().equals(ThingStatus.UNKNOWN)));
|
||||
verify(callback).statusUpdated(argThat(arg -> arg.getUID().equals(thing.getUID())),
|
||||
argThat(arg -> arg.getStatus().equals(ThingStatus.ONLINE)));
|
||||
assertEquals(63, configuration.getProperties().size());
|
||||
} finally {
|
||||
handler.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNode7EP2ChannelsCreation() {
|
||||
final Thing thing = ZwaveJSNodeHandlerMock.mockThing(7);
|
||||
final ThingHandlerCallback callback = mock(ThingHandlerCallback.class);
|
||||
final ZwaveJSNodeHandlerMock handler = ZwaveJSNodeHandlerMock.createAndInitHandler(callback, thing,
|
||||
"store_4.json");
|
||||
|
||||
ChannelUID channelid = new ChannelUID("zwavejs:test-bridge:test-thing:multilevel-switch-value-2");
|
||||
|
||||
try {
|
||||
verify(callback).statusUpdated(eq(thing), argThat(arg -> arg.getStatus().equals(ThingStatus.UNKNOWN)));
|
||||
verify(callback).statusUpdated(argThat(arg -> arg.getUID().equals(thing.getUID())),
|
||||
argThat(arg -> arg.getStatus().equals(ThingStatus.ONLINE)));
|
||||
verify(callback).stateUpdated(eq(channelid), eq(new PercentType(94)));
|
||||
} finally {
|
||||
handler.dispose();
|
||||
}
|
||||
|
||||
Channel channel = handler.getThing().getChannels().stream()
|
||||
.filter(f -> "multilevel-switch-value-2".equals(f.getUID().getId())).findFirst().orElse(null);
|
||||
|
||||
assertNotNull(channel);
|
||||
assertEquals("Dimmer", channel.getAcceptedItemType());
|
||||
assertEquals("EP2 Current Value", channel.getLabel());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNode7PowerEventUpdate() throws IOException {
|
||||
final Thing thing = ZwaveJSNodeHandlerMock.mockThing(7);
|
||||
final ThingHandlerCallback callback = mock(ThingHandlerCallback.class);
|
||||
final ZwaveJSNodeHandler handler = ZwaveJSNodeHandlerMock.createAndInitHandler(callback, thing, "store_4.json");
|
||||
|
||||
EventMessage eventMessage = DataUtil.fromJson("event_node_7_power.json", EventMessage.class);
|
||||
handler.onNodeStateChanged(eventMessage.event);
|
||||
|
||||
ChannelUID channelid = new ChannelUID("zwavejs:test-bridge:test-thing:meter-value-66049-1");
|
||||
try {
|
||||
verify(callback).statusUpdated(eq(thing), argThat(arg -> arg.getStatus().equals(ThingStatus.UNKNOWN)));
|
||||
verify(callback).statusUpdated(argThat(arg -> arg.getUID().equals(thing.getUID())),
|
||||
argThat(arg -> arg.getStatus().equals(ThingStatus.ONLINE)));
|
||||
verify(callback, times(1)).stateUpdated(eq(channelid), eq(new QuantityType<Power>(2.16, Units.WATT)));
|
||||
} finally {
|
||||
handler.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNode25SwitchEventUpdate() throws IOException {
|
||||
final Thing thing = ZwaveJSNodeHandlerMock.mockThing(25);
|
||||
final ThingHandlerCallback callback = mock(ThingHandlerCallback.class);
|
||||
final ZwaveJSNodeHandler handler = ZwaveJSNodeHandlerMock.createAndInitHandler(callback, thing, "store_4.json");
|
||||
|
||||
EventMessage eventMessage = DataUtil.fromJson("event_node_25_switch.json", EventMessage.class);
|
||||
handler.onNodeStateChanged(eventMessage.event);
|
||||
|
||||
ChannelUID channelid = new ChannelUID("zwavejs:test-bridge:test-thing:binary-switch-value-2");
|
||||
try {
|
||||
verify(callback).statusUpdated(eq(thing), argThat(arg -> arg.getStatus().equals(ThingStatus.UNKNOWN)));
|
||||
verify(callback).statusUpdated(argThat(arg -> arg.getUID().equals(thing.getUID())),
|
||||
argThat(arg -> arg.getStatus().equals(ThingStatus.ONLINE)));
|
||||
verify(callback).stateUpdated(eq(channelid), eq(OnOffType.OFF));
|
||||
} finally {
|
||||
handler.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNode44ChannelsCreation() {
|
||||
final Thing thing = ZwaveJSNodeHandlerMock.mockThing(44);
|
||||
final ThingHandlerCallback callback = mock(ThingHandlerCallback.class);
|
||||
final ZwaveJSNodeHandler handler = ZwaveJSNodeHandlerMock.createAndInitHandler(callback, thing, "store_4.json");
|
||||
|
||||
ChannelUID channelid = new ChannelUID("zwavejs:test-bridge:test-thing:color-switch-hex-color");
|
||||
|
||||
try {
|
||||
verify(callback).statusUpdated(eq(thing), argThat(arg -> arg.getStatus().equals(ThingStatus.UNKNOWN)));
|
||||
verify(callback).statusUpdated(argThat(arg -> arg.getUID().equals(thing.getUID())),
|
||||
argThat(arg -> arg.getStatus().equals(ThingStatus.ONLINE)));
|
||||
// 18 = 15 direct updates + 2 handled color updates + 1 handled color temperature update
|
||||
verify(callback, times(18)).stateUpdated(any(), any());
|
||||
verify(callback).stateUpdated(eq(channelid), eq(new HSBType("0,0,100")));
|
||||
} finally {
|
||||
handler.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNode78ChannelsCreation() {
|
||||
final Thing thing = ZwaveJSNodeHandlerMock.mockThing(78);
|
||||
final ThingHandlerCallback callback = mock(ThingHandlerCallback.class);
|
||||
final ZwaveJSNodeHandlerMock handler = ZwaveJSNodeHandlerMock.createAndInitHandler(callback, thing,
|
||||
"store_4.json");
|
||||
|
||||
try {
|
||||
verify(callback).statusUpdated(eq(thing), argThat(arg -> arg.getStatus().equals(ThingStatus.UNKNOWN)));
|
||||
verify(callback).statusUpdated(argThat(arg -> arg.getUID().equals(thing.getUID())),
|
||||
argThat(arg -> arg.getStatus().equals(ThingStatus.ONLINE)));
|
||||
verify(callback, times(10)).stateUpdated(any(), any());
|
||||
} finally {
|
||||
handler.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNode78SwitchEventUpdate() throws IOException {
|
||||
final Thing thing = ZwaveJSNodeHandlerMock.mockThing(78);
|
||||
final ThingHandlerCallback callback = mock(ThingHandlerCallback.class);
|
||||
final ZwaveJSNodeHandler handler = ZwaveJSNodeHandlerMock.createAndInitHandler(callback, thing, "store_4.json");
|
||||
|
||||
EventMessage eventMessage = DataUtil.fromJson("event_node_78_switch.json", EventMessage.class);
|
||||
handler.onNodeStateChanged(eventMessage.event);
|
||||
|
||||
ChannelUID channelid = new ChannelUID(
|
||||
"zwavejs:test-bridge:test-thing:notification-access-control-door-state-simple");
|
||||
try {
|
||||
verify(callback).statusUpdated(eq(thing), argThat(arg -> arg.getStatus().equals(ThingStatus.UNKNOWN)));
|
||||
verify(callback).statusUpdated(argThat(arg -> arg.getUID().equals(thing.getUID())),
|
||||
argThat(arg -> arg.getStatus().equals(ThingStatus.ONLINE)));
|
||||
verify(callback).stateUpdated(eq(channelid), eq(OnOffType.ON));
|
||||
} finally {
|
||||
handler.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNode186ChannelsCreation() {
|
||||
final Thing thing = ZwaveJSNodeHandlerMock.mockThing(186);
|
||||
final ThingHandlerCallback callback = mock(ThingHandlerCallback.class);
|
||||
final ZwaveJSNodeHandlerMock handler = ZwaveJSNodeHandlerMock.createAndInitHandler(callback, thing,
|
||||
"store_4.json");
|
||||
|
||||
try {
|
||||
verify(callback).statusUpdated(eq(thing), argThat(arg -> arg.getStatus().equals(ThingStatus.UNKNOWN)));
|
||||
verify(callback).statusUpdated(argThat(arg -> arg.getUID().equals(thing.getUID())),
|
||||
argThat(arg -> arg.getStatus().equals(ThingStatus.ONLINE)));
|
||||
verify(callback, times(15)).stateUpdated(any(), any());
|
||||
} finally {
|
||||
handler.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2025 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.zwavejs.internal.handler.mock;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.mockito.Mockito;
|
||||
import org.mockito.invocation.InvocationOnMock;
|
||||
import org.openhab.binding.zwavejs.internal.BindingConstants;
|
||||
import org.openhab.binding.zwavejs.internal.config.ZwaveJSBridgeConfiguration;
|
||||
import org.openhab.binding.zwavejs.internal.handler.ZwaveJSBridgeHandler;
|
||||
import org.openhab.core.config.core.Configuration;
|
||||
import org.openhab.core.io.net.http.WebSocketFactory;
|
||||
import org.openhab.core.thing.Bridge;
|
||||
import org.openhab.core.thing.ThingStatus;
|
||||
import org.openhab.core.thing.ThingUID;
|
||||
import org.openhab.core.thing.binding.ThingHandlerCallback;
|
||||
|
||||
/**
|
||||
* The {@link ZwaveJSBridgeHandlerMock} is responsible for mocking {@link ZwaveJSBridgeHandler}
|
||||
*
|
||||
* @author Leo Siepel - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class ZwaveJSBridgeHandlerMock extends ZwaveJSBridgeHandler {
|
||||
|
||||
private static Configuration createConfig(String hostname) {
|
||||
final Configuration config = new Configuration();
|
||||
config.put(BindingConstants.CONFIG_HOSTNAME, hostname);
|
||||
return config;
|
||||
}
|
||||
|
||||
public static Bridge mockBridge(String hostname) {
|
||||
return mockBridge(hostname, null);
|
||||
}
|
||||
|
||||
public static Bridge mockBridge(String hostname, @Nullable ThingStatus status) {
|
||||
final Bridge bridge = mock(Bridge.class);
|
||||
when(bridge.getUID()).thenReturn(new ThingUID(BindingConstants.BINDING_ID, "test-bridge"));
|
||||
when(bridge.getConfiguration()).thenReturn(createConfig(hostname));
|
||||
when(bridge.getStatus()).thenReturn(status == null ? ThingStatus.ONLINE : status);
|
||||
return bridge;
|
||||
}
|
||||
|
||||
public static ZwaveJSBridgeHandlerMock createAndInitHandler(final ThingHandlerCallback callback,
|
||||
final Bridge thing) {
|
||||
WebSocketFactory wsFactory = mock(WebSocketFactory.class);
|
||||
final ZwaveJSBridgeHandlerMock handler = spy(new ZwaveJSBridgeHandlerMock(thing, wsFactory));
|
||||
|
||||
handler.setCallback(callback);
|
||||
handler.initialize();
|
||||
|
||||
return handler;
|
||||
}
|
||||
|
||||
public ZwaveJSBridgeHandlerMock(Bridge bridge, WebSocketFactory wsFactory) {
|
||||
super(bridge, wsFactory);
|
||||
|
||||
executorService = Mockito.mock(ScheduledExecutorService.class);
|
||||
doAnswer((InvocationOnMock invocation) -> {
|
||||
((Runnable) invocation.getArguments()[0]).run();
|
||||
return null;
|
||||
}).when(executorService).scheduleWithFixedDelay(any(Runnable.class), anyLong(), anyLong(), any(TimeUnit.class));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void startClient(ZwaveJSBridgeConfiguration config) {
|
||||
// dont connect in unit test mode
|
||||
}
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2025 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.zwavejs.internal.handler.mock;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.zwavejs.internal.type.ZwaveJSChannelTypeProvider;
|
||||
import org.openhab.core.thing.type.ChannelType;
|
||||
import org.openhab.core.thing.type.ChannelTypeUID;
|
||||
|
||||
/**
|
||||
* @author Leo Siepel - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class ZwaveJSChannelTypeInMemmoryProvider implements ZwaveJSChannelTypeProvider {
|
||||
|
||||
private Map<ChannelTypeUID, ChannelType> typeCacheMap = new HashMap<>();
|
||||
|
||||
@Override
|
||||
public Collection<ChannelType> getChannelTypes(@Nullable Locale locale) {
|
||||
return new ArrayList<ChannelType>(typeCacheMap.values());
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable ChannelType getChannelType(ChannelTypeUID channelTypeUID, @Nullable Locale locale) {
|
||||
return typeCacheMap.get(channelTypeUID);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addChannelType(ChannelType channelType) {
|
||||
typeCacheMap.put(channelType.getUID(), channelType);
|
||||
}
|
||||
}
|
||||
+161
@@ -0,0 +1,161 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2025 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.zwavejs.internal.handler.mock;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
import static org.openhab.binding.zwavejs.internal.BindingConstants.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.mockito.Mockito;
|
||||
import org.mockito.invocation.InvocationOnMock;
|
||||
import org.openhab.binding.zwavejs.internal.BindingConstants;
|
||||
import org.openhab.binding.zwavejs.internal.DataUtil;
|
||||
import org.openhab.binding.zwavejs.internal.api.dto.messages.ResultMessage;
|
||||
import org.openhab.binding.zwavejs.internal.handler.ZwaveJSNodeHandler;
|
||||
import org.openhab.binding.zwavejs.internal.type.ZwaveJSChannelTypeProvider;
|
||||
import org.openhab.binding.zwavejs.internal.type.ZwaveJSConfigDescriptionProvider;
|
||||
import org.openhab.binding.zwavejs.internal.type.ZwaveJSConfigDescriptionProviderImpl;
|
||||
import org.openhab.binding.zwavejs.internal.type.ZwaveJSTypeGenerator;
|
||||
import org.openhab.binding.zwavejs.internal.type.ZwaveJSTypeGeneratorImpl;
|
||||
import org.openhab.core.config.core.Configuration;
|
||||
import org.openhab.core.thing.Bridge;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.thing.ThingRegistry;
|
||||
import org.openhab.core.thing.ThingStatus;
|
||||
import org.openhab.core.thing.ThingUID;
|
||||
import org.openhab.core.thing.binding.ThingHandlerCallback;
|
||||
import org.openhab.core.thing.binding.builder.ThingBuilder;
|
||||
|
||||
/**
|
||||
* The {@link ZwaveJSNodeHandlerMock} is responsible for mocking {@link ZwaveJSNodeHandler}
|
||||
*
|
||||
* @author Leo Siepel - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class ZwaveJSNodeHandlerMock extends ZwaveJSNodeHandler {
|
||||
private String filename = "";
|
||||
public boolean configAsChannel = false;
|
||||
|
||||
public static Configuration createConfig(int id) {
|
||||
final Configuration config = new Configuration();
|
||||
if (id > 0) {
|
||||
config.put(CONFIG_NODE_ID, id);
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
public static Configuration createBridgeConfig(boolean configurationAsChannel) {
|
||||
final Configuration config = new Configuration();
|
||||
config.put(CONFIG_CONFIG_AS_CHANNEL, configurationAsChannel);
|
||||
return config;
|
||||
}
|
||||
|
||||
public static Thing mockThing(int id) {
|
||||
return mockThing(id, null);
|
||||
}
|
||||
|
||||
public static Thing mockThing(int id, @Nullable ThingStatus status) {
|
||||
final Thing thing = mock(Thing.class);
|
||||
when(thing.getUID()).thenReturn(new ThingUID(BindingConstants.BINDING_ID, "test-bridge", "test-thing"));
|
||||
when(thing.getBridgeUID()).thenReturn(new ThingUID(BindingConstants.BINDING_ID, "test-bridge"));
|
||||
when(thing.getConfiguration()).thenReturn(createConfig(id));
|
||||
when(thing.getStatus()).thenReturn(status == null ? ThingStatus.ONLINE : status);
|
||||
|
||||
return thing;
|
||||
}
|
||||
|
||||
public static ZwaveJSNodeHandlerMock createAndInitHandler(final ThingHandlerCallback callback, final Thing thing,
|
||||
final String filename) {
|
||||
return createAndInitHandler(callback, thing, filename, false);
|
||||
}
|
||||
|
||||
public static ZwaveJSNodeHandlerMock createAndInitHandler(final ThingHandlerCallback callback, final Thing thing,
|
||||
final String filename, boolean configAsChannel) {
|
||||
ZwaveJSChannelTypeProvider channelTypeProvider = new ZwaveJSChannelTypeInMemmoryProvider();
|
||||
ZwaveJSConfigDescriptionProvider configDescriptionProvider = new ZwaveJSConfigDescriptionProviderImpl();
|
||||
ThingRegistry thingRegistry = mock(ThingRegistry.class);
|
||||
when(thingRegistry.get(any())).thenReturn(thing);
|
||||
ZwaveJSTypeGenerator typeGenerator = new ZwaveJSTypeGeneratorImpl(channelTypeProvider,
|
||||
configDescriptionProvider, thingRegistry);
|
||||
|
||||
final ZwaveJSNodeHandlerMock handler = spy(
|
||||
new ZwaveJSNodeHandlerMock(thing, typeGenerator, filename, configAsChannel));
|
||||
|
||||
handler.setCallback(callback);
|
||||
handler.initialize();
|
||||
|
||||
return handler;
|
||||
}
|
||||
|
||||
public ZwaveJSNodeHandlerMock(Thing thing, ZwaveJSTypeGenerator typeGenerator, String filename,
|
||||
boolean configAsChannel) {
|
||||
super(thing, typeGenerator);
|
||||
this.filename = filename;
|
||||
this.configAsChannel = configAsChannel;
|
||||
executorService = Mockito.mock(ScheduledExecutorService.class);
|
||||
doAnswer((InvocationOnMock invocation) -> {
|
||||
((Runnable) invocation.getArguments()[0]).run();
|
||||
return null;
|
||||
}).when(executorService).scheduleWithFixedDelay(any(Runnable.class), anyLong(), anyLong(), any(TimeUnit.class));
|
||||
|
||||
doAnswer((InvocationOnMock invocation) -> {
|
||||
((Runnable) invocation.getArguments()[0]).run();
|
||||
return null;
|
||||
}).when(executorService).execute(any(Runnable.class));
|
||||
}
|
||||
|
||||
public boolean isLinked(ChannelUID channelUID) {
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean isLinked(String channelId) {
|
||||
return true;
|
||||
}
|
||||
|
||||
public void updateThing(Thing thing) {
|
||||
super.updateThing(thing);
|
||||
}
|
||||
|
||||
public ThingBuilder editThing() {
|
||||
return super.editThing();
|
||||
}
|
||||
|
||||
public @Nullable Bridge getBridge() {
|
||||
final Bridge bridge = ZwaveJSBridgeHandlerMock.mockBridge("localhost");
|
||||
final ZwaveJSBridgeHandlerMock handler = mock(ZwaveJSBridgeHandlerMock.class);
|
||||
doNothing().when(handler).initialize();
|
||||
ResultMessage resultMessage;
|
||||
try {
|
||||
resultMessage = DataUtil.fromJson(filename, ResultMessage.class);
|
||||
} catch (IOException e) {
|
||||
return null;
|
||||
}
|
||||
|
||||
doAnswer(i -> {
|
||||
int nodeId = i.getArgument(0);
|
||||
return resultMessage.result.state.nodes.stream().filter(f -> f.nodeId == nodeId).findAny().orElse(null);
|
||||
}).when(handler).requestNodeDetails(anyInt());
|
||||
when(bridge.getStatus()).thenReturn(ThingStatus.ONLINE);
|
||||
when(bridge.getHandler()).thenReturn(handler);
|
||||
when(bridge.getConfiguration()).thenReturn(createBridgeConfig(configAsChannel));
|
||||
when(handler.getThing()).thenReturn(bridge);
|
||||
return bridge;
|
||||
}
|
||||
}
|
||||
+297
@@ -0,0 +1,297 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2025 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.zwavejs.internal.type;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.*;
|
||||
import static org.openhab.binding.zwavejs.internal.BindingConstants.BINDING_ID;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.openhab.binding.zwavejs.internal.BindingConstants;
|
||||
import org.openhab.binding.zwavejs.internal.DataUtil;
|
||||
import org.openhab.binding.zwavejs.internal.api.dto.Node;
|
||||
import org.openhab.binding.zwavejs.internal.api.dto.messages.ResultMessage;
|
||||
import org.openhab.binding.zwavejs.internal.handler.mock.ZwaveJSChannelTypeInMemmoryProvider;
|
||||
import org.openhab.core.config.core.Configuration;
|
||||
import org.openhab.core.thing.Channel;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.thing.ThingRegistry;
|
||||
import org.openhab.core.thing.ThingUID;
|
||||
import org.openhab.core.thing.type.ChannelType;
|
||||
import org.openhab.core.types.StateDescription;
|
||||
|
||||
/**
|
||||
* @author Leo Siepel - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class ZwaveJSTypeGeneratorTest {
|
||||
|
||||
@Nullable
|
||||
ZwaveJSTypeGenerator provider;
|
||||
ZwaveJSChannelTypeProvider channelTypeProvider = new ZwaveJSChannelTypeInMemmoryProvider();
|
||||
ZwaveJSConfigDescriptionProvider configDescriptionProvider = new ZwaveJSConfigDescriptionProviderImpl();
|
||||
|
||||
@BeforeEach
|
||||
public void setup() {
|
||||
ThingRegistry thingRegistry = mock(ThingRegistry.class);
|
||||
Thing thing = mock(Thing.class);
|
||||
when(thing.getUID()).thenReturn(new ThingUID(BindingConstants.BINDING_ID, "test-thing"));
|
||||
when(thing.getBridgeUID()).thenReturn(new ThingUID(BindingConstants.BINDING_ID, "test-bridge"));
|
||||
when(thingRegistry.get(any())).thenReturn(thing);
|
||||
provider = new ZwaveJSTypeGeneratorImpl(channelTypeProvider, configDescriptionProvider, thingRegistry);
|
||||
}
|
||||
|
||||
private Channel getChannel(String store, int nodeId, String channelId) throws IOException {
|
||||
return getChannel(store, nodeId, channelId, false);
|
||||
}
|
||||
|
||||
private Channel getChannel(String store, int nodeId, String channelId, boolean configurationAsChannels)
|
||||
throws IOException {
|
||||
Node node = DataUtil.getNodeFromStore(store, nodeId);
|
||||
ZwaveJSTypeGeneratorResult results = Objects.requireNonNull(provider).generate(
|
||||
new ThingUID(BINDING_ID, "test-bridge", "test-thing"), Objects.requireNonNull(node),
|
||||
configurationAsChannels);
|
||||
return Objects.requireNonNull(results.channels.get(channelId));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGenCTNode2TypeCount() throws IOException {
|
||||
Node node = DataUtil.getNodeFromStore("store_4.json", 2);
|
||||
|
||||
ZwaveJSTypeGeneratorResult results = Objects.requireNonNull(provider)
|
||||
.generate(new ThingUID(BINDING_ID, "test-thing"), Objects.requireNonNull(node), false);
|
||||
|
||||
assertEquals(8, results.channels.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGenCTNode2SensorSwitchType() throws IOException {
|
||||
Channel channel = getChannel("store_4.json", 2, "binary-sensor-any");
|
||||
ChannelType type = channelTypeProvider.getChannelType(Objects.requireNonNull(channel.getChannelTypeUID()),
|
||||
null);
|
||||
|
||||
assertNotNull(type);
|
||||
assertEquals("zwavejs:test-bridge:test-thing:binary-sensor-any", channel.getUID().getAsString());
|
||||
assertEquals("Switch", Objects.requireNonNull(type).getItemType());
|
||||
assertEquals("Sensor State (Any)", channel.getLabel());
|
||||
|
||||
assertNull(type.getState());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGenCTNode7MeterType() throws IOException {
|
||||
Channel channel = getChannel("store_4.json", 7, "meter-value-65537-1");
|
||||
ChannelType type = channelTypeProvider.getChannelType(Objects.requireNonNull(channel.getChannelTypeUID()),
|
||||
null);
|
||||
|
||||
assertNotNull(type);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGenCTNode7NotificationType() throws IOException {
|
||||
Channel channel = getChannel("store_4.json", 7, "notification-power-management-over-load-status-1");
|
||||
ChannelType type = channelTypeProvider.getChannelType(Objects.requireNonNull(channel.getChannelTypeUID()),
|
||||
null);
|
||||
|
||||
assertNotNull(type);
|
||||
assertEquals("Switch", type.getItemType());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGenCTNode7TypeCount() throws IOException {
|
||||
Node node = DataUtil.getNodeFromStore("store_4.json", 7);
|
||||
|
||||
ZwaveJSTypeGeneratorResult results = Objects.requireNonNull(provider)
|
||||
.generate(new ThingUID(BINDING_ID, "test-thing"), Objects.requireNonNull(node), false);
|
||||
|
||||
assertEquals(14, results.channels.values().stream().map(f -> f.getChannelTypeUID()).distinct().count());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGenCTNode7AsChannels() throws IOException {
|
||||
Node node = DataUtil.getNodeFromStore("store_4.json", 7);
|
||||
|
||||
ZwaveJSTypeGeneratorResult results = Objects.requireNonNull(provider)
|
||||
.generate(new ThingUID(BINDING_ID, "test-thing"), Objects.requireNonNull(node), true);
|
||||
|
||||
assertEquals(47, results.channels.values().stream().map(f -> f.getChannelTypeUID()).distinct().count());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGenCTNode7ConfigWriteProperty() throws IOException {
|
||||
Channel channel = getChannel("store_4.json", 7,
|
||||
"configuration-key-s-1-associations-send-on-with-single-click-1", true);
|
||||
|
||||
assertNull(channel.getConfiguration().get(BindingConstants.CONFIG_CHANNEL_WRITE_PROPERTY_STR));
|
||||
assertEquals(BigDecimal.valueOf(24),
|
||||
channel.getConfiguration().get(BindingConstants.CONFIG_CHANNEL_WRITE_PROPERTY_INT));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGenCTNode7Label() throws IOException {
|
||||
Channel channel = getChannel("store_4.json", 7, "meter-reset-1");
|
||||
|
||||
assertEquals("EP1 Reset Accumulated Values", channel.getLabel());
|
||||
assertNull(channel.getDescription());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGenCTNode7ReadProperty() throws IOException {
|
||||
Channel channel = getChannel("store_4.json", 7, "multilevel-switch-value-1");
|
||||
|
||||
assertEquals("currentValue", channel.getConfiguration().get(BindingConstants.CONFIG_CHANNEL_READ_PROPERTY));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGenCTNode7WriteProperty() throws IOException {
|
||||
Channel channel = getChannel("store_4.json", 7, "multilevel-switch-value-1");
|
||||
|
||||
assertEquals("targetValue", channel.getConfiguration().get(BindingConstants.CONFIG_CHANNEL_WRITE_PROPERTY_STR));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGenCTNode7MultilevelSwitchType() throws IOException {
|
||||
Channel channel = getChannel("store_4.json", 7, "multilevel-switch-value-1");
|
||||
ChannelType type = channelTypeProvider.getChannelType(Objects.requireNonNull(channel.getChannelTypeUID()),
|
||||
null);
|
||||
Configuration configuration = channel.getConfiguration();
|
||||
|
||||
assertNotNull(type);
|
||||
assertEquals("zwavejs:test-bridge:test-thing:multilevel-switch-value-1", channel.getUID().getAsString());
|
||||
assertEquals("Dimmer", Objects.requireNonNull(type).getItemType());
|
||||
assertEquals("EP1 Current Value", channel.getLabel());
|
||||
assertNotNull(configuration.get(BindingConstants.CONFIG_CHANNEL_WRITE_PROPERTY_STR));
|
||||
|
||||
StateDescription statePattern = type.getState();
|
||||
assertNotNull(statePattern);
|
||||
assertEquals(BigDecimal.valueOf(0), statePattern.getMinimum());
|
||||
assertEquals(BigDecimal.valueOf(100), statePattern.getMaximum());
|
||||
assertNull(statePattern.getStep());
|
||||
assertEquals("%1d %%", statePattern.getPattern());
|
||||
|
||||
assertNotNull(type);
|
||||
assertEquals("Dimmer", type.getItemType());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGenCTNode13MultilevelSwitchType() throws IOException {
|
||||
Channel channel = getChannel("store_4.json", 13, "multilevel-switch-value");
|
||||
ChannelType type = channelTypeProvider.getChannelType(Objects.requireNonNull(channel.getChannelTypeUID()),
|
||||
null);
|
||||
Configuration configuration = channel.getConfiguration();
|
||||
|
||||
assertNotNull(type);
|
||||
assertEquals("zwavejs:test-bridge:test-thing:multilevel-switch-value", channel.getUID().getAsString());
|
||||
assertEquals("Dimmer", Objects.requireNonNull(type).getItemType());
|
||||
assertEquals("Current Value", channel.getLabel());
|
||||
assertNotNull(configuration.get(BindingConstants.CONFIG_CHANNEL_WRITE_PROPERTY_STR));
|
||||
|
||||
StateDescription statePattern = type.getState();
|
||||
assertNotNull(statePattern);
|
||||
assertEquals(BigDecimal.valueOf(0), statePattern.getMinimum());
|
||||
assertEquals(BigDecimal.valueOf(100), statePattern.getMaximum());
|
||||
assertNull(statePattern.getStep());
|
||||
assertEquals("%1d %%", statePattern.getPattern());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGenCTNode16MultilevelSwitchType() throws IOException {
|
||||
Channel channel = getChannel("store_4.json", 16, "multilevel-switch-value");
|
||||
ChannelType type = channelTypeProvider.getChannelType(Objects.requireNonNull(channel.getChannelTypeUID()),
|
||||
null);
|
||||
Configuration configuration = channel.getConfiguration();
|
||||
|
||||
assertNotNull(type);
|
||||
assertEquals("zwavejs:test-bridge:test-thing:multilevel-switch-value", channel.getUID().getAsString());
|
||||
assertEquals("Dimmer", Objects.requireNonNull(type).getItemType());
|
||||
assertEquals("Current Value", channel.getLabel());
|
||||
assertNotNull(configuration.get(BindingConstants.CONFIG_CHANNEL_WRITE_PROPERTY_STR));
|
||||
|
||||
StateDescription statePattern = type.getState();
|
||||
assertNotNull(statePattern);
|
||||
assertEquals(BigDecimal.valueOf(0), statePattern.getMinimum());
|
||||
assertEquals(BigDecimal.valueOf(100), statePattern.getMaximum());
|
||||
assertNull(statePattern.getStep());
|
||||
assertEquals("%1d %%", statePattern.getPattern());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGenCTNode25WriteProperty() throws IOException {
|
||||
Channel channel = getChannel("store_4.json", 25, "binary-switch-value-1");
|
||||
|
||||
assertEquals("targetValue", channel.getConfiguration().get(BindingConstants.CONFIG_CHANNEL_WRITE_PROPERTY_STR));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGenCTNode44ColorType() throws IOException {
|
||||
Channel channel = getChannel("store_4.json", 44, "color-switch-hex-color");
|
||||
ChannelType type = channelTypeProvider.getChannelType(Objects.requireNonNull(channel.getChannelTypeUID()),
|
||||
null);
|
||||
Configuration configuration = channel.getConfiguration();
|
||||
|
||||
assertNotNull(type);
|
||||
assertEquals("zwavejs:test-bridge:test-thing:color-switch-hex-color", channel.getUID().getAsString());
|
||||
assertEquals("Color", Objects.requireNonNull(type).getItemType());
|
||||
assertEquals("RGB Color", channel.getLabel());
|
||||
assertNotNull(configuration.get(BindingConstants.CONFIG_CHANNEL_WRITE_PROPERTY_STR));
|
||||
|
||||
StateDescription statePattern = type.getState();
|
||||
assertNotNull(statePattern);
|
||||
|
||||
assertNotNull(type);
|
||||
assertEquals("Color", type.getItemType());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGenCTNode186WeirdType() throws IOException {
|
||||
Channel channel = getChannel("store_4.json", 186, "door-lock-inside-handles-can-open-door");
|
||||
ChannelType type = channelTypeProvider.getChannelType(Objects.requireNonNull(channel.getChannelTypeUID()),
|
||||
null);
|
||||
Configuration configuration = channel.getConfiguration();
|
||||
|
||||
assertNotNull(type);
|
||||
assertEquals("zwavejs:test-bridge:test-thing:door-lock-inside-handles-can-open-door",
|
||||
channel.getUID().getAsString());
|
||||
assertEquals("String", type.getItemType());
|
||||
assertEquals("Which Inside Handles Can Open The Door (Actual Status)", channel.getLabel());
|
||||
assertNull(configuration.get(BindingConstants.CONFIG_CHANNEL_WRITE_PROPERTY_STR));
|
||||
|
||||
StateDescription statePattern = type.getState();
|
||||
assertNull(statePattern);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGenCTAllNodes() throws IOException {
|
||||
ResultMessage resultMessage = DataUtil.fromJson("store_4.json", ResultMessage.class);
|
||||
Map<String, Channel> channels = new HashMap<>();
|
||||
|
||||
for (Node node : resultMessage.result.state.nodes) {
|
||||
ZwaveJSTypeGeneratorResult results = Objects.requireNonNull(provider)
|
||||
.generate(new ThingUID(BINDING_ID, "test-thing"), Objects.requireNonNull(node), false);
|
||||
channels.putAll(results.channels);
|
||||
}
|
||||
|
||||
assertEquals(43, channels.values().stream().map(f -> f.getChannelTypeUID()).distinct().count());
|
||||
assertTrue(channels.containsKey("color-switch-color-temperature"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"type": "event",
|
||||
"event": {
|
||||
"source": "controller",
|
||||
"event": "statistics updated",
|
||||
"statistics": {
|
||||
"messagesTX": 1030,
|
||||
"messagesRX": 1031,
|
||||
"messagesDroppedRX": 0,
|
||||
"NAK": 0,
|
||||
"CAN": 0,
|
||||
"timeoutACK": 0,
|
||||
"timeoutResponse": 0,
|
||||
"timeoutCallback": 0,
|
||||
"messagesDroppedTX": 0,
|
||||
"backgroundRSSI": {
|
||||
"channel0": {
|
||||
"current": -99,
|
||||
"average": -97
|
||||
},
|
||||
"channel1": {
|
||||
"current": -106,
|
||||
"average": -104
|
||||
},
|
||||
"channel2": {
|
||||
"current": -106,
|
||||
"average": -104
|
||||
},
|
||||
"channel3": {
|
||||
"current": 127,
|
||||
"average": 0
|
||||
},
|
||||
"timestamp": 1732922541970
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"type": "event",
|
||||
"event": {
|
||||
"source": "node",
|
||||
"event": "value updated",
|
||||
"nodeId": 25,
|
||||
"args": {
|
||||
"commandClassName": "Binary Switch",
|
||||
"commandClass": 37,
|
||||
"property": "currentValue",
|
||||
"endpoint": 0,
|
||||
"newValue": false,
|
||||
"prevValue": true,
|
||||
"propertyName": "currentValue"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"type": "event",
|
||||
"event": {
|
||||
"source": "node",
|
||||
"event": "value updated",
|
||||
"nodeId": 78,
|
||||
"args": {
|
||||
"commandClassName": "Notification",
|
||||
"commandClass": 113,
|
||||
"property": "Access Control",
|
||||
"propertyName": "Access Control",
|
||||
"endpoint": 0,
|
||||
"newValue": 22,
|
||||
"prevValue": 23,
|
||||
"propertyKey": "Door state (simple)"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"type": "event",
|
||||
"event": {
|
||||
"source": "node",
|
||||
"event": "value updated",
|
||||
"nodeId": 7,
|
||||
"args": {
|
||||
"commandClassName": "Meter",
|
||||
"commandClass": 50,
|
||||
"property": "value",
|
||||
"propertyKey": 66049,
|
||||
"endpoint": 1,
|
||||
"newValue": 2.16,
|
||||
"prevValue": 204.96,
|
||||
"propertyName": "value",
|
||||
"propertyKeyName": "Electric_W_Consumed"
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -482,6 +482,7 @@
|
||||
<module>org.openhab.binding.yioremote</module>
|
||||
<module>org.openhab.binding.yeelight</module>
|
||||
<module>org.openhab.binding.zoneminder</module>
|
||||
<module>org.openhab.binding.zwavejs</module>
|
||||
<module>org.openhab.binding.zway</module>
|
||||
<!-- persistence -->
|
||||
<module>org.openhab.persistence.dynamodb</module>
|
||||
|
||||
Reference in New Issue
Block a user