[somfycul] Initial contribution (#19207)

* initial somfycul binding creation (openhab#15974)
init codebase from @weisserd

Signed-off-by: Marc Klasser <shox06@gmx.de>
This commit is contained in:
Nookyx
2025-11-27 18:46:11 +01:00
committed by GitHub
parent 0e1b4481a5
commit b308947d39
16 changed files with 982 additions and 0 deletions
+1
View File
@@ -377,6 +377,7 @@
/bundles/org.openhab.binding.solarmax/ @jamietownsend
/bundles/org.openhab.binding.solarwatt/ @sven-carstens
/bundles/org.openhab.binding.solax/ @theater
/bundles/org.openhab.binding.somfycul/ @Nookyx
/bundles/org.openhab.binding.somfymylink/ @loungeflyz
/bundles/org.openhab.binding.somfytahoma/ @octa22
/bundles/org.openhab.binding.somneo/ @0x4d4d
+5
View File
@@ -1861,6 +1861,11 @@
<artifactId>org.openhab.binding.solax</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.binding.somfycul</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.binding.somfymylink</artifactId>
@@ -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
@@ -0,0 +1,110 @@
# SomfyCUL Binding
This binding supports controlling [Somfy RTS Rollershutters](https://www.somfysystems.com/en-us/products/rolling-shutters) via CUL sticks.
## Supported Things
Currently these things are supported:
- `cul-device`: The CUL stick bridge used to send the actual signals to your rollershutters
- `somfy-device`: The rollershutters (UP, DOWN, MY/STOP control of a rollershutter)
## Discovery
There is no auto discovery
## Thing Configuration
### `cul-device` Thing Configuration
| Parameter | Type | Description | Default | Required | Advanced |
| -------------------- | ------ | --------------------------------------------------------------------- | ------------ | -------- | -------- |
| port | String | The serial port (COM1, /dev/ttyS0, ...) your CUL stick is attached to | /dev/ttyUSB0 | yes | no |
| baudrate | int | The serial port baud rate | 38400 | yes | no |
### `somfy-device` Thing Configuration
There is no thing configuration for the `somfy-device` things.
## Channels
| Channel | Type | Description |
| -------- | ------------- | ---------------------------------- |
| position | Rollershutter | Device control (UP, DOWN, MY/STOP) |
| program | Switch | Device program button (pairing) |
## Full Example
Things and Items can be configured in the UI, or by using a `things` or `items` file like in the following example:
### Thing Configuration
somfy.things:
```java
// the CUL stick
somfycul:cul-device:cul [ port="/dev/ttyUSB0", baudrate="38400" ]
/** optionally, define a custom port (e.g. /dev/ttyNanoCUL) as a udev rule, for example
* file: /etc/udev/rules.d/20_nanoCUL.rules
* content: SUBSYSTEM=="tty", ATTRS{idVendor}=="0403", ATTRS{idProduct}=="6001", ATTRS{serial}=="433", SYMLINK+="ttyNanoCUL"
*
* change idVendor, idProduct and idSerial depending on your CUL stick.
* you can inspect the values of your CUL stick like this:
* $ udevadm info -a -p $(udevadm info -q path -n /dev/ttyUSB0)
*/
// the rollershutters
somfycul:somfy-device:livingroom_shutter_left (somfycul:cul-device:cul)
somfycul:somfy-device:livingroom_shutter_right (somfycul:cul-device:cul)
somfycul:somfy-device:bedroom_shutter (somfycul:cul-device:cul)
```
### Item Configuration
somfy.items:
```java
// you only need the switch items to for pairing/copy from your existing RTS remote and should remove/comment them later on
Switch Shutter_Livingroom_Left "Shutter Livingroom left" {channel="somfycul:somfy-device:livingroom_shutter_left:program"}
Switch Shutter_Livingroom_Right "Shutter Livingroom right" {channel="somfycul:somfy-device:livingroom_shutter_right:program"}
Switch Shutter_Bedroom "Shutter Bedroom" {channel="somfycul:somfy-device:bedroom_shutter:program"}
// these will be your rollershutters with UP, DOWN, MY/STOP commands
Rollershutter Shutter_Livingroom_Left "Shutter Livingroom left" {channel="somfycul:somfy-device:livingroom_shutter_left:position"}
Rollershutter Shutter_Livingroom_Right "Shutter Livingroom right" {channel="somfycul:somfy-device:livingroom_shutter_right:position"}
Rollershutter Shutter_Bedroom "Shutter Bedroom" {channel="somfycul:somfy-device:bedroom_shutter:position"}
```
### Sitemap Configuration
```perl
sitemap home label="Home" {
Frame label="Ground Floor" icon="groundfloor" {
Text label="Livingroom" icon="livingroom" {
Default item=Shutter_Livingroom_Left
Default item=Shutter_Livingroom_Right
}
}
Frame label="First Floor" icon="firstfloor" {
Text label="Bedroom" icon="bedroom" {
Default item=Shutter_Bedroom
}
}
}
```
### Clone Somfy RTS Remote to `somfy-device`
To clone an existing Somfy RTS Remote as a `somfy-device`, have your Somfy RTS remote ready and properly programmed to control your rollershutter. You'll need to have your `somfy-device` `items` set to `Switch` type and use the `program` channel.
Use the manufacturers procedure to _copy_ a remote to another remote. Usually this means selecting the channel you wish to copy on your Somfy RTS remote, pressing and holding the `program` or `PROG` button for 2-3 seconds until the rollershutter briefly moves up/down to confirm it is about to be programmed.
Use your `somfy-device` `Switch` `item` to _press_ `program` shortly after to copy the remote channel to this `somfy-device`.
You can now set your `somfy-device` `items` back to `Rollershutter` item type.
## Props
Shoutout to [@weisserd](https://github.com/weisserd) for his initial creation of this binding and allowing me to use his codebase to make it an official openHAB binding.
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.addons.reactor.bundles</artifactId>
<version>5.1.0-SNAPSHOT</version>
</parent>
<artifactId>org.openhab.binding.somfycul</artifactId>
<name>openHAB Add-ons :: Bundles :: SomfyCUL Binding</name>
</project>
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<features name="org.openhab.binding.somfycul-${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-somfycul" description="SomfyCUL Binding" version="${project.version}">
<feature>openhab-runtime-base</feature>
<feature>openhab-transport-serial</feature>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.somfycul/${project.version}</bundle>
</feature>
</features>
@@ -0,0 +1,27 @@
/*
* Copyright (c) 2010-2025 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.somfycul.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Configuration of a CUL stick
*
* @author Marc Klasser - Initial contribution
*/
@NonNullByDefault
public class CULConfiguration {
public String port = "";
public int baudrate = 0;
}
@@ -0,0 +1,237 @@
/*
* 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.somfycul.internal;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.stream.Collectors;
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.io.transport.serial.PortInUseException;
import org.openhab.core.io.transport.serial.SerialPort;
import org.openhab.core.io.transport.serial.SerialPortIdentifier;
import org.openhab.core.io.transport.serial.SerialPortManager;
import org.openhab.core.io.transport.serial.UnsupportedCommOperationException;
import org.openhab.core.thing.Bridge;
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.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 CULHandler} is responsible for handling commands, which are
* sent via the CUL stick.
*
* @author Marc Klasser - Initial contribution
*
*/
@NonNullByDefault
public class CULHandler extends BaseBridgeHandler {
private final Logger logger = LoggerFactory.getLogger(CULHandler.class);
private static final int COMMAND_DELAY_MS = 100;
private long lastCommandTime = 0;
private final SerialPortManager serialPortManager;
private final Bundle bundle;
private final LocaleProvider localeProvider;
private final TranslationProvider i18nProvider;
private @Nullable SerialPortIdentifier portId;
private @Nullable SerialPort serialPort;
private @Nullable OutputStream outputStream;
private @Nullable InputStream inputStream;
public CULHandler(Bridge bridge, SerialPortManager serialPortManager, LocaleProvider localeProvider,
TranslationProvider i18nProvider) {
super(bridge);
this.serialPortManager = serialPortManager;
this.localeProvider = localeProvider;
this.i18nProvider = i18nProvider;
this.bundle = FrameworkUtil.getBundle(CULHandler.class);
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
// the bridge does not have any channels
}
/**
* Executes the given {@link SomfyCommand} for the given {@link Thing} (RTS Device).
*
* @param somfyDevice the RTS Device which is the receiver of the command.
* @param somfyCommand the command to execute
* @param rollingCode the current rolling code for the device
* @param address the device address
* @return true if the command was successfully transmitted to the CUL device, false otherwise
*/
public boolean executeCULCommand(Thing somfyDevice, SomfyCommand somfyCommand, String rollingCode, String address) {
// culCommand syntax (basically the serial data payload): Ys + EncryptionKey=A1 + Command + 0 + RollingCode +
// Address
String culCommand = "YsA1" + somfyCommand.getActionKey() + "0" + rollingCode + address;
logger.debug("Send message {} for thing {}", culCommand, somfyDevice.getLabel());
return writeString(culCommand);
}
/**
* Sends a string to the serial port of this device.
* The writing of the msg is executed synchronized, so it's guaranteed that the device doesn't get
* multiple messages concurrently.
*
* @param msg the string to send
* @return true, if the message has been transmitted successfully, otherwise false.
*/
protected synchronized boolean writeString(final String msg) {
final SerialPortIdentifier localPortId = portId;
final OutputStream localOutputStream = outputStream;
if (localPortId == null || localOutputStream == null) {
logger.warn("Cannot write to serial port - port or stream not initialized");
return false;
}
logger.debug("Trying to write '{}' to serial port {}", msg, localPortId.getName());
final long earliestNextExecution = lastCommandTime + COMMAND_DELAY_MS;
while (earliestNextExecution > System.currentTimeMillis()) {
try {
Thread.sleep(COMMAND_DELAY_MS);
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
return false;
}
}
try {
localOutputStream.write((msg + "\n").getBytes());
localOutputStream.flush();
lastCommandTime = System.currentTimeMillis();
return true;
} catch (IOException e) {
logger.warn("Error writing '{}' to serial port {}: {}", msg, localPortId.getName(), e.getMessage());
}
return false;
}
@Override
public void initialize() {
CULConfiguration config = getConfigAs(CULConfiguration.class);
if (!validConfiguration(config)) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"@text/offline.config-error-missing");
return;
}
String port = config.port;
SerialPortIdentifier localPortId = serialPortManager.getIdentifier(port);
if (localPortId == null) {
String availablePorts = serialPortManager.getIdentifiers().map(id -> id.getName())
.collect(Collectors.joining(System.lineSeparator()));
String description = i18nProvider.getText(bundle, "offline.config-error-port-not-found",
"Serial port {0} could not be found. Available ports are:\n{1}", localeProvider.getLocale(), port,
availablePorts);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, description);
return;
}
portId = localPortId;
logger.debug("got port: {}", config.port);
try {
SerialPort localSerialPort = localPortId.open("openHAB", 2000);
// set port parameters
int baudRate = config.baudrate;
localSerialPort.setSerialPortParams(baudRate, SerialPort.DATABITS_8, SerialPort.STOPBITS_1,
SerialPort.PARITY_NONE);
InputStream localInputStream = localSerialPort.getInputStream();
OutputStream localOutputStream = localSerialPort.getOutputStream();
// Only set instance variables after successful initialization
serialPort = localSerialPort;
inputStream = localInputStream;
outputStream = localOutputStream;
updateStatus(ThingStatus.ONLINE);
} catch (IOException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, i18nProvider.getText(bundle,
"offline.comm-error-io", "IO Error: {0}", localeProvider.getLocale(), e.getMessage()));
} catch (PortInUseException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, i18nProvider.getText(bundle,
"offline.comm-error-port-in-use", "Port already in use: {0}", localeProvider.getLocale(), port));
} catch (UnsupportedCommOperationException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
i18nProvider.getText(bundle, "offline.comm-error-unsupported-operation",
"Unsupported operation on port: {0}: {1}", localeProvider.getLocale(), port,
e.getMessage()));
}
}
private boolean validConfiguration(@Nullable CULConfiguration config) {
if (config == null) {
logger.debug("somfycul configuration missing");
return false;
}
if (config.port.isEmpty() || config.baudrate <= 0) {
logger.debug("somfycul port or baudrate not specified");
return false;
}
return true;
}
@Override
public void dispose() {
super.dispose();
// Store references to avoid NPE if they're nulled during disposal
final SerialPort localSerialPort = serialPort;
final OutputStream localOutputStream = outputStream;
final InputStream localInputStream = inputStream;
// Close resources in reverse order of acquisition
if (localOutputStream != null) {
try {
localOutputStream.close();
} catch (IOException e) {
logger.debug("Error while closing the output stream: {}", e.getMessage());
}
}
if (localInputStream != null) {
try {
localInputStream.close();
} catch (IOException e) {
logger.debug("Error while closing the input stream: {}", e.getMessage());
}
}
if (localSerialPort != null) {
localSerialPort.removeEventListener();
localSerialPort.close();
}
// Clear all references
outputStream = null;
inputStream = null;
serialPort = null;
portId = null;
}
}
@@ -0,0 +1,50 @@
/*
* 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.somfycul.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.ThingTypeUID;
/**
* The {@link SomfyCULBindingConstants} class defines common constants, which are
* used across the whole binding.
*
* @author Marc Klasser - Initial contribution
*/
@NonNullByDefault
public class SomfyCULBindingConstants {
private static final String BINDING_ID = "somfycul";
// List of all Thing Type UIDs
/**
* CUL stick
*/
public static final ThingTypeUID CUL_DEVICE_THING_TYPE = new ThingTypeUID(BINDING_ID, "cul-device");
/**
* Somfy RTS device (e.g. rollershutter)
*/
public static final ThingTypeUID SOMFY_DEVICE_THING_TYPE = new ThingTypeUID(BINDING_ID, "somfy-device");
// List of all Channel ids
/**
* Rollershutter's position
*/
public static final String POSITION = "position";
/**
* Rollershutter's program
*/
public static final String PROGRAM = "program";
}
@@ -0,0 +1,264 @@
/*
* 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.somfycul.internal;
import static org.openhab.binding.somfycul.internal.SomfyCULBindingConstants.*;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.util.Properties;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.OpenHAB;
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.StopMoveType;
import org.openhab.core.library.types.UpDownType;
import org.openhab.core.thing.Bridge;
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.ThingHandler;
import org.openhab.core.types.Command;
import org.openhab.core.types.State;
import org.osgi.framework.Bundle;
import org.osgi.framework.FrameworkUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link SomfyCULHandler} handles roller shutter commands.
*
* Properties are persisted in the user data folder per Thing UID.
* Initialization is async and repeated on enable/disable.
*
* @author Marc Klasser - Initial contribution
*/
@NonNullByDefault
public class SomfyCULHandler extends BaseThingHandler {
private final Logger logger = LoggerFactory.getLogger(SomfyCULHandler.class);
private final Bundle bundle;
private final LocaleProvider localeProvider;
private final TranslationProvider i18nProvider;
private @Nullable File propertyFile = null;
private @Nullable Properties properties = null;
/**
* Initializes the thing. As persistent state is necessary, the properties are stored in the user data directory and
* fetched within the initialization.
*
* @param thing the Thing instance to be handled
* @param localeProvider the provider for locale information
* @param i18nProvider the provider for internationalization/translation
*/
public SomfyCULHandler(Thing thing, LocaleProvider localeProvider, TranslationProvider i18nProvider) {
super(thing);
this.localeProvider = localeProvider;
this.i18nProvider = i18nProvider;
this.bundle = FrameworkUtil.getBundle(CULHandler.class);
}
/**
* The roller shutter is initialized and set to online by default, as there is no feedback that can check if the
* shutter is available, other than being able to read the properties file.
*/
@Override
public void initialize() {
updateStatus(ThingStatus.UNKNOWN);
scheduler.execute(() -> {
try {
String somfyFolderName = OpenHAB.getUserDataFolder() + File.separator + "somfycul";
File folder = new File(somfyFolderName);
if (!folder.exists() && !folder.mkdirs()) {
throw new IOException("Cannot create directory: " + folder.getAbsolutePath());
}
propertyFile = new File(somfyFolderName + File.separator
+ getThing().getUID().getAsString().replace(':', '_') + ".properties");
loadOrCreateProperties();
updateStatus(ThingStatus.ONLINE);
} catch (Exception e) {
logger.warn("Failed to initialize SomfyCULHandler for {}: {}", getThing().getUID(), e.getMessage());
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR,
i18nProvider.getText(bundle, "offline.init-error", "Initialization failed: {0}",
localeProvider.getLocale(), e.getMessage()));
}
});
}
private void loadOrCreateProperties() throws IOException {
File file = propertyFile;
if (file == null) {
throw new IOException("Property file not initialized");
}
Properties p = new Properties();
if (!file.exists()) {
File parent = file.getParentFile();
if (parent == null) {
throw new IOException("Cannot access parent directory for property files");
}
long newAddress = computeNewAddress(parent);
p.setProperty("rollingCode", "0000");
p.setProperty("address", String.format("%06X", newAddress));
try (FileWriter fw = new FileWriter(file)) {
p.store(fw, "Initialized fields");
}
} else {
try (FileReader fr = new FileReader(file)) {
p.load(fr);
}
}
this.properties = p;
}
private long computeNewAddress(File directory) throws IOException {
File[] files = directory.listFiles((d, name) -> name != null && name.endsWith(".properties"));
if (files == null) {
throw new IOException("Cannot list files in " + directory.getAbsolutePath());
}
long maxAddr = 0;
for (File f : files) {
if (f.equals(propertyFile)) {
continue;
}
Properties other = new Properties();
try (FileReader fr = new FileReader(f)) {
other.load(fr);
String addr = other.getProperty("address");
if (addr != null) {
try {
long val = Long.decode("0x" + addr);
maxAddr = Math.max(maxAddr, val);
} catch (NumberFormatException ignored) {
}
}
}
}
return maxAddr + 1;
}
@Override
public void thingUpdated(Thing thing) {
super.thingUpdated(thing);
this.propertyFile = null;
this.properties = null;
initialize();
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
Properties p = properties;
File file = propertyFile;
if (p == null || file == null) {
logger.warn("Ignoring command — properties not yet loaded");
return;
}
SomfyCommand somfyCommand = null;
switch (channelUID.getId()) {
case POSITION:
if (command instanceof UpDownType upDownCommand) {
switch (upDownCommand) {
case UP -> somfyCommand = SomfyCommand.UP;
case DOWN -> somfyCommand = SomfyCommand.DOWN;
}
} else if (command instanceof StopMoveType stopMoveCommand) {
if (stopMoveCommand == StopMoveType.STOP) {
somfyCommand = SomfyCommand.MY;
}
}
break;
case PROGRAM:
if (command instanceof OnOffType) {
// Don't check for on/off - always trigger program mode
somfyCommand = SomfyCommand.PROG;
}
break;
}
if (somfyCommand == null) {
return;
}
Bridge bridge = getBridge();
if (bridge == null) {
return;
}
ThingHandler handler = bridge.getHandler();
if (!(handler instanceof CULHandler cul)) {
return;
}
String rollingCode = p.getProperty("rollingCode");
String address = p.getProperty("address");
if (rollingCode == null || address == null) {
return;
}
final SomfyCommand finalCommand = somfyCommand;
boolean ok = cul.executeCULCommand(getThing(), somfyCommand, rollingCode, address);
if (!ok) {
return;
}
if (command instanceof State state) {
updateState(channelUID, state);
}
long newRolling = (Long.decode("0x" + rollingCode) + 1) & 0xFFFF;
String newStr = String.format("%04X", newRolling);
p.setProperty("rollingCode", newStr);
scheduler.execute(() -> {
try (FileWriter fw = new FileWriter(file)) {
p.store(fw, "Last command: " + finalCommand);
} catch (IOException e) {
logger.warn("Error writing property file: {}", e.getMessage());
}
});
}
@Override
public void dispose() {
properties = null;
propertyFile = null;
super.dispose();
}
}
@@ -0,0 +1,76 @@
/*
* 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.somfycul.internal;
import static org.openhab.binding.somfycul.internal.SomfyCULBindingConstants.*;
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.io.transport.serial.SerialPortManager;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerFactory;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
/**
* The {@link SomfyCULHandlerFactory} is responsible for creating things and thing
* handlers.
*
* @author Marc Klasser - Initial contribution
*/
@NonNullByDefault
@Component(configurationPid = "binding.somfycul", service = ThingHandlerFactory.class)
public class SomfyCULHandlerFactory extends BaseThingHandlerFactory {
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(CUL_DEVICE_THING_TYPE,
SOMFY_DEVICE_THING_TYPE);
private final SerialPortManager serialPortManager;
private final LocaleProvider localeProvider;
private final TranslationProvider i18nProvider;
@Activate
public SomfyCULHandlerFactory(final @Reference SerialPortManager serialPortManager,
final @Reference LocaleProvider localeProvider, final @Reference TranslationProvider i18nProvider) {
this.serialPortManager = serialPortManager;
this.localeProvider = localeProvider;
this.i18nProvider = i18nProvider;
}
@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(CUL_DEVICE_THING_TYPE) && thing instanceof Bridge bridge) {
return new CULHandler(bridge, serialPortManager, localeProvider, i18nProvider);
} else if (thingTypeUID.equals(SOMFY_DEVICE_THING_TYPE)) {
return new SomfyCULHandler(thing, localeProvider, i18nProvider);
}
return null;
}
}
@@ -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.somfycul.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@code SomfyCommand} provides the available commands due to Somfy's RTS protocol.
*
* http://culfw.de/commandref.html#cmd_Y
*
* @author Marc Klasser - Initial contribution
*/
@NonNullByDefault
public enum SomfyCommand {
MY("1"),
UP("2"),
DOWN("4"),
PROG("8");
private String actionKey;
private SomfyCommand(String actionKey) {
this.actionKey = actionKey;
}
/**
* Returns the action key which is used for communicating with the CUL device.
*
* @return the action key
*/
public String getActionKey() {
return actionKey;
}
}
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<addon:addon id="somfycul" 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>SomfyCUL Binding</name>
<description>This is the binding for SomfyCUL.</description>
<connection>local</connection>
</addon:addon>
@@ -0,0 +1,40 @@
# add-on
addon.somfycul.name = SomfyCUL Binding
addon.somfycul.description = This is the binding for SomfyCUL.
# thing types
thing-type.somfycul.cul-device.label = CUL Stick
thing-type.somfycul.cul-device.description = The CUL stick controlling the Somfy RTS devices
thing-type.somfycul.somfy-device.label = Somfy RTS Rollershutter
thing-type.somfycul.somfy-device.description = The Somfy RTS Rollershutter device controlled by the CUL stick
# thing types config
thing-type.config.somfycul.cul-device.baudrate.label = Baud Rate
thing-type.config.somfycul.cul-device.baudrate.description = Set the serial port baud rate (must match the CUL stick configuration)
thing-type.config.somfycul.cul-device.baudrate.option.2400 = 2400
thing-type.config.somfycul.cul-device.baudrate.option.9600 = 9600
thing-type.config.somfycul.cul-device.baudrate.option.19200 = 19200
thing-type.config.somfycul.cul-device.baudrate.option.38400 = 38400
thing-type.config.somfycul.cul-device.baudrate.option.57600 = 57600
thing-type.config.somfycul.cul-device.group.port.label = Port Configuration
thing-type.config.somfycul.cul-device.port.label = Serial Port
thing-type.config.somfycul.cul-device.port.description = Select serial port (COM1, /dev/ttyS0, ...)
# channel types
channel-type.somfycul.position.label = Control
channel-type.somfycul.position.description = Device control (UP, DOWN, MY/STOP)
channel-type.somfycul.program.label = Program
channel-type.somfycul.program.description = Device program
# thing status descriptions
offline.config-error-missing = SomfyCUL configuration missing or invalid
offline.config-error-port-not-found = Serial port {0} could not be found. Available ports are:\n{1}
offline.comm-error-io = IO Error: {0}
offline.comm-error-port-in-use = Port already in use: {0}
offline.comm-error-unsupported-operation = Unsupported operation on port: {0}: {1}
offline.init-error = Initialization failed: {0}
@@ -0,0 +1,75 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="somfycul"
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">
<!-- The CUL stick as bridge -->
<bridge-type id="cul-device">
<label>CUL Stick</label>
<description>The CUL stick controlling the Somfy RTS devices</description>
<config-description>
<parameter-group name="port">
<context>communication</context>
<label>Port Configuration</label>
</parameter-group>
<parameter name="port" type="text" required="true" groupName="port">
<context>serial-port</context>
<label>Serial Port</label>
<description>Select serial port (COM1, /dev/ttyS0, ...)</description>
<default>/dev/ttyUSB0</default>
</parameter>
<parameter name="baudrate" type="integer" required="true" groupName="port">
<label>Baud Rate</label>
<description>Set the serial port baud rate. The selected baud rate must match the configuration of your CUL stick
for reliable device communication.</description>
<default>38400</default>
<limitToOptions>true</limitToOptions>
<options>
<option value="2400">2400</option>
<option value="9600">9600</option>
<option value="19200">19200</option>
<option value="38400">38400</option>
<option value="57600">57600</option>
</options>
</parameter>
</config-description>
</bridge-type>
<!-- A Somfy RTS thing -->
<thing-type id="somfy-device">
<supported-bridge-type-refs>
<bridge-type-ref id="cul-device"/>
</supported-bridge-type-refs>
<label>Somfy RTS Rollershutter</label>
<description>The Somfy RTS Rollershutter device controlled by the CUL stick</description>
<channels>
<channel id="position" typeId="position"/>
<channel id="program" typeId="program"/>
</channels>
</thing-type>
<channel-type id="position">
<item-type>Rollershutter</item-type>
<label>Control</label>
<description>Device control (UP, DOWN, MY/STOP)</description>
<tags>
<tag>Control</tag>
<tag>OpenLevel</tag>
</tags>
</channel-type>
<channel-type id="program">
<item-type>Switch</item-type>
<label>Program</label>
<description>Device program</description>
<tags>
<tag>Control</tag>
<tag>Mode</tag>
</tags>
</channel-type>
</thing:thing-descriptions>
+1
View File
@@ -410,6 +410,7 @@
<module>org.openhab.binding.solarmax</module>
<module>org.openhab.binding.solarwatt</module>
<module>org.openhab.binding.solax</module>
<module>org.openhab.binding.somfycul</module>
<module>org.openhab.binding.somfymylink</module>
<module>org.openhab.binding.somfytahoma</module>
<module>org.openhab.binding.somneo</module>