mirror of
https://github.com/danieldemus/openhab-addons
synced 2026-07-29 12:34:21 +02:00
[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:
@@ -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
|
||||
|
||||
@@ -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>
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2025 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.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;
|
||||
}
|
||||
+237
@@ -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;
|
||||
}
|
||||
}
|
||||
+50
@@ -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";
|
||||
}
|
||||
+264
@@ -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();
|
||||
}
|
||||
}
|
||||
+76
@@ -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;
|
||||
}
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2025 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.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>
|
||||
+40
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user