[rollershutterposition] Initial contribution (#13259)

* Initial contribution

Signed-off-by: Jeff James <jeff@james-online.com>
This commit is contained in:
jsjames 2023-02-26 13:54:27 -08:00 committed by GitHub
parent 60d70efce6
commit fe0f49ea63
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 507 additions and 0 deletions

View File

@ -399,6 +399,7 @@
/bundles/org.openhab.transform.jsonpath/ @clinique
/bundles/org.openhab.transform.map/ @openhab/add-ons-maintainers
/bundles/org.openhab.transform.regex/ @openhab/add-ons-maintainers
/bundles/org.openhab.transform.rollershutterposition/ @jsjames
/bundles/org.openhab.transform.scale/ @clinique
/bundles/org.openhab.transform.xpath/ @openhab/add-ons-maintainers
/bundles/org.openhab.transform.xslt/ @openhab/add-ons-maintainers

View File

@ -1986,6 +1986,11 @@
<artifactId>org.openhab.transform.regex</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.transform.rollershutterposition</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.transform.scale</artifactId>

View File

@ -0,0 +1,13 @@
This content is produced and maintained by the openHAB project.
* Project home: https://www.openhab.org
== Declared Project Licenses
This program and the accompanying materials are made available under the terms
of the Eclipse Public License 2.0 which is available at
https://www.eclipse.org/legal/epl-2.0/.
== Source Code
https://github.com/openhab/openhab-addons

View File

@ -0,0 +1,19 @@
# Rollershutter Position Emulation Profile Service
The Rollershutter Position emulates absolute position setting for Rollershutter devices which only support basic UP/DOWN/STOP commands.
This allows a Rollershutter to be set to an absolution position from 0..100 even if the controller does not support this feature (i.e. Somfy controllers).
The logic code used for this profile service was adapted from Tarag Gautier's JavaScript implementation VASRollershutter.js.
By implementing as a profile, it eliminates the need for setting up a jsr233 js environment and simplifies the configuration.
## Configuration
To use this profile, simply include the profile on the Rollershutter item which is assigned to the Rollershutter channel.
The parameters <uptime> and <downtime> are the time it takes for the Rollershutter to fully extend or close in seconds.
The precision parameter can be used to specify the minimum movement that can be made.
This is useful when latencies in the system limit prevent very small movements and will reduce the accuracy of the position estimation.
```java
Rollershutter <itemName> { channel="<channelUID>"[profile="rollershutter:position", uptime=<uptime>, downtime=<downtime>, precision=<minimun percent movement>]]}
```

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.addons.reactor.bundles</artifactId>
<version>4.0.0-SNAPSHOT</version>
</parent>
<artifactId>org.openhab.transform.rollershutterposition</artifactId>
<name>openHAB Add-ons :: Bundles :: Transformation Service :: Roller Shutter Position</name>
</project>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<features name="org.openhab.transform.rollershutterposition-${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-transformation-rollershutterposition" description="Roller Shutter Position Emulation" version="${project.version}">
<feature>openhab-runtime-base</feature>
<bundle start-level="75">mvn:org.openhab.addons.bundles/org.openhab.transform.rollershutterposition/${project.version}</bundle>
</feature>
</features>

View File

@ -0,0 +1,39 @@
/**
* Copyright (c) 2010-2023 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.transform.rollershutterposition.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.profiles.ProfileTypeUID;
import org.openhab.core.transform.TransformationService;
/**
* The {@link RollerShutterPositionConstants} class to define transform constants
* used across the whole binding.
*
* @author Jeff James - Initial contribution
*/
@NonNullByDefault
public class RollerShutterPositionConstants {
// Profile Type UID
public static final ProfileTypeUID PROFILE_TYPE_UID = new ProfileTypeUID(
TransformationService.TRANSFORM_PROFILE_SCOPE, "ROLLERSHUTTERPOSITION");
// Parameters
public static final String UPTIME_PARAM = "uptime";
public static final String DOWNTIME_PARAM = "downtime";
public static final String PRECISION_PARAM = "precision";
public static final int POSITION_UPDATE_PERIOD_MILLISECONDS = 800;
public static final int DEFAULT_PRECISION = 5;
}

View File

@ -0,0 +1,282 @@
/**
* Copyright (c) 2010-2023 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.transform.rollershutterposition.internal;
import static org.openhab.transform.rollershutterposition.internal.RollerShutterPositionConstants.*;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Objects;
import java.util.concurrent.ScheduledExecutorService;
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.core.common.ThreadPoolManager;
import org.openhab.core.library.types.PercentType;
import org.openhab.core.library.types.StopMoveType;
import org.openhab.core.library.types.UpDownType;
import org.openhab.core.thing.profiles.ProfileCallback;
import org.openhab.core.thing.profiles.ProfileContext;
import org.openhab.core.thing.profiles.ProfileTypeUID;
import org.openhab.core.thing.profiles.StateProfile;
import org.openhab.core.types.Command;
import org.openhab.core.types.State;
import org.openhab.transform.rollershutterposition.internal.config.RollerShutterPositionConfig;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Profile to implement the RollerShutterPosition ItemChannelLink
*
* @author Jeff James - Initial contribution
*
* Core logic in this module has been heavily adapted from Tarag Gautier js script implementation
* VASRollershutter.js
*/
@NonNullByDefault
public class RollerShutterPositionProfile implements StateProfile {
private static final String PROFILE_THREADPOOL_NAME = "profile-rollershutterposition";
private final Logger logger = LoggerFactory.getLogger(RollerShutterPositionProfile.class);
private final ProfileCallback callback;
RollerShutterPositionConfig configuration;
private int position = 0; // current position of the roller shutter (assumes 0 when system starts)
private int targetPosition;
private boolean isValidConfiguration = false;
private Instant movingSince = Instant.MIN;
private UpDownType direction = UpDownType.DOWN;
private final ScheduledExecutorService scheduler = ThreadPoolManager.getScheduledPool(PROFILE_THREADPOOL_NAME);
protected @Nullable ScheduledFuture<?> stopTimer = null;
protected @Nullable ScheduledFuture<?> updateTimer = null;
public RollerShutterPositionProfile(final ProfileCallback callback, final ProfileContext context) {
this.callback = callback;
this.configuration = context.getConfiguration().as(RollerShutterPositionConfig.class);
if (configuration.uptime == 0) {
logger.info("Profile paramater {} must not be 0", UPTIME_PARAM);
return;
}
if (configuration.downtime == 0) {
configuration.downtime = configuration.uptime;
}
if (configuration.precision == 0) {
configuration.precision = DEFAULT_PRECISION;
}
this.isValidConfiguration = true;
logger.debug("Profile configured with '{}'='{}' ms, '{}'={} ms, '{}'={}", UPTIME_PARAM, configuration.uptime,
DOWNTIME_PARAM, configuration.downtime, PRECISION_PARAM, configuration.precision);
}
@Override
public ProfileTypeUID getProfileTypeUID() {
return PROFILE_TYPE_UID;
}
@Override
public void onCommandFromItem(Command command) {
logger.debug("onCommandFromItem: {}", command);
// pass through command if profile has not been configured properly
if (!isValidConfiguration) {
callback.handleCommand(command);
return;
}
if (command instanceof UpDownType) {
if (command == UpDownType.UP) {
moveTo(0);
} else if (command == UpDownType.DOWN) {
moveTo(100);
}
} else if (command instanceof StopMoveType) {
stop();
} else {
moveTo(((PercentType) command).intValue());
}
}
private boolean isMoving() {
return (!movingSince.equals(Instant.MIN));
}
private void moveTo(int targetPos) {
boolean alreadyMoving = false;
if (targetPos < 0 || targetPos > 100) {
logger.debug("moveTo() position is invalid: {}", targetPos);
return;
}
int curPos = currentPosition();
int posOffset = targetPos - curPos;
UpDownType newCmd;
if (targetPos == position && !isMoving()) {
logger.debug("moveTo() position already current: {}", targetPos);
if (targetPos == 0) { // always send command if either 0 or 100 in case it is not already in that position
callback.handleCommand(UpDownType.UP);
} else if (targetPos == 100) {
callback.handleCommand(UpDownType.DOWN);
}
return;
} else if (targetPos == 0 || targetPos == 100) {
logger.debug("moveTo() bounding position");
newCmd = targetPos == 0 ? UpDownType.UP : UpDownType.DOWN;
} else if (Math.abs(posOffset) < configuration.precision) {
callback.sendUpdate(new PercentType(position)); // update position because autoupdate will assume the
// movement happened
logger.info("moveTo() is less than the precision setting of {}", configuration.precision);
return;
} else {
newCmd = posOffset > 0 ? UpDownType.DOWN : UpDownType.UP;
}
logger.debug("moveTo() targetPosition: {} from currentPosition: {}", targetPos, curPos);
long time = (long) ((Math.abs(posOffset) / 100d)
* (posOffset > 0 ? (double) configuration.downtime * 1000 : (double) configuration.uptime * 1000));
logger.debug("moveTo() computed movement offset: {} / {} / {} ms", posOffset, newCmd, time);
if (isMoving()) {
position = curPos; // Update "starting" position if already in motion since the last move did not finish
if (direction == newCmd) {
alreadyMoving = true;
}
}
this.targetPosition = targetPos;
this.direction = newCmd;
this.movingSince = Instant.now();
if (stopTimer != null) {
Objects.requireNonNull(stopTimer).cancel(true);
}
this.stopTimer = scheduler.schedule(stopTimeoutTask, time, TimeUnit.MILLISECONDS);
if (updateTimer != null) {
Objects.requireNonNull(updateTimer).cancel(true);
}
this.updateTimer = scheduler.scheduleWithFixedDelay(updateTimeoutTask, 0, POSITION_UPDATE_PERIOD_MILLISECONDS,
TimeUnit.MILLISECONDS);
if (!alreadyMoving) {
logger.debug("moveTo() sending command for movement: {}, timer set in {} ms", direction, time);
callback.handleCommand(direction);
} else {
logger.debug("moveTo() updating timing but already moving in right directio: {}, timer set in {} ms",
direction, time);
}
}
private void stop() {
callback.handleCommand(StopMoveType.STOP);
this.position = currentPosition();
this.movingSince = Instant.MIN;
if (stopTimer != null) {
Objects.requireNonNull(stopTimer).cancel(true);
this.stopTimer = null;
}
if (updateTimer != null) {
Objects.requireNonNull(updateTimer).cancel(true);
this.updateTimer = null;
}
callback.sendUpdate(new PercentType(position));
}
private int currentPosition() {
if (isMoving()) {
logger.trace("currentPosition() while moving");
// movingSince is always set if moving
long millis = movingSince.until(Instant.now(), ChronoUnit.MILLIS);
double delta = 0;
if (direction == UpDownType.UP) {
delta = -(millis / (configuration.uptime * 1000)) * 100d;
} else {
delta = (millis / (configuration.downtime * 1000)) * 100d;
}
return (int) Math.max(0, Math.min(100, Math.round(position + delta)));
} else {
return position;
}
}
// Runnable task to time duration of the move to make
private Runnable stopTimeoutTask = new Runnable() {
@Override
public void run() {
if (targetPosition == 0 || targetPosition == 100) {
// Don't send stop command to re-sync position using the motor end stop
logger.debug("arrived at end position, not stopping for calibration");
} else {
callback.handleCommand(StopMoveType.STOP);
logger.debug("arrived at position, sending STOP command");
}
logger.trace("stopTimeoutTask() position: {}", targetPosition);
if (updateTimer != null) {
Objects.requireNonNull(updateTimer).cancel(true);
updateTimer = null;
}
movingSince = Instant.MIN;
position = targetPosition;
targetPosition = -1;
callback.sendUpdate(new PercentType(position));
}
};
// Runnable task to update the item on position while the roller shutter is moving
private Runnable updateTimeoutTask = new Runnable() {
@Override
public void run() {
if (isMoving()) {
int pos = currentPosition();
if (pos < 0 || pos > 100) {
return;
}
callback.sendUpdate(new PercentType(pos));
logger.trace("updateTimeoutTask(): {}", pos);
}
}
};
@Override
public void onStateUpdateFromItem(State state) {
}
@Override
public void onCommandFromHandler(Command command) {
}
@Override
public void onStateUpdateFromHandler(State state) {
}
}

View File

@ -0,0 +1,58 @@
/**
* Copyright (c) 2010-2023 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.transform.rollershutterposition.internal;
import static org.openhab.transform.rollershutterposition.internal.RollerShutterPositionConstants.PROFILE_TYPE_UID;
import java.util.Collection;
import java.util.List;
import java.util.Locale;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.library.CoreItemFactory;
import org.openhab.core.thing.profiles.Profile;
import org.openhab.core.thing.profiles.ProfileCallback;
import org.openhab.core.thing.profiles.ProfileContext;
import org.openhab.core.thing.profiles.ProfileFactory;
import org.openhab.core.thing.profiles.ProfileType;
import org.openhab.core.thing.profiles.ProfileTypeBuilder;
import org.openhab.core.thing.profiles.ProfileTypeProvider;
import org.openhab.core.thing.profiles.ProfileTypeUID;
import org.osgi.service.component.annotations.Component;
/**
* {@link RollerShutterPositionProfileFactory} Factory to create the profile
*
* @author Jeff James - Initial contribution
*/
@NonNullByDefault
@Component(service = { ProfileFactory.class, ProfileTypeProvider.class })
public class RollerShutterPositionProfileFactory implements ProfileFactory, ProfileTypeProvider {
@Override
public Collection<ProfileType> getProfileTypes(@Nullable Locale locale) {
return List.of(ProfileTypeBuilder.newState(PROFILE_TYPE_UID, PROFILE_TYPE_UID.getId())
.withSupportedItemTypes(CoreItemFactory.ROLLERSHUTTER).build());
}
@Override
public @Nullable Profile createProfile(ProfileTypeUID profileTypeUID, ProfileCallback callback,
ProfileContext profileContext) {
return new RollerShutterPositionProfile(callback, profileContext);
}
@Override
public Collection<ProfileTypeUID> getSupportedProfileTypeUIDs() {
return List.of(PROFILE_TYPE_UID);
}
}

View File

@ -0,0 +1,30 @@
/**
* Copyright (c) 2010-2023 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.transform.rollershutterposition.internal.config;
import static org.openhab.transform.rollershutterposition.internal.RollerShutterPositionConstants.DEFAULT_PRECISION;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link RollerShutterPositionConfig} class contains the parameters for RollerShutterPosition
*
* @author Jeff James - initial contribution
*
*/
@NonNullByDefault
public class RollerShutterPositionConfig {
public float uptime; // uptime in seconds (set by param)
public float downtime; // downtime in seconds (set by param)
public int precision = DEFAULT_PRECISION; // minimum movement
}

View File

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<config-description:config-descriptions
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0 https://openhab.org/schemas/config-description-1.0.0.xsd">
<config-description uri="profile:rollershutter:position">
<parameter name="uptime" type="decimal" required="true">
<label>Up Time</label>
<description>Time it takes for roller shutter to fully open (in seconds).</description>
<required>true</required>
</parameter>
<parameter name="downtime" type="decimal">
<label>Down Time</label>
<description>Time it takes for roller shutter to extend the full length (in seconds). Defaults to Up Time if not
specified.</description>
</parameter>
<parameter name="precision" type="integer">
<label>Precision</label>
<description>Minimum movement (in percent) that can be requested. If the requested change is less than this amount,
no action will be taken. This may be required for systems where there is a lag in the stop command and
consequently
it is not possible for fine control of movement. (default = 5)</description>
<default>5</default>
</parameter>
</config-description>
</config-description:config-descriptions>

View File

@ -0,0 +1,6 @@
profile.config.rollershutter.position.downtime.label = Down Time
profile.config.rollershutter.position.downtime.description = Time it takes for roller shutter to extend the full length (in seconds). Defaults to Up Time if not specified.
profile.config.rollershutter.position.precision.label = Precision
profile.config.rollershutter.position.precision.description = Minimum movement (in percent) that can be requested. If the requested change is less than this amount, no action will be taken. This may be required for systems where there is a lag in the stop command and consequently it is not possible for fine control of movement. (default = 5)
profile.config.rollershutter.position.uptime.label = Up Time
profile.config.rollershutter.position.uptime.description = Time it takes for roller shutter to fully open (in seconds).

View File

@ -39,6 +39,7 @@
<module>org.openhab.transform.jsonpath</module>
<module>org.openhab.transform.map</module>
<module>org.openhab.transform.regex</module>
<module>org.openhab.transform.rollershutterposition</module>
<module>org.openhab.transform.scale</module>
<module>org.openhab.transform.xpath</module>
<module>org.openhab.transform.xslt</module>