[sbus] Initial contribution (#18019)

* Initial contribution

Signed-off-by: Ciprian Pascu <contact@ciprianpascu.ro>
This commit is contained in:
Ciprian Pascu
2025-02-12 21:07:11 +01:00
committed by GitHub
parent ee9697b955
commit 2a7a76f8ba
25 changed files with 1884 additions and 0 deletions
+1
View File
@@ -332,6 +332,7 @@
/bundles/org.openhab.binding.salus/ @magx2
/bundles/org.openhab.binding.samsungtv/ @NickWaterton
/bundles/org.openhab.binding.satel/ @druciak
/bundles/org.openhab.binding.sbus/ @cipianpascu
/bundles/org.openhab.binding.semsportal/ @itb3
/bundles/org.openhab.binding.senechome/ @vctender @KorbinianP @eguib
/bundles/org.openhab.binding.seneye/ @nikotanghe
+5
View File
@@ -1636,6 +1636,11 @@
<artifactId>org.openhab.binding.satel</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.binding.sbus</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.binding.semsportal</artifactId>
+13
View File
@@ -0,0 +1,13 @@
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
+136
View File
@@ -0,0 +1,136 @@
# Sbus Binding
This binding integrates Sbus devices with openHAB, allowing control and monitoring of Sbus-compatible devices over UDP.
Sbus is a protocol used for home automation devices that communicate over UDP networks.
The binding supports various device types including RGB/RGBW controllers, temperature sensors, and switch controllers.
## Supported Things
- `udp` - Sbus Bridge for UDP communication
- `rgbw` - RGB/RGBW Controllers for color and brightness control
- `temperature` - Temperature Sensors for monitoring environmental conditions
- `switch` - Switch Controllers for basic on/off and dimming control
## Discovery
Sbus devices communicate via UDP broadcast, but manual configuration is required to set up the devices in openHAB.
Auto-discovery is not supported at this moment.
## Binding Configuration
The binding itself does not require any special configuration.
## Thing Configuration
### Bridge Configuration
The Sbus Bridge has the following configuration parameters:
| Name | Type | Description | Default | Required | Advanced |
|---------|---------|------------------------------------------------------|---------|----------|-----------|
| host | text | IP address of the Sbus device (typically broadcast) | N/A | yes | no |
| port | integer | UDP port number | 6000 | no | no |
### RGBW Controller Configuration
| Name | Type | Description | Default | Required | Advanced |
|---------|---------|------------------------------------------------------|---------|----------|-----------|
| subnetId| integer | Subnet ID the RGBW controller is part of | N/A | yes | no |
| id | integer | Device ID of the RGBW controller | N/A | yes | no |
| refresh | integer | Refresh interval in seconds | 30 | no | yes |
### Temperature Sensor Configuration
| Name | Type | Description | Default | Required | Advanced |
|---------|---------|------------------------------------------------------|---------|----------|-----------|
| subnetId| integer | Subnet ID the temperature sensor is part of | N/A | yes | no |
| id | integer | Device ID of the temperature sensor | N/A | yes | no |
| refresh | integer | Refresh interval in seconds | 30 | no | yes |
### Switch Controller Configuration
| Name | Type | Description | Default | Required | Advanced |
|---------|---------|------------------------------------------------------|---------|----------|-----------|
| subnetId| integer | Subnet ID the switch controller is part of | N/A | yes | no |
| id | integer | Device ID of the switch controller | N/A | yes | no |
| refresh | integer | Refresh interval in seconds | 30 | no | yes |
## Channels
### RGBW Controller Channels
| Channel | Type | Read/Write | Description |
|---------|--------|------------|------------------------------------------------------------|
| color | Color | RW | HSB color picker that controls RGBW components (0-100%) |
| switch | Switch | RW | On/Off control for the RGBW output with optional timer |
### Temperature Sensor Channels
| Channel | Type | Read/Write | Description |
|-------------|---------------------|------------|--------------------------------|
| temperature | Number:Temperature | R | Current temperature reading. Can be configured to use Celsius (default) or Fahrenheit units |
### Switch Controller Channels
| Channel | Type | Read/Write | Description |
|---------|---------|------------|-----------------------------------------------------------|
| switch | Switch | RW | Basic ON/OFF state control |
| dimmer | Dimmer | RW | ON/OFF state with timer transition |
| paired | Contact | RW | OPEN/CLOSED state for two paired channels (e.g., curtains)|
## Full Example
### Thing Configuration
```java
Bridge sbus:udp:mybridge [ host="192.168.1.255", port=5000 ] {
Thing rgbw colorctrl [ id=72, refresh=30 ] {
Channels:
Type color-channel : color [ channelNumber=1 ] // HSB color picker, RGBW values stored at channel 1
Type switch-channel : power [ channelNumber=1 ] // On/Off control for the RGBW output For complex scenes, one Sbus color controller can keep up to 40 color states. The switch channelNumber has to fall into this range.
}
Thing temperature temp1 [ id=62, refresh=30 ] {
Channels:
Type temperature-channel : temperature [ channelNumber=1 ]
}
Thing switch switch1 [ id=75, refresh=30 ] {
Channels:
Type switch-channel : first_switch [ channelNumber=1 ]
Type dimmer-channel : second_switch [ channelNumber=2 ]
Type paired-channel : third_switch [ channelNumber=3 ]
}
}
```
### Item Configuration
```java
// Temperature Sensor
Number:Temperature Temp_Sensor "Temperature [%.1f °C]" { channel="sbus:temperature:mybridge:temp1:temperature" }
// Basic Switch
Switch Light_Switch "Switch" { channel="sbus:switch:mybridge:switch1:switch" }
// Paired Channel (e.g., for curtains)
Contact Curtain_Switch "Curtain [%s]" { channel="sbus:switch:mybridge:switch1:third_switch" }
// RGBW Controller with Power Control
Group gLight "RGBW Light" <light> ["Lighting"]
Color rgbwColor "Color" <colorwheel> (gLight) ["Control", "Light"] { channel="sbus:rgbw:mybridge:colorctrl:color" }
Switch rgbwPower "Power" <switch> (gLight) ["Switch", "Light"] { channel="sbus:rgbw:mybridge:colorctrl:power" }
```
### Sitemap Configuration
```perl
sitemap sbus label="Sbus Demo"
{
Frame label="Sbus Controls" {
Colorpicker item=Light_RGB
Text item=Temp_Sensor
Switch item=Light_Switch
Text item=Curtain_Switch
}
}
+26
View File
@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<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.sbus</artifactId>
<name>openHAB Add-ons :: Bundles :: Sbus Binding</name>
<dependencies>
<dependency>
<groupId>ro.ciprianpascu</groupId>
<artifactId>j2sbus</artifactId>
<version>1.5.7</version>
<scope>compile</scope>
</dependency>
</dependencies>
</project>
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<features name="org.openhab.binding.sbus-${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-sbus" description="Sbus Binding" version="${project.version}">
<feature>openhab-runtime-base</feature>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.sbus/${project.version}</bundle>
</feature>
</features>
@@ -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.sbus;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.ThingTypeUID;
/**
* The {@link BindingConstants} class defines common constants used across the Sbus binding.
*
* @author Ciprian Pascu - Initial contribution
*/
@NonNullByDefault
public class BindingConstants {
private BindingConstants() {
// Prevent instantiation
}
public static final String BINDING_ID = "sbus";
// Bridge Type
public static final ThingTypeUID THING_TYPE_UDP_BRIDGE = new ThingTypeUID(BINDING_ID, "udp");
// Thing Types
public static final ThingTypeUID THING_TYPE_SWITCH = new ThingTypeUID(BINDING_ID, "switch");
public static final ThingTypeUID THING_TYPE_TEMPERATURE = new ThingTypeUID(BINDING_ID, "temperature");
public static final ThingTypeUID THING_TYPE_RGBW = new ThingTypeUID(BINDING_ID, "rgbw");
// Channel IDs for Switch Device
public static final String CHANNEL_SWITCH_STATE = "state";
// Channel IDs for Temperature Device
public static final String CHANNEL_TEMPERATURE = "temperature";
// Channel IDs for RGBW Device
public static final String CHANNEL_RED = "red";
public static final String CHANNEL_GREEN = "green";
public static final String CHANNEL_BLUE = "blue";
public static final String CHANNEL_WHITE = "white";
public static final String CHANNEL_COLOR = "color";
// Channel Types
public static final String CHANNEL_TYPE_COLOR = "color-channel";
public static final String CHANNEL_TYPE_SWITCH = "switch-channel";
public static final String CHANNEL_TYPE_DIMMER = "dimmer-channel";
public static final String CHANNEL_TYPE_PAIRED = "paired-channel";
}
@@ -0,0 +1,164 @@
/*
* 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.sbus.handler;
import static org.openhab.binding.sbus.BindingConstants.BINDING_ID;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.sbus.handler.config.SbusDeviceConfig;
import org.openhab.core.config.core.Configuration;
import org.openhab.core.i18n.LocaleProvider;
import org.openhab.core.i18n.TranslationProvider;
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.binding.BaseThingHandler;
import org.openhab.core.thing.binding.builder.ChannelBuilder;
import org.openhab.core.thing.binding.builder.ThingBuilder;
import org.openhab.core.thing.type.ChannelTypeUID;
import org.osgi.framework.Bundle;
import org.osgi.framework.FrameworkUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link AbstractSbusHandler} is the base class for all Sbus device handlers.
* It provides common functionality for device initialization, channel management, and polling.
*
* @author Ciprian Pascu - Initial contribution
*/
@NonNullByDefault
public abstract class AbstractSbusHandler extends BaseThingHandler {
protected final Logger logger = LoggerFactory.getLogger(getClass());
protected @Nullable SbusService sbusAdapter;
protected @Nullable ScheduledFuture<?> pollingJob;
protected final TranslationProvider translationProvider;
protected final LocaleProvider localeProvider;
public AbstractSbusHandler(Thing thing, TranslationProvider translationProvider, LocaleProvider localeProvider) {
super(thing);
this.translationProvider = translationProvider;
this.localeProvider = localeProvider;
}
@Override
public final void initialize() {
logger.debug("Initializing Sbus handler for thing {}", getThing().getUID());
initializeChannels();
Bridge bridge = getBridge();
if (bridge == null) {
Bundle bundle = FrameworkUtil.getBundle(getClass());
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
translationProvider.getText(bundle, "error.device.no-bridge", null, localeProvider.getLocale()));
return;
}
SbusBridgeHandler bridgeHandler = (SbusBridgeHandler) bridge.getHandler();
if (bridgeHandler == null || bridgeHandler.getThing().getStatus() != ThingStatus.ONLINE) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
return;
}
sbusAdapter = bridgeHandler.getSbusConnection();
if (sbusAdapter == null) {
Bundle bundle = FrameworkUtil.getBundle(getClass());
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED, translationProvider
.getText(bundle, "error.device.bridge-not-initialized", null, localeProvider.getLocale()));
return;
}
startPolling();
}
/**
* Initialize channels for this device based on its configuration.
* This method should be implemented by concrete handlers to set up their specific channels.
*/
protected abstract void initializeChannels();
/**
* Create or update a channel with the specified ID and type.
*
* @param channelId The ID of the channel to create/update
* @param channelTypeId The type ID of the channel
*/
protected void createChannel(String channelId, String channelTypeId) {
ThingBuilder thingBuilder = ThingBuilder.create(getThing().getThingTypeUID(), getThing().getUID())
.withConfiguration(getThing().getConfiguration()).withBridge(getThing().getBridgeUID());
// Add all existing channels except the one we're creating/updating
ChannelUID newChannelUID = new ChannelUID(getThing().getUID(), channelId);
for (Channel existingChannel : getThing().getChannels()) {
if (!existingChannel.getUID().equals(newChannelUID)) {
thingBuilder.withChannel(existingChannel);
}
}
// Add the new channel
Channel channel = ChannelBuilder.create(newChannelUID).withType(new ChannelTypeUID(BINDING_ID, channelTypeId))
.withConfiguration(new Configuration()).build();
thingBuilder.withChannel(channel);
// Update the thing with the new channel configuration
updateThing(thingBuilder.build());
}
/**
* Start polling the device for updates based on the configured refresh interval.
*/
protected void startPolling() {
SbusDeviceConfig config = getConfigAs(SbusDeviceConfig.class);
if (config.refresh > 0) {
pollingJob = scheduler.scheduleWithFixedDelay(() -> {
try {
pollDevice();
} catch (Exception e) {
Bundle bundle = FrameworkUtil.getBundle(getClass());
logger.warn("{}", translationProvider.getText(bundle, "error.device.polling", null,
localeProvider.getLocale()), e);
}
}, 0, config.refresh, TimeUnit.SECONDS);
}
}
/**
* Poll the device for updates. This method should be implemented by concrete handlers
* to update their specific channel states.
*/
protected abstract void pollDevice();
@Override
public void dispose() {
ScheduledFuture<?> job = pollingJob;
if (job != null) {
job.cancel(true);
}
final SbusService adapter = sbusAdapter;
if (adapter != null) {
adapter.close();
}
pollingJob = null;
sbusAdapter = null;
super.dispose();
}
}
@@ -0,0 +1,115 @@
/*
* 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.sbus.handler;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.sbus.handler.config.SbusBridgeConfig;
import org.openhab.core.i18n.LocaleProvider;
import org.openhab.core.i18n.TranslationProvider;
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.types.Command;
import org.osgi.framework.Bundle;
import org.osgi.framework.FrameworkUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link SbusBridgeHandler} is responsible for handling communication with the Sbus bridge.
*
* @author Ciprian Pascu - Initial contribution
*/
@NonNullByDefault
public class SbusBridgeHandler extends BaseBridgeHandler {
private final Logger logger = LoggerFactory.getLogger(SbusBridgeHandler.class);
private final @Nullable SbusService sbusService;
private final TranslationProvider translationProvider;
private final LocaleProvider localeProvider;
/**
* Constructs a new SbusBridgeHandler.
*
* @param bridge the bridge
* @param sbusService the Sbus service
*/
public SbusBridgeHandler(Bridge bridge, @Nullable SbusService sbusService, TranslationProvider translationProvider,
LocaleProvider localeProvider) {
super(bridge);
this.sbusService = sbusService;
this.translationProvider = translationProvider;
this.localeProvider = localeProvider;
}
/**
* Initializes the Sbus bridge handler by establishing a connection to the Sbus network.
*/
@Override
public void initialize() {
logger.debug("Initializing Sbus bridge handler for bridge {}", getThing().getUID());
// Get configuration using the config class
SbusBridgeConfig config = getConfigAs(SbusBridgeConfig.class);
if (config.host.isBlank()) {
Bundle bundle = FrameworkUtil.getBundle(getClass());
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, translationProvider.getText(bundle,
"error.bridge.host-not-configured", null, localeProvider.getLocale()));
return;
}
try {
// Initialize Sbus service with the configuration parameters
final SbusService service = sbusService;
if (service == null) {
Bundle bundle = FrameworkUtil.getBundle(getClass());
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.HANDLER_INITIALIZING_ERROR, translationProvider
.getText(bundle, "error.bridge.service-not-available", null, localeProvider.getLocale()));
return;
}
service.initialize(config.host, config.port);
updateStatus(ThingStatus.ONLINE);
} catch (Exception e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
}
}
/**
* Gets the Sbus service.
*
* @return the Sbus service
*/
public @Nullable SbusService getSbusConnection() {
return sbusService;
}
/**
* Disposes the handler by closing the Sbus connection.
*/
@Override
public void dispose() {
logger.debug("Disposing Sbus bridge handler");
final SbusService service = sbusService;
if (service != null) {
service.close();
}
super.dispose();
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
// Bridge doesn't handle commands directly
}
}
@@ -0,0 +1,101 @@
/*
* 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.sbus.handler;
import static org.openhab.binding.sbus.BindingConstants.*;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.i18n.LocaleProvider;
import org.openhab.core.i18n.TranslationProvider;
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.Component;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link SbusHandlerFactory} is responsible for creating things and thing handlers.
*
* @author Ciprian Pascu - Initial contribution
*/
@NonNullByDefault
@Component(service = ThingHandlerFactory.class, configurationPid = "binding.sbus")
public class SbusHandlerFactory extends BaseThingHandlerFactory {
private final Logger logger = LoggerFactory.getLogger(SbusHandlerFactory.class);
private @Nullable SbusService sbusService;
private @Nullable TranslationProvider translationProvider;
private @Nullable LocaleProvider localeProvider;
@Reference
public void setSbusService(final SbusService service) {
this.sbusService = service;
}
@Reference
public void setTranslationProvider(TranslationProvider translationProvider) {
this.translationProvider = translationProvider;
}
@Reference
public void setLocaleProvider(LocaleProvider localeProvider) {
this.localeProvider = localeProvider;
}
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_UDP_BRIDGE, THING_TYPE_SWITCH,
THING_TYPE_TEMPERATURE, THING_TYPE_RGBW);
@Override
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
}
@Override
protected @Nullable ThingHandler createHandler(Thing thing) {
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
final TranslationProvider tp = translationProvider;
final LocaleProvider lp = localeProvider;
if (tp == null || lp == null) {
logger.error("Required services not available");
return null;
}
if (thingTypeUID.equals(THING_TYPE_UDP_BRIDGE)) {
logger.debug("Creating Sbus UDP bridge handler for thing {}", thing.getUID());
return new SbusBridgeHandler((Bridge) thing, sbusService, tp, lp);
}
if (thingTypeUID.equals(THING_TYPE_SWITCH)) {
logger.debug("Creating Sbus switch handler for thing {}", thing.getUID());
return new SbusSwitchHandler(thing, tp, lp);
} else if (thingTypeUID.equals(THING_TYPE_TEMPERATURE)) {
logger.debug("Creating Sbus temperature handler for thing {}", thing.getUID());
return new SbusTemperatureHandler(thing, tp, lp);
} else if (thingTypeUID.equals(THING_TYPE_RGBW)) {
logger.debug("Creating Sbus RGBW handler for thing {}", thing.getUID());
return new SbusRgbwHandler(thing, tp, lp);
}
logger.debug("Unknown thing type: {}", thingTypeUID);
return null;
}
}
@@ -0,0 +1,202 @@
/*
* 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.sbus.handler;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.sbus.BindingConstants;
import org.openhab.binding.sbus.handler.config.SbusChannelConfig;
import org.openhab.binding.sbus.handler.config.SbusDeviceConfig;
import org.openhab.core.i18n.LocaleProvider;
import org.openhab.core.i18n.TranslationProvider;
import org.openhab.core.library.types.HSBType;
import org.openhab.core.library.types.OnOffType;
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.types.Command;
import org.openhab.core.util.ColorUtil;
import org.osgi.framework.Bundle;
import org.osgi.framework.FrameworkUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link SbusRgbwHandler} is responsible for handling commands for Sbus RGBW devices.
* It supports reading and controlling red, green, blue, and white color channels.
*
* @author Ciprian Pascu - Initial contribution
*/
@NonNullByDefault
public class SbusRgbwHandler extends AbstractSbusHandler {
private final Logger logger = LoggerFactory.getLogger(SbusRgbwHandler.class);
public SbusRgbwHandler(Thing thing, TranslationProvider translationProvider, LocaleProvider localeProvider) {
super(thing, translationProvider, localeProvider);
}
/**
* Checks if any RGBW value is greater than 0.
*
* @param rgbw an int array [R, G, B, W] each in [0..255]
* @return true if any value is greater than 0, false otherwise
*/
private boolean isAnyRgbwValueActive(int[] rgbw) {
if (rgbw.length < 4) {
return false;
}
for (int value : rgbw) {
if (value > 0) {
return true;
}
}
return false;
}
@Override
protected void initializeChannels() {
int switchChannelCount = 0;
// Validate all channel configurations
for (Channel channel : getThing().getChannels()) {
SbusChannelConfig channelConfig = channel.getConfiguration().as(SbusChannelConfig.class);
var channelTypeUID = channel.getChannelTypeUID();
if (channelTypeUID == null) {
Bundle bundle = FrameworkUtil.getBundle(getClass());
logger.warn("{}", translationProvider.getText(bundle, "error.channel.no-type",
channel.getUID().toString(), localeProvider.getLocale()));
continue;
}
String channelTypeId = channelTypeUID.getId();
if (BindingConstants.CHANNEL_TYPE_COLOR.equals(channelTypeId)) {
if (channelConfig.channelNumber <= 0) {
Bundle bundle = FrameworkUtil.getBundle(getClass());
logger.warn("{}", translationProvider.getText(bundle, "error.channel.invalid-number",
channel.getUID().toString(), localeProvider.getLocale()));
}
}
if (BindingConstants.CHANNEL_TYPE_SWITCH.equals(channelTypeId)) {
switchChannelCount++;
if (channelConfig.channelNumber <= 0) {
logger.warn("Channel {} has invalid channel number configuration", channel.getUID());
}
}
}
if (switchChannelCount > 1) {
Bundle bundle = FrameworkUtil.getBundle(getClass());
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, translationProvider.getText(bundle,
"error.rgbw.too-many-switches", getThing().getUID().toString(), localeProvider.getLocale()));
return;
}
}
@Override
protected void pollDevice() {
final SbusService adapter = super.sbusAdapter;
if (adapter == null) {
Bundle bundle = FrameworkUtil.getBundle(getClass());
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, translationProvider.getText(bundle,
"error.device.adapter-not-initialized", null, localeProvider.getLocale()));
return;
}
try {
SbusDeviceConfig config = getConfigAs(SbusDeviceConfig.class);
// Update all channels
for (Channel channel : getThing().getChannels()) {
if (!isLinked(channel.getUID())) {
continue;
}
var channelTypeUID = channel.getChannelTypeUID();
if (channelTypeUID == null) {
logger.warn("Channel {} has no channel type", channel.getUID().toString());
continue;
}
String channelTypeId = channelTypeUID.getId();
SbusChannelConfig channelConfig = channel.getConfiguration().as(SbusChannelConfig.class);
if (BindingConstants.CHANNEL_TYPE_COLOR.equals(channelTypeId)) {
// Read RGBW values for this channel
int[] rgbwValues = adapter.readRgbw(config.subnetId, config.id, channelConfig.channelNumber);
if (rgbwValues.length >= 4) {
// Convert RGBW to HSB using our custom conversion
HSBType hsbType = ColorUtil.rgbToHsb(rgbwValues);
updateState(channel.getUID(), hsbType);
}
} else if (BindingConstants.CHANNEL_TYPE_SWITCH.equals(channelTypeId)) {
// Read status channels for switch states
int[] statuses = adapter.readStatusChannels(config.subnetId, config.id);
// Update switch state
boolean isActive = isAnyRgbwValueActive(statuses);
updateState(channel.getUID(), isActive ? OnOffType.ON : OnOffType.OFF);
}
}
updateStatus(ThingStatus.ONLINE);
} catch (Exception e) {
Bundle bundle = FrameworkUtil.getBundle(getClass());
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
translationProvider.getText(bundle, "error.device.read-state", null, localeProvider.getLocale()));
}
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
final SbusService adapter = super.sbusAdapter;
if (adapter == null) {
Bundle bundle = FrameworkUtil.getBundle(getClass());
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, translationProvider.getText(bundle,
"error.device.adapter-not-initialized", null, localeProvider.getLocale()));
return;
}
try {
Channel channel = getThing().getChannel(channelUID.getId());
if (channel != null) {
var channelTypeUID = channel.getChannelTypeUID();
if (channelTypeUID == null) {
Bundle bundle = FrameworkUtil.getBundle(getClass());
logger.warn("{}", translationProvider.getText(bundle, "error.channel.no-type",
channel.getUID().toString(), localeProvider.getLocale()));
return;
}
String channelTypeId = channelTypeUID.getId();
SbusDeviceConfig config = getConfigAs(SbusDeviceConfig.class);
SbusChannelConfig channelConfig = channel.getConfiguration().as(SbusChannelConfig.class);
if (BindingConstants.CHANNEL_TYPE_COLOR.equals(channelTypeId)
&& command instanceof HSBType hsbCommand) {
// Handle color command
int[] rgbw = ColorUtil.hsbToRgbw(hsbCommand);
adapter.writeRgbw(config.subnetId, config.id, channelConfig.channelNumber, rgbw);
updateState(channelUID, hsbCommand);
} else if (BindingConstants.CHANNEL_TYPE_SWITCH.equals(channelTypeId)
&& command instanceof OnOffType onOffCommand) {
// Handle switch command
boolean isOn = onOffCommand == OnOffType.ON;
adapter.writeSingleChannel(config.subnetId, config.id, channelConfig.channelNumber, isOn ? 100 : 0,
-1);
updateState(channelUID, OnOffType.from(isOn));
}
}
} catch (Exception e) {
Bundle bundle = FrameworkUtil.getBundle(getClass());
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
translationProvider.getText(bundle, "error.device.send-command", null, localeProvider.getLocale()));
}
}
}
@@ -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.sbus.handler;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link SbusService} defines the interface for handling Sbus communication.
*
* @author Ciprian Pascu - Initial contribution
*/
@NonNullByDefault
public interface SbusService {
/**
* Reads temperature values from a device.
*
* @param subnetId the subnet ID of the device
* @param id the device ID
* @param temperatureUnit The unit of measurement (FAHRENHEIT or CELSIUS)
* @return array of temperature values in Celsius
* @throws Exception if reading fails
*/
float[] readTemperatures(int subnetId, int id, TemperatureUnit temperatureUnit) throws Exception;
/**
* Reads RGBW values from a device channel.
*
* @param subnetId the subnet ID of the device
* @param id the device ID
* @param channelNumber the channel number to read
* @return array of RGBW values [R, G, B, W]
* @throws Exception if reading fails
*/
int[] readRgbw(int subnetId, int id, int channelNumber) throws Exception;
/**
* Reads status values from device channels.
*
* @param subnetId the subnet ID of the device
* @param id the device ID
* @return array of channel status values
* @throws Exception if reading fails
*/
int[] readStatusChannels(int subnetId, int id) throws Exception;
/**
* Writes RGBW values to a device channel.
*
* @param subnetId the subnet ID of the device
* @param id the device ID
* @param channelNumber the channel number to write to
* @param color An array of 4 integers representing RGBW values (0-255 each)
* @throws Exception if writing fails
*/
void writeRgbw(int subnetId, int id, int channelNumber, int[] color) throws Exception;
/**
* Writes a value to a single channel.
*
* @param subnetId the subnet ID of the device
* @param id the device ID
* @param channelNumber the channel number to write to
* @param value the value to write
* @param timer timer value (-1 for no timer)
* @throws Exception if writing fails
*/
void writeSingleChannel(int subnetId, int id, int channelNumber, int value, int timer) throws Exception;
/**
* Closes the service and releases resources.
*/
void close();
void initialize(String host, int port) throws Exception;
}
@@ -0,0 +1,189 @@
/*
* 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.sbus.handler;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.sbus.BindingConstants;
import org.openhab.binding.sbus.handler.config.SbusChannelConfig;
import org.openhab.binding.sbus.handler.config.SbusDeviceConfig;
import org.openhab.core.i18n.LocaleProvider;
import org.openhab.core.i18n.TranslationProvider;
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.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.types.Command;
import org.osgi.framework.Bundle;
import org.osgi.framework.FrameworkUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link SbusSwitchHandler} is responsible for handling commands for Sbus switch devices.
* It supports reading the current state and switching the device on/off.
*
* @author Ciprian Pascu - Initial contribution
*/
@NonNullByDefault
public class SbusSwitchHandler extends AbstractSbusHandler {
private final Logger logger = LoggerFactory.getLogger(SbusSwitchHandler.class);
public SbusSwitchHandler(Thing thing, TranslationProvider translationProvider, LocaleProvider localeProvider) {
super(thing, translationProvider, localeProvider);
}
@Override
protected void initializeChannels() {
// Get all channel configurations from the thing
for (Channel channel : getThing().getChannels()) {
// Channels are already defined in thing-types.xml, just validate their configuration
SbusChannelConfig channelConfig = channel.getConfiguration().as(SbusChannelConfig.class);
if (channelConfig.channelNumber <= 0) {
Bundle bundle = FrameworkUtil.getBundle(getClass());
logger.warn("{}", translationProvider.getText(bundle, "error.channel.invalid-number",
channel.getUID().toString(), localeProvider.getLocale()));
}
}
}
@Override
protected void pollDevice() {
final SbusService adapter = super.sbusAdapter;
if (adapter == null) {
Bundle bundle = FrameworkUtil.getBundle(getClass());
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, translationProvider.getText(bundle,
"error.device.adapter-not-initialized", null, localeProvider.getLocale()));
return;
}
try {
SbusDeviceConfig config = getConfigAs(SbusDeviceConfig.class);
int[] statuses = adapter.readStatusChannels(config.subnetId, config.id);
// Iterate over all channels and update their states
for (Channel channel : getThing().getChannels()) {
if (!isLinked(channel.getUID())) {
continue;
}
SbusChannelConfig channelConfig = channel.getConfiguration().as(SbusChannelConfig.class);
if (channelConfig.channelNumber > 0 && channelConfig.channelNumber <= statuses.length) {
var channelTypeUID = channel.getChannelTypeUID();
if (channelTypeUID == null) {
Bundle bundle = FrameworkUtil.getBundle(getClass());
logger.warn("{}", translationProvider.getText(bundle, "error.channel.no-type",
channel.getUID().toString(), localeProvider.getLocale()));
continue;
}
String channelTypeId = channelTypeUID.getId();
// 100 when on, something else when off
boolean isActive = statuses[channelConfig.channelNumber - 1] == 0x64;
if (BindingConstants.CHANNEL_TYPE_SWITCH.equals(channelTypeId)) {
updateState(channel.getUID(), OnOffType.from(isActive));
} else if (BindingConstants.CHANNEL_TYPE_DIMMER.equals(channelTypeId)) {
updateState(channel.getUID(), new PercentType(statuses[channelConfig.channelNumber - 1]));
} else if (BindingConstants.CHANNEL_TYPE_PAIRED.equals(channelTypeId)) {
updateState(channel.getUID(), isActive ? OpenClosedType.OPEN : OpenClosedType.CLOSED);
}
}
}
} catch (Exception e) {
Bundle bundle = FrameworkUtil.getBundle(getClass());
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
translationProvider.getText(bundle, "error.device.read-state", null, localeProvider.getLocale()));
}
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
final SbusService adapter = super.sbusAdapter;
if (adapter == null) {
Bundle bundle = FrameworkUtil.getBundle(getClass());
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, translationProvider.getText(bundle,
"error.device.adapter-not-initialized", null, localeProvider.getLocale()));
return;
}
try {
Channel channel = getThing().getChannel(channelUID);
if (channel != null) {
SbusChannelConfig channelConfig = channel.getConfiguration().as(SbusChannelConfig.class);
if (channelConfig.channelNumber <= 0) {
Bundle bundle = FrameworkUtil.getBundle(getClass());
logger.warn("{}", translationProvider.getText(bundle, "error.channel.invalid-number",
channelUID.toString(), localeProvider.getLocale()));
return;
}
SbusDeviceConfig config = getConfigAs(SbusDeviceConfig.class);
if (command instanceof OnOffType onOffCommand) {
handleOnOffCommand(onOffCommand, config, channelConfig, channelUID, adapter);
} else if (command instanceof PercentType percentCommand) {
handlePercentCommand(percentCommand, config, channelConfig, channelUID, adapter);
} else if (command instanceof OpenClosedType openClosedCommand) {
handleOpenClosedCommand(openClosedCommand, config, channelConfig, channelUID, adapter);
}
}
} catch (Exception e) {
Bundle bundle = FrameworkUtil.getBundle(getClass());
logger.warn("{}",
translationProvider.getText(bundle, "error.device.send-command", null, localeProvider.getLocale()),
e);
}
}
private void handleOnOffCommand(OnOffType command, SbusDeviceConfig config, SbusChannelConfig channelConfig,
ChannelUID channelUID, SbusService adapter) throws Exception {
boolean isOn = command == OnOffType.ON;
adapter.writeSingleChannel(config.subnetId, config.id, channelConfig.channelNumber, isOn ? 100 : 0,
channelConfig.timer);
updateState(channelUID, OnOffType.from(isOn));
}
private void handlePercentCommand(PercentType command, SbusDeviceConfig config, SbusChannelConfig channelConfig,
ChannelUID channelUID, SbusService adapter) throws Exception {
adapter.writeSingleChannel(config.subnetId, config.id, channelConfig.channelNumber, command.intValue(),
channelConfig.timer);
updateState(channelUID, command);
}
private void handleOpenClosedCommand(OpenClosedType command, SbusDeviceConfig config,
SbusChannelConfig channelConfig, ChannelUID channelUID, SbusService adapter) throws Exception {
boolean isOpen = command == OpenClosedType.OPEN;
// Set main channel
if (getChannelToClose(channelConfig, isOpen) > 0) {
adapter.writeSingleChannel(config.subnetId, config.id, getChannelToClose(channelConfig, isOpen), 0,
channelConfig.timer);
}
// Set paired channel to opposite state if configured
if (getChannelToOpen(channelConfig, isOpen) > 0) {
adapter.writeSingleChannel(config.subnetId, config.id, getChannelToOpen(channelConfig, isOpen), 0x64,
channelConfig.timer);
}
updateState(channelUID, isOpen ? OpenClosedType.OPEN : OpenClosedType.CLOSED);
}
private int getChannelToOpen(SbusChannelConfig channelConfig, boolean state) {
return state ? channelConfig.channelNumber : channelConfig.pairedChannelNumber;
}
private int getChannelToClose(SbusChannelConfig channelConfig, boolean state) {
return state ? channelConfig.pairedChannelNumber : channelConfig.channelNumber;
}
}
@@ -0,0 +1,115 @@
/*
* 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.sbus.handler;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.sbus.handler.config.SbusDeviceConfig;
import org.openhab.binding.sbus.handler.config.TemperatureChannelConfig;
import org.openhab.core.i18n.LocaleProvider;
import org.openhab.core.i18n.TranslationProvider;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.unit.ImperialUnits;
import org.openhab.core.library.unit.SIUnits;
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.types.Command;
import org.osgi.framework.Bundle;
import org.osgi.framework.FrameworkUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link SbusTemperatureHandler} is responsible for handling commands for Sbus temperature sensors.
* It supports reading temperature values in Celsius.
*
* @author Ciprian Pascu - Initial contribution
*/
@NonNullByDefault
public class SbusTemperatureHandler extends AbstractSbusHandler {
private final Logger logger = LoggerFactory.getLogger(SbusTemperatureHandler.class);
public SbusTemperatureHandler(Thing thing, TranslationProvider translationProvider, LocaleProvider localeProvider) {
super(thing, translationProvider, localeProvider);
}
@Override
protected void initializeChannels() {
// Get all channel configurations from the thing
for (Channel channel : getThing().getChannels()) {
// Channels are already defined in thing-types.xml, just validate their configuration
TemperatureChannelConfig channelConfig = channel.getConfiguration().as(TemperatureChannelConfig.class);
if (!channelConfig.isValid()) {
Bundle bundle = FrameworkUtil.getBundle(getClass());
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
translationProvider.getText(bundle, "error.channel.invalid-number", channel.getUID().toString(),
localeProvider.getLocale()));
return;
}
}
}
@Override
protected void pollDevice() {
final SbusService adapter = super.sbusAdapter;
if (adapter == null) {
Bundle bundle = FrameworkUtil.getBundle(getClass());
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, translationProvider.getText(bundle,
"error.device.adapter-not-initialized", null, localeProvider.getLocale()));
return;
}
try {
SbusDeviceConfig config = getConfigAs(SbusDeviceConfig.class);
// Read temperatures in Celsius from device
float[] temperatures = adapter.readTemperatures(config.subnetId, config.id, TemperatureUnit.CELSIUS);
// Iterate over all channels and update their states with corresponding temperatures
for (Channel channel : getThing().getChannels()) {
if (!isLinked(channel.getUID())) {
continue;
}
TemperatureChannelConfig channelConfig = channel.getConfiguration().as(TemperatureChannelConfig.class);
if (channelConfig.channelNumber > 0 && channelConfig.channelNumber <= temperatures.length) {
float temperatureCelsius = temperatures[channelConfig.channelNumber - 1];
if (channelConfig.isFahrenheit()) {
// Convert Celsius to Fahrenheit
float temperatureFahrenheit = (temperatureCelsius * 9 / 5) + 32;
updateState(channel.getUID(),
new QuantityType<>(temperatureFahrenheit, ImperialUnits.FAHRENHEIT));
} else {
updateState(channel.getUID(), new QuantityType<>(temperatureCelsius, SIUnits.CELSIUS));
}
}
}
updateStatus(ThingStatus.ONLINE);
} catch (Exception e) {
Bundle bundle = FrameworkUtil.getBundle(getClass());
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
translationProvider.getText(bundle, "error.device.read-state", null, localeProvider.getLocale()));
}
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
// Temperature sensors are read-only
Bundle bundle = FrameworkUtil.getBundle(getClass());
logger.debug("{}",
translationProvider.getText(bundle, "info.temperature.readonly", null, localeProvider.getLocale()));
}
}
@@ -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.sbus.handler;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link TemperatureUnit} defines the available temperature units.
*
* @author Ciprian Pascu - Initial contribution
*/
@NonNullByDefault
public enum TemperatureUnit {
FAHRENHEIT(0),
CELSIUS(1);
private final int value;
TemperatureUnit(int value) {
this.value = value;
}
public int getValue() {
return value;
}
public static TemperatureUnit fromValue(int value) {
for (TemperatureUnit unit : values()) {
if (unit.getValue() == value) {
return unit;
}
}
throw new IllegalArgumentException("Invalid temperature unit value: " + value);
}
}
@@ -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.sbus.handler.config;
import org.eclipse.jdt.annotation.NonNullByDefault;
import ro.ciprianpascu.sbus.Sbus;
/**
* The {@link SbusBridgeConfig} class contains fields mapping bridge configuration parameters.
*
* @author Ciprian Pascu - Initial contribution
*/
@NonNullByDefault
public class SbusBridgeConfig {
/**
* The host address of the Sbus bridge
*/
public String host = "localhost";
/**
* The port number for Sbus communication
*/
public int port = Sbus.DEFAULT_PORT;
}
@@ -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.sbus.handler.config;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link SbusChannelConfig} class contains fields mapping channel configuration parameters.
*
* @author Ciprian Pascu - Initial contribution
*/
@NonNullByDefault
public class SbusChannelConfig {
/**
* The physical channel number on the Sbus device.
*/
public int channelNumber;
/**
* The paired channel number for OpenClosedType channels.
* When the main channel is opened, this channel will be closed and vice versa.
*/
public int pairedChannelNumber;
/**
* Timer in seconds to automatically turn off the switch (-1 = disabled).
*/
public int timer = 0;
}
@@ -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.sbus.handler.config;
import org.eclipse.jdt.annotation.NonNullByDefault;
import ro.ciprianpascu.sbus.Sbus;
/**
* The {@link SbusDeviceConfig} class contains fields mapping thing configuration parameters.
*
* @author Ciprian Pascu - Initial contribution
*/
@NonNullByDefault
public class SbusDeviceConfig {
/**
* The ID of the Sbus device
*/
public int id = Sbus.DEFAULT_UNIT_ID;
/**
* The subnet ID for Sbus communication
*/
public int subnetId = Sbus.DEFAULT_SUBNET_ID;
/**
* Refresh interval in seconds
*/
public int refresh = 30; // Default value from thing-types.xml
}
@@ -0,0 +1,52 @@
/*
* 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.sbus.handler.config;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Configuration class for the temperature channel.
*
* @author Ciprian Pascu - Initial contribution
*/
@NonNullByDefault
public class TemperatureChannelConfig {
/**
* The physical channel number on the Sbus device
*/
public int channelNumber = 1;
/**
* The unit to use for temperature readings (CELSIUS or FAHRENHEIT)
*/
public String unit = "CELSIUS";
/**
* Validates the configuration parameters.
*
* @return true if the configuration is valid
*/
public boolean isValid() {
return channelNumber > 0 && ("CELSIUS".equals(unit) || "FAHRENHEIT".equals(unit));
}
/**
* Checks if the configured unit is Fahrenheit
*
* @return true if Fahrenheit is configured, false otherwise
*/
public boolean isFahrenheit() {
return "FAHRENHEIT".equals(unit);
}
}
@@ -0,0 +1,110 @@
/*
* 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.sbus.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.sbus.handler.SbusService;
import org.openhab.binding.sbus.handler.TemperatureUnit;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Deactivate;
import ro.ciprianpascu.sbus.facade.SbusAdapter;
/**
* The {@link SbusServiceImpl} implements the SbusService interface by delegating to SbusAdapter.
*
* @author Ciprian Pascu - Initial contribution
*/
@Component(service = SbusService.class)
@NonNullByDefault
public class SbusServiceImpl implements SbusService {
private @Nullable SbusAdapter adapter;
@Activate
public SbusServiceImpl() {
// Service is activated but adapter is initialized later with connection parameters
}
/**
* Initializes the underlying SbusAdapter with connection parameters.
*
* @param host the host address of the Sbus device
* @param port the port number to use
* @throws Exception if initialization fails
*/
@Override
public void initialize(String host, int port) throws Exception {
this.adapter = new SbusAdapter(host, port);
}
@Deactivate
public void deactivate() {
close();
}
@Override
public float[] readTemperatures(int subnetId, int id, TemperatureUnit temperatureUnit) throws Exception {
final SbusAdapter adapter = this.adapter;
if (adapter == null) {
throw new IllegalStateException("SbusAdapter not initialized");
}
return adapter.readTemperatures(subnetId, id, temperatureUnit.getValue());
}
@Override
public int[] readRgbw(int subnetId, int id, int channelNumber) throws Exception {
final SbusAdapter adapter = this.adapter;
if (adapter == null) {
throw new IllegalStateException("SbusAdapter not initialized");
}
return adapter.readRgbw(subnetId, id, channelNumber);
}
@Override
public int[] readStatusChannels(int subnetId, int id) throws Exception {
final SbusAdapter adapter = this.adapter;
if (adapter == null) {
throw new IllegalStateException("SbusAdapter not initialized");
}
return adapter.readStatusChannels(subnetId, id);
}
@Override
public void writeRgbw(int subnetId, int id, int channelNumber, int[] color) throws Exception {
final SbusAdapter adapter = this.adapter;
if (adapter == null) {
throw new IllegalStateException("SbusAdapter not initialized");
}
adapter.writeRgbw(subnetId, id, channelNumber, color);
}
@Override
public void writeSingleChannel(int subnetId, int id, int channelNumber, int value, int timer) throws Exception {
final SbusAdapter adapter = this.adapter;
if (adapter == null) {
throw new IllegalStateException("SbusAdapter not initialized");
}
adapter.writeSingleChannel(subnetId, id, channelNumber, value, timer);
}
@Override
public void close() {
final SbusAdapter adapter = this.adapter;
if (adapter != null) {
adapter.close();
this.adapter = null;
}
}
}
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<addon:addon id="sbus" 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>Sbus Binding</name>
<description>Binding for Sbus</description>
<connection>local</connection>
</addon:addon>
@@ -0,0 +1,124 @@
# add-on
addon.sbus.name = Sbus Binding
addon.sbus.description = Binding for Sbus
# thing types
thing-type.sbus.rgbw.label = Sbus RGBW Controller
thing-type.sbus.rgbw.description = Sbus RGBW lighting controller
thing-type.sbus.switch.label = Sbus Switch
thing-type.sbus.switch.description = Sbus switch device
thing-type.sbus.temperature.label = Sbus Temperature Sensor
thing-type.sbus.temperature.description = Sbus temperature sensor device
thing-type.sbus.udp.label = Sbus UDP Bridge
thing-type.sbus.udp.description = Endpoint for Sbus UDP slaves
# thing types config
thing-type.config.sbus.rgbw.id.label = Device ID
thing-type.config.sbus.rgbw.id.description = The ID of the Sbus device
thing-type.config.sbus.rgbw.refresh.label = Refresh Interval
thing-type.config.sbus.rgbw.refresh.description = Refresh interval in seconds
thing-type.config.sbus.rgbw.subnetId.label = SubnetId
thing-type.config.sbus.rgbw.subnetId.description = Slave subnet id. Can take any value between 1 and 255. 255 for broadcast.
thing-type.config.sbus.rgbw.subnetId.option.1 = 1
thing-type.config.sbus.rgbw.subnetId.option.255 = 255
thing-type.config.sbus.switch.id.label = Device ID
thing-type.config.sbus.switch.id.description = The ID of the Sbus device
thing-type.config.sbus.switch.refresh.label = Refresh Interval
thing-type.config.sbus.switch.refresh.description = Refresh interval in seconds
thing-type.config.sbus.switch.subnetId.label = SubnetId
thing-type.config.sbus.switch.subnetId.description = Slave subnet id. Can take any value between 1 and 255. 255 for broadcast.
thing-type.config.sbus.switch.subnetId.option.1 = 1
thing-type.config.sbus.switch.subnetId.option.255 = 255
thing-type.config.sbus.temperature.id.label = Device ID
thing-type.config.sbus.temperature.id.description = The ID of the Sbus device
thing-type.config.sbus.temperature.refresh.label = Refresh Interval
thing-type.config.sbus.temperature.refresh.description = Refresh interval in seconds
thing-type.config.sbus.temperature.subnetId.label = SubnetId
thing-type.config.sbus.temperature.subnetId.description = Slave subnet id. Can take any value between 1 and 255. 255 for broadcast.
thing-type.config.sbus.temperature.subnetId.option.1 = 1
thing-type.config.sbus.temperature.subnetId.option.255 = 255
thing-type.config.sbus.udp.host.label = IP Address or Hostname
thing-type.config.sbus.udp.host.description = Network address of the device
thing-type.config.sbus.udp.port.label = Port
thing-type.config.sbus.udp.port.description = Port of the slave
# channel types
channel-type.sbus.color-channel.label = Color
channel-type.sbus.color-channel.description = Color control
channel-type.sbus.dimmer-channel.label = Dimmer State
channel-type.sbus.dimmer-channel.description = Dimmer state (0-100%)
channel-type.sbus.paired-channel.label = Paired Channel State
channel-type.sbus.paired-channel.description = Paired channel state (OPEN/CLOSED) - controls two opposite channels
channel-type.sbus.switch-channel.label = Switch State
channel-type.sbus.switch-channel.description = Switch state (ON/OFF)
channel-type.sbus.temperature-channel.label = Temperature
channel-type.sbus.temperature-channel.description = Temperature reading from the device
# channel types config
channel-type.config.sbus.color-channel.channelNumber.label = Channel Number
channel-type.config.sbus.color-channel.channelNumber.description = The physical channel number on the Sbus device
channel-type.config.sbus.dimmer-channel.channelNumber.label = Channel Number
channel-type.config.sbus.dimmer-channel.channelNumber.description = The physical channel number on the Sbus device
channel-type.config.sbus.dimmer-channel.timer.label = Timer
channel-type.config.sbus.dimmer-channel.timer.description = Timer in seconds to automatically turn off the switch (0 = disabled)
channel-type.config.sbus.paired-channel.channelNumber.label = Channel Number
channel-type.config.sbus.paired-channel.channelNumber.description = The physical channel number on the Sbus device
channel-type.config.sbus.paired-channel.pairedChannelNumber.label = Paired Channel Number
channel-type.config.sbus.paired-channel.pairedChannelNumber.description = The physical channel number of the paired channel (will be set to opposite state)
channel-type.config.sbus.switch-channel.channelNumber.label = Channel Number
channel-type.config.sbus.switch-channel.channelNumber.description = The physical channel number on the Sbus device
channel-type.config.sbus.temperature-channel.channelNumber.label = Channel Number
channel-type.config.sbus.temperature-channel.channelNumber.description = The physical channel number on the Sbus device
channel-type.config.sbus.temperature-channel.unit.label = Temperature Unit
channel-type.config.sbus.temperature-channel.unit.description = The unit to use for temperature readings (°C or °F)
channel-type.config.sbus.temperature-channel.unit.option.CELSIUS = Celsius
channel-type.config.sbus.temperature-channel.unit.option.FAHRENHEIT = Fahrenheit
# thing types config
thing-type.config.sbus.udp.afterConnectionDelayMillis.label = Connection warm-up time
thing-type.config.sbus.udp.afterConnectionDelayMillis.description = Connection warm-up time. Additional time which is spent on preparing connection which should be spent waiting while end device is getting ready to answer first sbus call. In milliseconds.
thing-type.config.sbus.udp.connectMaxTries.label = Maximum Connection Tries
thing-type.config.sbus.udp.connectMaxTries.description = How many times we try to establish the connection. Should be at least 1.
thing-type.config.sbus.udp.connectTimeoutMillis.label = Timeout for Establishing the Connection
thing-type.config.sbus.udp.connectTimeoutMillis.description = The maximum time that is waited when establishing the connection. Value of zero means that system/OS default is respected. In milliseconds.
thing-type.config.sbus.udp.enableDiscovery.label = Discovery Enabled
thing-type.config.sbus.udp.enableDiscovery.description = When enabled we try to find a device specific handler. Turn this on if you're using one of the supported devices.
thing-type.config.sbus.udp.reconnectAfterMillis.label = Reconnect Again After
thing-type.config.sbus.udp.reconnectAfterMillis.description = The connection is kept open at least the time specified here. Value of zero means that connection is disconnected after every Sbus transaction. In milliseconds.
thing-type.config.sbus.udp.rtuEncoded.label = RTU Encoding
thing-type.config.sbus.udp.rtuEncoded.description = Use RTU Encoding over IP
thing-type.config.sbus.udp.timeBetweenReconnectMillis.label = Time Between Reconnections
thing-type.config.sbus.udp.timeBetweenReconnectMillis.description = How long to wait to before trying to establish a new connection after the previous one has been disconnected. In milliseconds.
thing-type.config.sbus.udp.timeBetweenTransactionsMillis.label = Time Between Transactions
thing-type.config.sbus.udp.timeBetweenTransactionsMillis.description = How long to delay we must have at minimum between two consecutive Sbus transactions. In milliseconds.
# channel group types
channel-group-type.sbus.colors.label = Color Channels
channel-group-type.sbus.colors.description = Group of RGBW color channels
channel-group-type.sbus.sensors.label = Temperature Sensors
channel-group-type.sbus.sensors.description = Group of temperature sensors
channel-group-type.sbus.switches.label = Switch Channels
channel-group-type.sbus.switches.description = Group of switch channels
# error messages
error.bridge.host-not-configured = Host address not configured
error.bridge.service-not-available = Sbus service not available
error.device.no-bridge = No bridge configured
error.device.bridge-not-initialized = Bridge connection not initialized
error.device.polling = Error polling Sbus device
error.channel.no-type = Channel {0} has no channel type
error.channel.invalid-number = Channel {0} has invalid channel number configuration
error.rgbw.too-many-switches = Only one switch channel is allowed for RGBW thing {0}
error.device.adapter-not-initialized = Sbus adapter not initialized
error.device.read-state = Error reading device state
error.device.send-command = Error sending command to device
# info messages
info.temperature.readonly = Temperature device is read-only, ignoring command
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="sbus"
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="udp">
<label>Sbus UDP Bridge</label>
<description>Endpoint for Sbus UDP slaves</description>
<config-description>
<parameter name="host" type="text" required="true">
<label>IP Address or Hostname</label>
<description>Network address of the device</description>
<default>localhost</default>
<context>network-address</context>
</parameter>
<parameter name="port" type="integer">
<label>Port</label>
<description>Port of the slave</description>
<default>6000</default>
</parameter>
</config-description>
</bridge-type>
</thing:thing-descriptions>
@@ -0,0 +1,185 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="sbus"
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">
<!-- Switch Device -->
<thing-type id="switch">
<supported-bridge-type-refs>
<bridge-type-ref id="udp"/>
</supported-bridge-type-refs>
<label>Sbus Switch</label>
<description>Sbus switch device</description>
<config-description>
<parameter name="subnetId" type="integer">
<label>SubnetId</label>
<description>Slave subnet id. Can take any value between 1 and 255. 255 for broadcast.</description>
<default>1</default>
<options>
<option value="1">1</option>
<option value="255">255</option>
</options>
</parameter>
<parameter name="id" type="integer" required="true">
<label>Device ID</label>
<description>The ID of the Sbus device</description>
</parameter>
<parameter name="refresh" type="integer">
<label>Refresh Interval</label>
<description>Refresh interval in seconds</description>
<default>30</default>
<unitLabel>s</unitLabel>
</parameter>
</config-description>
</thing-type>
<!-- Temperature Device -->
<thing-type id="temperature">
<supported-bridge-type-refs>
<bridge-type-ref id="udp"/>
</supported-bridge-type-refs>
<label>Sbus Temperature Sensor</label>
<description>Sbus temperature sensor device</description>
<config-description>
<parameter name="subnetId" type="integer">
<label>SubnetId</label>
<description>Slave subnet id. Can take any value between 1 and 255. 255 for broadcast.</description>
<default>1</default>
<options>
<option value="1">1</option>
<option value="255">255</option>
</options>
</parameter>
<parameter name="id" type="integer" required="true">
<label>Device ID</label>
<description>The ID of the Sbus device</description>
</parameter>
<parameter name="refresh" type="integer">
<label>Refresh Interval</label>
<description>Refresh interval in seconds</description>
<default>30</default>
<unitLabel>s</unitLabel>
</parameter>
</config-description>
</thing-type>
<!-- RGBW Device -->
<thing-type id="rgbw">
<supported-bridge-type-refs>
<bridge-type-ref id="udp"/>
</supported-bridge-type-refs>
<label>Sbus RGBW Controller</label>
<description>Sbus RGBW lighting controller</description>
<config-description>
<parameter name="subnetId" type="integer">
<label>SubnetId</label>
<description>Slave subnet id. Can take any value between 1 and 255. 255 for broadcast.</description>
<default>1</default>
<options>
<option value="1">1</option>
<option value="255">255</option>
</options>
</parameter>
<parameter name="id" type="integer" required="true">
<label>Device ID</label>
<description>The ID of the Sbus device</description>
</parameter>
<parameter name="refresh" type="integer">
<label>Refresh Interval</label>
<description>Refresh interval in seconds</description>
<default>30</default>
<unitLabel>s</unitLabel>
</parameter>
</config-description>
</thing-type>
<!-- Channel Types -->
<channel-type id="switch-channel">
<item-type>Switch</item-type>
<label>Switch State</label>
<description>Switch state (ON/OFF)</description>
<category>Switch</category>
<config-description>
<parameter name="channelNumber" type="integer" required="true">
<label>Channel Number</label>
<description>The physical channel number on the Sbus device</description>
</parameter>
</config-description>
</channel-type>
<channel-type id="dimmer-channel">
<item-type>Dimmer</item-type>
<label>Dimmer State</label>
<description>Dimmer state (0-100%)</description>
<category>DimmableLight</category>
<config-description>
<parameter name="channelNumber" type="integer" required="true">
<label>Channel Number</label>
<description>The physical channel number on the Sbus device</description>
</parameter>
<parameter name="timer" type="integer">
<label>Timer</label>
<description>Timer in seconds to automatically turn off the switch (0 = disabled)</description>
<default>0</default>
<advanced>true</advanced>
<unitLabel>s</unitLabel>
</parameter>
</config-description>
</channel-type>
<channel-type id="paired-channel">
<item-type>Contact</item-type>
<label>Paired Channel State</label>
<description>Paired channel state (OPEN/CLOSED) - controls two opposite channels</description>
<category>Contact</category>
<config-description>
<parameter name="channelNumber" type="integer" required="true">
<label>Channel Number</label>
<description>The physical channel number on the Sbus device</description>
</parameter>
<parameter name="pairedChannelNumber" type="integer" required="true">
<label>Paired Channel Number</label>
<description>The physical channel number of the paired channel (will be set to opposite state)</description>
</parameter>
</config-description>
</channel-type>
<channel-type id="temperature-channel">
<item-type>Number:Temperature</item-type>
<label>Temperature</label>
<description>Temperature reading from the device</description>
<category>Temperature</category>
<state readOnly="true" pattern="%.1f %unit%"/>
<config-description>
<parameter name="channelNumber" type="integer" required="true">
<label>Channel Number</label>
<description>The physical channel number on the Sbus device</description>
</parameter>
<parameter name="unit" type="text">
<label>Temperature Unit</label>
<description>The unit to use for temperature readings (°C or °F)</description>
<options>
<option value="CELSIUS">Celsius</option>
<option value="FAHRENHEIT">Fahrenheit</option>
</options>
<default>CELSIUS</default>
</parameter>
</config-description>
</channel-type>
<channel-type id="color-channel">
<item-type>Color</item-type>
<label>Color</label>
<description>Color control</description>
<category>ColorLight</category>
<config-description>
<parameter name="channelNumber" type="integer" required="true">
<label>Channel Number</label>
<description>The physical channel number on the Sbus device</description>
</parameter>
</config-description>
</channel-type>
</thing:thing-descriptions>
+1
View File
@@ -365,6 +365,7 @@
<module>org.openhab.binding.salus</module>
<module>org.openhab.binding.samsungtv</module>
<module>org.openhab.binding.satel</module>
<module>org.openhab.binding.sbus</module>
<module>org.openhab.binding.semsportal</module>
<module>org.openhab.binding.senechome</module>
<module>org.openhab.binding.seneye</module>