diff --git a/CODEOWNERS b/CODEOWNERS index 97bd8089e8d..84a1767a37d 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -331,6 +331,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 diff --git a/bundles/org.openhab.binding.sbus/NOTICE b/bundles/org.openhab.binding.sbus/NOTICE new file mode 100644 index 00000000000..38d625e3492 --- /dev/null +++ b/bundles/org.openhab.binding.sbus/NOTICE @@ -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 diff --git a/bundles/org.openhab.binding.sbus/README.md b/bundles/org.openhab.binding.sbus/README.md new file mode 100644 index 00000000000..064946b30c5 --- /dev/null +++ b/bundles/org.openhab.binding.sbus/README.md @@ -0,0 +1,132 @@ +# 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 requires 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 | Paired | RW | ON/OFF state for two paired channels (e.g., curtains) | + +## Full Example + +### Thing Configuration + +``` +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 + +``` +// 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" } + +// RGBW Controller with Power Control +Group gLight "RGBW Light" ["Lighting"] +Color rgbwColor "Color" (gLight) ["Control", "Light"] { channel="sbus:rgbw:mybridge:colorctrl:color" } +Switch rgbwPower "Power" (gLight) ["Switch", "Light"] { channel="sbus:rgbw:mybridge:colorctrl:power" } +``` + +### Sitemap Configuration + +``` +sitemap sbus label="Sbus Demo" +{ + Frame label="Sbus Controls" { + Colorpicker item=Light_RGB + Text item=Temp_Sensor + Switch item=Light_Switch + } +} diff --git a/bundles/org.openhab.binding.sbus/pom.xml b/bundles/org.openhab.binding.sbus/pom.xml new file mode 100644 index 00000000000..35b19d9cc75 --- /dev/null +++ b/bundles/org.openhab.binding.sbus/pom.xml @@ -0,0 +1,46 @@ + + + + 4.0.0 + + + org.openhab.addons.bundles + org.openhab.addons.reactor.bundles + 5.0.0-SNAPSHOT + + + org.openhab.binding.sbus + + openHAB Add-ons :: Bundles :: Sbus Binding + + + + ro.ciprianpascu + j2sbus + 1.5.4 + compile + + + + + + + org.apache.felix + maven-bundle-plugin + 6.0.0 + + + + ro.ciprianpascu.sbus.facade, + * + + j2sbus;scope=compile + + + + + + + + diff --git a/bundles/org.openhab.binding.sbus/src/main/feature/feature.xml b/bundles/org.openhab.binding.sbus/src/main/feature/feature.xml new file mode 100644 index 00000000000..d767c571389 --- /dev/null +++ b/bundles/org.openhab.binding.sbus/src/main/feature/feature.xml @@ -0,0 +1,9 @@ + + + mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features + + + openhab-runtime-base + mvn:org.openhab.addons.bundles/org.openhab.binding.sbus/${project.version} + + diff --git a/bundles/org.openhab.binding.sbus/src/main/java/org/openhab/binding/sbus/BindingConstants.java b/bundles/org.openhab.binding.sbus/src/main/java/org/openhab/binding/sbus/BindingConstants.java new file mode 100644 index 00000000000..feecdb7185f --- /dev/null +++ b/bundles/org.openhab.binding.sbus/src/main/java/org/openhab/binding/sbus/BindingConstants.java @@ -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; + +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"; +} diff --git a/bundles/org.openhab.binding.sbus/src/main/java/org/openhab/binding/sbus/handler/AbstractSbusHandler.java b/bundles/org.openhab.binding.sbus/src/main/java/org/openhab/binding/sbus/handler/AbstractSbusHandler.java new file mode 100644 index 00000000000..a4cb6ae8fd3 --- /dev/null +++ b/bundles/org.openhab.binding.sbus/src/main/java/org/openhab/binding/sbus/handler/AbstractSbusHandler.java @@ -0,0 +1,151 @@ +/** + * 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.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.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; + + public AbstractSbusHandler(Thing thing) { + super(thing); + } + + @Override + public final void initialize() { + logger.debug("Initializing Sbus handler for thing {}", getThing().getUID()); + + initializeChannels(); + + Bridge bridge = getBridge(); + if (bridge == null) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "No bridge configured"); + return; + } + + SbusBridgeHandler bridgeHandler = (SbusBridgeHandler) bridge.getHandler(); + if (bridgeHandler == null || bridgeHandler.getThing().getStatus() != ThingStatus.ONLINE) { + updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.BRIDGE_OFFLINE, "Bridge is not online"); + return; + } + + sbusAdapter = bridgeHandler.getSbusConnection(); + if (sbusAdapter == null) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED, + "Bridge connection not initialized"); + 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) { + logger.error("Error polling Sbus device", 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(); + } +} diff --git a/bundles/org.openhab.binding.sbus/src/main/java/org/openhab/binding/sbus/handler/SbusBridgeHandler.java b/bundles/org.openhab.binding.sbus/src/main/java/org/openhab/binding/sbus/handler/SbusBridgeHandler.java new file mode 100644 index 00000000000..803314dd387 --- /dev/null +++ b/bundles/org.openhab.binding.sbus/src/main/java/org/openhab/binding/sbus/handler/SbusBridgeHandler.java @@ -0,0 +1,104 @@ +/** + * 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.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.service.component.annotations.Reference; +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); + + @Reference + private @Nullable SbusService sbusService; + + /** + * Constructs a new SbusBridgeHandler. + * + * @param bridge the bridge + */ + public SbusBridgeHandler(Bridge bridge) { + super(bridge); + } + + /** + * 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()) { + updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.CONFIGURATION_ERROR, "Host address not configured"); + return; + } + try { + // Initialize Sbus service with the configuration parameters + final SbusService service = sbusService; + if (service == null) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.HANDLER_INITIALIZING_ERROR, + "Sbus service not available"); + 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 + } +} diff --git a/bundles/org.openhab.binding.sbus/src/main/java/org/openhab/binding/sbus/handler/SbusHandlerFactory.java b/bundles/org.openhab.binding.sbus/src/main/java/org/openhab/binding/sbus/handler/SbusHandlerFactory.java new file mode 100644 index 00000000000..8ec3a6ca2c2 --- /dev/null +++ b/bundles/org.openhab.binding.sbus/src/main/java/org/openhab/binding/sbus/handler/SbusHandlerFactory.java @@ -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.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.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.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 static final Set 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(); + + if (thingTypeUID.equals(THING_TYPE_UDP_BRIDGE)) { + logger.debug("Creating Sbus UDP bridge handler for thing {}", thing.getUID()); + return new SbusBridgeHandler((Bridge) thing); + } + + if (thingTypeUID.equals(THING_TYPE_SWITCH)) { + logger.debug("Creating Sbus switch handler for thing {}", thing.getUID()); + return new SbusSwitchHandler(thing); + } else if (thingTypeUID.equals(THING_TYPE_TEMPERATURE)) { + logger.debug("Creating Sbus temperature handler for thing {}", thing.getUID()); + return new SbusTemperatureHandler(thing); + } else if (thingTypeUID.equals(THING_TYPE_RGBW)) { + logger.debug("Creating Sbus RGBW handler for thing {}", thing.getUID()); + return new SbusRgbwHandler(thing); + } + + logger.debug("Unknown thing type: {}", thingTypeUID); + return null; + } +} diff --git a/bundles/org.openhab.binding.sbus/src/main/java/org/openhab/binding/sbus/handler/SbusRgbwHandler.java b/bundles/org.openhab.binding.sbus/src/main/java/org/openhab/binding/sbus/handler/SbusRgbwHandler.java new file mode 100644 index 00000000000..8b4abadb750 --- /dev/null +++ b/bundles/org.openhab.binding.sbus/src/main/java/org/openhab/binding/sbus/handler/SbusRgbwHandler.java @@ -0,0 +1,246 @@ +/** + * 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.SbusChannelConfig; +import org.openhab.binding.sbus.handler.config.SbusDeviceConfig; +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.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.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) { + super(thing); + } + + /** + * Converts an openHAB HSBType into an RGBW array ([R, G, B, W]), + * with each channel in [0..255]. + * + * We extract 'white' by taking the minimum of R, G, B and + * subtracting it from each color channel. + * + * @param hsbType the openHAB HSBType (hue [0..360], sat [0..100], bri [0..100]) + * @return an int array [R, G, B, W] each in [0..255] + */ + private int[] hsbToRgbw(HSBType hsbType) { + // Convert HSBType to standard RGB [0..255] + PercentType[] rgb = ColorUtil.hsbToRgbPercent(hsbType); + // Convert each channel from 0..100 to 0..255 + int r = (int) Math.round(rgb[0].floatValue() * 2.55); + int g = (int) Math.round(rgb[1].floatValue() * 2.55); + int b = (int) Math.round(rgb[2].floatValue() * 2.55); + + // Determine the white component as the min of R, G, B + int w = Math.min(r, Math.min(g, b)); + + // Subtract W from each + r -= w; + g -= w; + b -= w; + + return new int[] { r, g, b, w }; + } + + /** + * Converts an RGBW array ([R, G, B, W]) back to an openHAB HSBType. + * + * We add the W channel back into R, G, and B, then clamp to [0..255]. + * Finally, we create an HSBType via fromRGB(). + * + * @param rgbw an int array [R, G, B, W] each in [0..255] + * @return an HSBType (hue [0..360], saturation/brightness [0..100]) + */ + private HSBType rgbwToHsb(int[] rgbw) { + if (rgbw.length < 4) { + throw new IllegalArgumentException("rgbw must have 4 elements: [R, G, B, W]."); + } + + int r = rgbw[0]; + int g = rgbw[1]; + int b = rgbw[2]; + int w = rgbw[3]; + + // Restore the combined R, G, B + int rTotal = r + w; + int gTotal = g + w; + int bTotal = b + w; + + // Clamp to [0..255] + rTotal = Math.min(255, Math.max(0, rTotal)); + gTotal = Math.min(255, Math.max(0, gTotal)); + bTotal = Math.min(255, Math.max(0, bTotal)); + + // Convert back to an HSBType via fromRGB + HSBType hsbType = HSBType.fromRGB(rTotal, gTotal, bTotal); + + return hsbType; + } + + /** + * 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) { + logger.warn("Channel {} has no channel type", channel.getUID()); + continue; + } + String channelTypeId = channelTypeUID.getId(); + if ("color-channel".equals(channelTypeId)) { + if (channelConfig.channelNumber <= 0) { + logger.warn("Channel {} has invalid channel number configuration", channel.getUID()); + } + } + if ("switch-channel".equals(channelTypeId)) { + switchChannelCount++; + if (channelConfig.channelNumber <= 0) { + logger.warn("Channel {} has invalid channel number configuration", channel.getUID()); + } + } + } + if (switchChannelCount > 1) { + logger.error("Only one switch channel is allowed for RGBW thing {}", getThing().getUID()); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "Only one switch channel is allowed"); + return; + } + } + + @Override + protected void pollDevice() { + final SbusService adapter = super.sbusAdapter; + if (adapter == null) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Sbus adapter not initialized"); + return; + } + + try { + SbusDeviceConfig config = getConfigAs(SbusDeviceConfig.class); + + // Update all channels + for (Channel channel : getThing().getChannels()) { + var channelTypeUID = channel.getChannelTypeUID(); + if (channelTypeUID == null) { + logger.warn("Channel {} has no channel type", channel.getUID()); + continue; + } + String channelTypeId = channelTypeUID.getId(); + SbusChannelConfig channelConfig = channel.getConfiguration().as(SbusChannelConfig.class); + + if ("color-channel".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 = rgbwToHsb(rgbwValues); + updateState(channel.getUID(), hsbType); + } + } else if ("switch-channel".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) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Error reading device state"); + } + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + final SbusService adapter = super.sbusAdapter; + if (adapter == null) { + logger.warn("Sbus adapter not initialized"); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Sbus adapter not initialized"); + return; + } + + try { + Channel channel = getThing().getChannel(channelUID.getId()); + if (channel != null) { + var channelTypeUID = channel.getChannelTypeUID(); + if (channelTypeUID == null) { + logger.warn("Channel {} has no channel type", channel.getUID()); + return; + } + String channelTypeId = channelTypeUID.getId(); + SbusDeviceConfig config = getConfigAs(SbusDeviceConfig.class); + SbusChannelConfig channelConfig = channel.getConfiguration().as(SbusChannelConfig.class); + + if ("color-channel".equals(channelTypeId) && command instanceof HSBType hsbCommand) { + // Handle color command + int[] rgbw = hsbToRgbw(hsbCommand); + adapter.writeRgbw(config.subnetId, config.id, channelConfig.channelNumber, rgbw[0], rgbw[1], + rgbw[2], rgbw[3]); + updateState(channelUID, hsbCommand); + } else if ("switch-channel".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, isOn ? OnOffType.ON : OnOffType.OFF); + } + } + } catch (Exception e) { + logger.error("Error handling command", e); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Error sending command to device"); + } + } +} diff --git a/bundles/org.openhab.binding.sbus/src/main/java/org/openhab/binding/sbus/handler/SbusService.java b/bundles/org.openhab.binding.sbus/src/main/java/org/openhab/binding/sbus/handler/SbusService.java new file mode 100644 index 00000000000..f5fb14de52b --- /dev/null +++ b/bundles/org.openhab.binding.sbus/src/main/java/org/openhab/binding/sbus/handler/SbusService.java @@ -0,0 +1,88 @@ +/** + * 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 + * @return array of temperature values in Celsius + * @throws Exception if reading fails + */ + float[] readTemperatures(int subnetId, int id) 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 r red value (0-255) + * @param g green value (0-255) + * @param b blue value (0-255) + * @param w white value (0-255) + * @throws Exception if writing fails + */ + void writeRgbw(int subnetId, int id, int channelNumber, int r, int g, int b, int w) 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; +} diff --git a/bundles/org.openhab.binding.sbus/src/main/java/org/openhab/binding/sbus/handler/SbusSwitchHandler.java b/bundles/org.openhab.binding.sbus/src/main/java/org/openhab/binding/sbus/handler/SbusSwitchHandler.java new file mode 100644 index 00000000000..d4dc4285335 --- /dev/null +++ b/bundles/org.openhab.binding.sbus/src/main/java/org/openhab/binding/sbus/handler/SbusSwitchHandler.java @@ -0,0 +1,167 @@ +/** + * 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.SbusChannelConfig; +import org.openhab.binding.sbus.handler.config.SbusDeviceConfig; +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.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) { + super(thing); + } + + @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) { + logger.warn("Channel {} has invalid channel number configuration", channel.getUID()); + } + } + } + + @Override + protected void pollDevice() { + final SbusService adapter = super.sbusAdapter; + if (adapter == null) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Sbus adapter not initialized"); + 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()) { + SbusChannelConfig channelConfig = channel.getConfiguration().as(SbusChannelConfig.class); + if (channelConfig.channelNumber > 0 && channelConfig.channelNumber <= statuses.length) { + var channelTypeUID = channel.getChannelTypeUID(); + if (channelTypeUID == null) { + logger.warn("Channel {} has no channel type", channel.getUID()); + continue; + } + String channelTypeId = channelTypeUID.getId(); + // 100 when on, something else when off + boolean isActive = statuses[channelConfig.channelNumber - 1] == 0x64; + + if ("switch-channel".equals(channelTypeId)) { + updateState(channel.getUID(), isActive ? OnOffType.ON : OnOffType.OFF); + } else if ("dimmer-channel".equals(channelTypeId)) { + updateState(channel.getUID(), new PercentType(statuses[channelConfig.channelNumber - 1])); + } else if ("paired-channel".equals(channelTypeId)) { + updateState(channel.getUID(), isActive ? OpenClosedType.OPEN : OpenClosedType.CLOSED); + } + } + } + } catch (Exception e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Error reading device state"); + } + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + final SbusService adapter = super.sbusAdapter; + if (adapter == null) { + logger.warn("Sbus adapter not initialized"); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Sbus adapter not initialized"); + return; + } + + try { + Channel channel = getThing().getChannel(channelUID); + if (channel != null) { + SbusChannelConfig channelConfig = channel.getConfiguration().as(SbusChannelConfig.class); + if (channelConfig.channelNumber <= 0) { + logger.warn("Invalid channel number for {}", channelUID); + return; + } + + SbusDeviceConfig config = getConfigAs(SbusDeviceConfig.class); + + if (command instanceof OnOffType) { + handleOnOffCommand((OnOffType) command, config, channelConfig, channelUID, adapter); + } else if (command instanceof PercentType) { + handlePercentCommand((PercentType) command, config, channelConfig, channelUID, adapter); + } else if (command instanceof OpenClosedType) { + handleOpenClosedCommand((OpenClosedType) command, config, channelConfig, channelUID, adapter); + } + } + } catch (Exception e) { + logger.error("Error handling command", 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, isOn ? OnOffType.ON : OnOffType.OFF); + } + + 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; + } +} diff --git a/bundles/org.openhab.binding.sbus/src/main/java/org/openhab/binding/sbus/handler/SbusTemperatureHandler.java b/bundles/org.openhab.binding.sbus/src/main/java/org/openhab/binding/sbus/handler/SbusTemperatureHandler.java new file mode 100644 index 00000000000..4abcfbb3d08 --- /dev/null +++ b/bundles/org.openhab.binding.sbus/src/main/java/org/openhab/binding/sbus/handler/SbusTemperatureHandler.java @@ -0,0 +1,100 @@ +/** + * 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.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.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) { + super(thing); + } + + @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()) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "Invalid channel configuration: " + channel.getUID()); + return; + } + } + } + + @Override + protected void pollDevice() { + final SbusService adapter = super.sbusAdapter; + if (adapter == null) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Sbus adapter not initialized"); + return; + } + + try { + SbusDeviceConfig config = getConfigAs(SbusDeviceConfig.class); + + // Read temperatures in Celsius from device + float[] temperatures = adapter.readTemperatures(config.subnetId, config.id); + + // Iterate over all channels and update their states with corresponding temperatures + for (Channel channel : getThing().getChannels()) { + 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) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Error reading device state"); + } + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + // Temperature sensors are read-only + logger.debug("Temperature device is read-only, ignoring command"); + } +} diff --git a/bundles/org.openhab.binding.sbus/src/main/java/org/openhab/binding/sbus/handler/config/SbusBridgeConfig.java b/bundles/org.openhab.binding.sbus/src/main/java/org/openhab/binding/sbus/handler/config/SbusBridgeConfig.java new file mode 100644 index 00000000000..f066a066676 --- /dev/null +++ b/bundles/org.openhab.binding.sbus/src/main/java/org/openhab/binding/sbus/handler/config/SbusBridgeConfig.java @@ -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; +} diff --git a/bundles/org.openhab.binding.sbus/src/main/java/org/openhab/binding/sbus/handler/config/SbusChannelConfig.java b/bundles/org.openhab.binding.sbus/src/main/java/org/openhab/binding/sbus/handler/config/SbusChannelConfig.java new file mode 100644 index 00000000000..c1ba76471a6 --- /dev/null +++ b/bundles/org.openhab.binding.sbus/src/main/java/org/openhab/binding/sbus/handler/config/SbusChannelConfig.java @@ -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; +} diff --git a/bundles/org.openhab.binding.sbus/src/main/java/org/openhab/binding/sbus/handler/config/SbusDeviceConfig.java b/bundles/org.openhab.binding.sbus/src/main/java/org/openhab/binding/sbus/handler/config/SbusDeviceConfig.java new file mode 100644 index 00000000000..f5d5c1d4ff2 --- /dev/null +++ b/bundles/org.openhab.binding.sbus/src/main/java/org/openhab/binding/sbus/handler/config/SbusDeviceConfig.java @@ -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 +} diff --git a/bundles/org.openhab.binding.sbus/src/main/java/org/openhab/binding/sbus/handler/config/TemperatureChannelConfig.java b/bundles/org.openhab.binding.sbus/src/main/java/org/openhab/binding/sbus/handler/config/TemperatureChannelConfig.java new file mode 100644 index 00000000000..6fc01bae02c --- /dev/null +++ b/bundles/org.openhab.binding.sbus/src/main/java/org/openhab/binding/sbus/handler/config/TemperatureChannelConfig.java @@ -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); + } +} diff --git a/bundles/org.openhab.binding.sbus/src/main/java/org/openhab/binding/sbus/internal/SbusServiceImpl.java b/bundles/org.openhab.binding.sbus/src/main/java/org/openhab/binding/sbus/internal/SbusServiceImpl.java new file mode 100644 index 00000000000..824e4a9cbdd --- /dev/null +++ b/bundles/org.openhab.binding.sbus/src/main/java/org/openhab/binding/sbus/internal/SbusServiceImpl.java @@ -0,0 +1,108 @@ +/** + * 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.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 + */ + 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) throws Exception { + final SbusAdapter adapter = this.adapter; + if (adapter == null) { + throw new IllegalStateException("SbusAdapter not initialized"); + } + return adapter.readTemperatures(subnetId, id); + } + + @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 r, int g, int b, int w) throws Exception { + final SbusAdapter adapter = this.adapter; + if (adapter == null) { + throw new IllegalStateException("SbusAdapter not initialized"); + } + adapter.writeRgbw(subnetId, id, channelNumber, r, g, b, w); + } + + @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; + } + } +} diff --git a/bundles/org.openhab.binding.sbus/src/main/resources/OH-INF/addon/addon.xml b/bundles/org.openhab.binding.sbus/src/main/resources/OH-INF/addon/addon.xml new file mode 100644 index 00000000000..306e7145677 --- /dev/null +++ b/bundles/org.openhab.binding.sbus/src/main/resources/OH-INF/addon/addon.xml @@ -0,0 +1,10 @@ + + + binding + Sbus Binding + Binding for Sbus + local + + diff --git a/bundles/org.openhab.binding.sbus/src/main/resources/OH-INF/config/gainOffset.xml b/bundles/org.openhab.binding.sbus/src/main/resources/OH-INF/config/gainOffset.xml new file mode 100644 index 00000000000..341a6ee38cc --- /dev/null +++ b/bundles/org.openhab.binding.sbus/src/main/resources/OH-INF/config/gainOffset.xml @@ -0,0 +1,21 @@ + + + + + + + Offset to add to raw value towards the item (before the gain). The negative + offset will be applied in the + reverse direction (before inverting the gain). If omitted, zero offset is used. + + + + Gain to apply to the state towards the item. One can also specify the unit to declare resulting unit. + This is used as divisor for values in the reverse direction. If omitted, gain of 1 is used. + + + diff --git a/bundles/org.openhab.binding.sbus/src/main/resources/OH-INF/i18n/sbus.properties b/bundles/org.openhab.binding.sbus/src/main/resources/OH-INF/i18n/sbus.properties new file mode 100644 index 00000000000..923c4a4f2eb --- /dev/null +++ b/bundles/org.openhab.binding.sbus/src/main/resources/OH-INF/i18n/sbus.properties @@ -0,0 +1,95 @@ +# 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 Slave +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 0 and 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 0 and 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 0 and 255. +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.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 +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 + +# 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 diff --git a/bundles/org.openhab.binding.sbus/src/main/resources/OH-INF/thing/bridge-udp.xml b/bundles/org.openhab.binding.sbus/src/main/resources/OH-INF/thing/bridge-udp.xml new file mode 100644 index 00000000000..d0c6bbe2035 --- /dev/null +++ b/bundles/org.openhab.binding.sbus/src/main/resources/OH-INF/thing/bridge-udp.xml @@ -0,0 +1,78 @@ + + + + + Endpoint for Sbus UDP slaves + + + + Network address of the device + localhost + network-address + + + + Port of the slave + 6000 + + + + + When enabled we try to find a device specific handler. Turn this on if you're using one of the + supported devices. + false + + + + + Use RTU Encoding over IP + false + + + + + + How long to delay we must have at minimum between two consecutive Sbus transactions. In milliseconds. + + 60 + + + + How long to wait to before trying to establish a new connection after the previous one has been + disconnected. In milliseconds. + 0 + true + + + + How many times we try to establish the connection. Should be at least 1. + 1 + true + + + + 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. + 0 + true + + + + 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. + 0 + true + + + + The maximum time that is waited when establishing the connection. Value of zero means that system/OS + default is respected. In milliseconds. + 10000 + true + + + + diff --git a/bundles/org.openhab.binding.sbus/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.sbus/src/main/resources/OH-INF/thing/thing-types.xml new file mode 100644 index 00000000000..8a9232b17da --- /dev/null +++ b/bundles/org.openhab.binding.sbus/src/main/resources/OH-INF/thing/thing-types.xml @@ -0,0 +1,185 @@ + + + + + + + + + + Sbus switch device + + + + Slave subnet id. Can take any value between 1 and 255. 255 for broadcast. + 1 + + + + + + + + The ID of the Sbus device + + + + Refresh interval in seconds + 30 + s + + + + + + + + + + + Sbus temperature sensor device + + + + Slave subnet id. Can take any value between 1 and 255. 255 for broadcast. + 1 + + + + + + + + The ID of the Sbus device + + + + Refresh interval in seconds + 30 + s + + + + + + + + + + + Sbus RGBW lighting controller + + + + Slave subnet id. Can take any value between 1 and 255. 255 for broadcast. + 1 + + + + + + + + The ID of the Sbus device + + + + Refresh interval in seconds + 30 + s + + + + + + + Switch + + Switch state (ON/OFF) + Switch + + + + The physical channel number on the Sbus device + + + + + + Dimmer + + Dimmer state (0-100%) + DimmableLight + + + + The physical channel number on the Sbus device + + + + Timer in seconds to automatically turn off the switch (0 = disabled) + 0 + true + s + + + + + + Contact + + Paired channel state (OPEN/CLOSED) - controls two opposite channels + Contact + + + + The physical channel number on the Sbus device + + + + The physical channel number of the paired channel (will be set to opposite state) + + + + + + Number:Temperature + + Temperature reading from the device + Temperature + + + + + The physical channel number on the Sbus device + + + + The unit to use for temperature readings (°C or °F) + + + + + CELSIUS + + + + + + Color + + Color control + ColorLight + + + + The physical channel number on the Sbus device + + + + + diff --git a/bundles/pom.xml b/bundles/pom.xml index 6fed251e593..e76c56e4533 100644 --- a/bundles/pom.xml +++ b/bundles/pom.xml @@ -364,6 +364,7 @@ org.openhab.binding.salus org.openhab.binding.samsungtv org.openhab.binding.satel + org.openhab.binding.sbus org.openhab.binding.semsportal org.openhab.binding.senechome org.openhab.binding.seneye diff --git a/features/openhab-addons/pom.xml b/features/openhab-addons/pom.xml index dbf79cf9d16..98a178cf04d 100644 --- a/features/openhab-addons/pom.xml +++ b/features/openhab-addons/pom.xml @@ -48,6 +48,7 @@ + @@ -73,6 +74,7 @@ openhab-binding-bluetooth openhab-binding-modbus + openhab-binding-sbus openhab-binding-mqtt diff --git a/features/openhab-addons/src/main/resources/footer.xml b/features/openhab-addons/src/main/resources/footer.xml index 8867b5f5423..aec27b7826f 100644 --- a/features/openhab-addons/src/main/resources/footer.xml +++ b/features/openhab-addons/src/main/resources/footer.xml @@ -48,5 +48,4 @@ mvn:org.openhab.addons.bundles/org.openhab.binding.modbus.sungrow/${project.version} mvn:org.openhab.addons.bundles/org.openhab.binding.modbus.sunspec/${project.version} -