[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:
lsiepel
2025-07-13 12:10:50 +02:00
committed by GitHub
co-authored by Andrew Fiddian-Green Cody Cutrer
parent af3d22d500
commit 0f91185566
98 changed files with 32922 additions and 0 deletions
+1
View File
@@ -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
+5
View File
@@ -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>
@@ -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>
@@ -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";
}
@@ -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));
}
@@ -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;
}
}
@@ -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");
}
}
@@ -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;
}
}
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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
}
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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 {
}
@@ -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;
}
@@ -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
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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,
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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";
}
}
@@ -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";
}
}
@@ -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";
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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();
}
}
@@ -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";
}
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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);
}
}
@@ -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 + "]";
}
}
@@ -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();
}
}
@@ -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;
}
@@ -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;
}
}
@@ -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();
}
}
@@ -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();
}
}
@@ -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();
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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);
}
@@ -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;
}
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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);
}
@@ -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);
}
@@ -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);
}
}
@@ -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);
}
@@ -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);
}
}
@@ -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);
}
@@ -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);
}
}
}
@@ -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>
@@ -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>
@@ -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));
}
}
@@ -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);
}
}
@@ -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"));
}
}
}
@@ -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());
}
}
@@ -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();
}
}
}
@@ -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();
}
}
}
@@ -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
}
}
@@ -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);
}
}
@@ -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;
}
}
@@ -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
+1
View File
@@ -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>