Skip to content

Commit

Permalink
[profiles] Added 'Range' profile (openhab#2046)
Browse files Browse the repository at this point in the history
Signed-off-by: Christoph Weitkamp <[email protected]>
  • Loading branch information
cweitkamp authored Feb 27, 2021
1 parent 7de57f9 commit 9c0302e
Show file tree
Hide file tree
Showing 8 changed files with 469 additions and 12 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
import tec.uom.se.AbstractUnit;

/***
* This is the default implementation for a {@link SystemHysteresisStateProfile}}.
* This is the default implementation for a {@link SystemHysteresisStateProfile}.
*
* @author Christoph Weitkamp - Initial contribution
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,13 +64,13 @@ public class SystemProfileFactory implements ProfileFactory, ProfileAdvisor, Pro
private final ChannelTypeRegistry channelTypeRegistry;

private static final Set<ProfileType> SUPPORTED_PROFILE_TYPES = Set.of(DEFAULT_TYPE, FOLLOW_TYPE, HYSTERESIS_TYPE,
OFFSET_TYPE, RAWBUTTON_ON_OFF_SWITCH_TYPE, RAWBUTTON_TOGGLE_PLAYER_TYPE, RAWBUTTON_TOGGLE_SWITCH_TYPE,
RAWROCKER_DIMMER_TYPE, RAWROCKER_NEXT_PREVIOUS_TYPE, RAWROCKER_ON_OFF_TYPE, RAWROCKER_PLAY_PAUSE_TYPE,
RAWROCKER_REWIND_FASTFORWARD_TYPE, RAWROCKER_STOP_MOVE_TYPE, RAWROCKER_UP_DOWN_TYPE, TIMESTAMP_CHANGE_TYPE,
TIMESTAMP_UPDATE_TYPE);
OFFSET_TYPE, RANGE_TYPE, RAWBUTTON_ON_OFF_SWITCH_TYPE, RAWBUTTON_TOGGLE_PLAYER_TYPE,
RAWBUTTON_TOGGLE_SWITCH_TYPE, RAWROCKER_DIMMER_TYPE, RAWROCKER_NEXT_PREVIOUS_TYPE, RAWROCKER_ON_OFF_TYPE,
RAWROCKER_PLAY_PAUSE_TYPE, RAWROCKER_REWIND_FASTFORWARD_TYPE, RAWROCKER_STOP_MOVE_TYPE,
RAWROCKER_UP_DOWN_TYPE, TIMESTAMP_CHANGE_TYPE, TIMESTAMP_UPDATE_TYPE);

private static final Set<ProfileTypeUID> SUPPORTED_PROFILE_TYPE_UIDS = Set.of(DEFAULT, FOLLOW, HYSTERESIS, OFFSET,
RAWBUTTON_ON_OFF_SWITCH, RAWBUTTON_TOGGLE_PLAYER, RAWBUTTON_TOGGLE_SWITCH, RAWROCKER_DIMMER,
RANGE, RAWBUTTON_ON_OFF_SWITCH, RAWBUTTON_TOGGLE_PLAYER, RAWBUTTON_TOGGLE_SWITCH, RAWROCKER_DIMMER,
RAWROCKER_NEXT_PREVIOUS, RAWROCKER_ON_OFF, RAWROCKER_PLAY_PAUSE, RAWROCKER_REWIND_FASTFORWARD,
RAWROCKER_STOP_MOVE, RAWROCKER_UP_DOWN, TIMESTAMP_CHANGE, TIMESTAMP_UPDATE);

Expand Down Expand Up @@ -99,6 +99,8 @@ public SystemProfileFactory(final @Reference ChannelTypeRegistry channelTypeRegi
return new SystemHysteresisStateProfile(callback, context);
} else if (OFFSET.equals(profileTypeUID)) {
return new SystemOffsetProfile(callback, context);
} else if (RANGE.equals(profileTypeUID)) {
return new SystemRangeStateProfile(callback, context);
} else if (RAWBUTTON_ON_OFF_SWITCH.equals(profileTypeUID)) {
return new RawButtonOnOffSwitchProfile(callback);
} else if (RAWBUTTON_TOGGLE_SWITCH.equals(profileTypeUID)) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
/**
* Copyright (c) 2010-2021 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.core.thing.internal.profiles;

import java.math.BigDecimal;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.unit.Units;
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.thing.profiles.SystemProfiles;
import org.openhab.core.types.Command;
import org.openhab.core.types.State;
import org.openhab.core.types.Type;
import org.openhab.core.types.UnDefType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import tec.uom.se.AbstractUnit;

/***
* This is the default implementation for a {@link SystemRangeStateProfile}.
*
* @author Christoph Weitkamp - Initial contribution
*/
@NonNullByDefault
public class SystemRangeStateProfile implements StateProfile {

static final String LOWER_PARAM = "lower";
static final String UPPER_PARAM = "upper";
static final String INVERTED_PARAM = "inverted";

private final Logger logger = LoggerFactory.getLogger(SystemRangeStateProfile.class);

private final ProfileCallback callback;

private final QuantityType<?> lower;
private final QuantityType<?> upper;
private final OnOffType inRange;
private final OnOffType notInRange;

private Type previousType = UnDefType.UNDEF;

public SystemRangeStateProfile(ProfileCallback callback, ProfileContext context) {
this.callback = callback;

final QuantityType<?> lowerParam = getParam(context, LOWER_PARAM);
if (lowerParam == null) {
throw new IllegalArgumentException(String.format("Parameter '%s' is not a Number value.", LOWER_PARAM));
}
this.lower = lowerParam;
final QuantityType<?> upperParam = getParam(context, UPPER_PARAM);
if (upperParam == null) {
throw new IllegalArgumentException(String.format("Parameter '%s' is not a Number value.", UPPER_PARAM));
}
final QuantityType<?> convertedUpperParam = upperParam.toUnit(lower.getUnit());
if (convertedUpperParam == null) {
throw new IllegalArgumentException(
String.format("Units of parameters '%s' and '%s' are not compatible: %s != %s", LOWER_PARAM,
UPPER_PARAM, lower, upperParam));
}
if (convertedUpperParam.doubleValue() <= lower.doubleValue()) {
throw new IllegalArgumentException(
String.format("Parameter '%s' (%s) is less than or equal to '%s' (%s) parameter.", UPPER_PARAM,
convertedUpperParam, LOWER_PARAM, lower));
}
this.upper = convertedUpperParam;

final Object paramValue = context.getConfiguration().get(INVERTED_PARAM);
final boolean inverted = paramValue == null ? false : Boolean.valueOf(paramValue.toString());
this.inRange = inverted ? OnOffType.OFF : OnOffType.ON;
this.notInRange = inverted ? OnOffType.ON : OnOffType.OFF;
}

private @Nullable QuantityType<?> getParam(ProfileContext context, String param) {
final Object paramValue = context.getConfiguration().get(param);
logger.debug("Configuring profile with {} parameter '{}'", param, paramValue);
if (paramValue instanceof String) {
try {
return new QuantityType<>((String) paramValue);
} catch (IllegalArgumentException e) {
logger.error("Cannot convert value '{}' of parameter {} into a valid QuantityType.", paramValue, param);
}
} else if (paramValue instanceof BigDecimal) {
final BigDecimal value = (BigDecimal) paramValue;
return QuantityType.valueOf(value.doubleValue(), AbstractUnit.ONE);
}
return null;
}

@Override
public ProfileTypeUID getProfileTypeUID() {
return SystemProfiles.RANGE;
}

@Override
public void onStateUpdateFromItem(State state) {
// do nothing
}

@Override
public void onCommandFromHandler(Command command) {
final Type mappedCommand = mapValue(command);
logger.trace("Mapped command from '{}' to command '{}'.", command, mappedCommand);
if (mappedCommand instanceof Command) {
callback.sendCommand((Command) mappedCommand);
}
}

@Override
public void onCommandFromItem(Command command) {
// do nothing
}

@Override
public void onStateUpdateFromHandler(State state) {
final Type mappedState = mapValue(state);
logger.trace("Mapped state from '{}' to state '{}'.", state, mappedState);
if (mappedState instanceof State) {
callback.sendUpdate((State) mappedState);
}
}

private Type mapValue(Type value) {
if (value instanceof QuantityType) {
final QuantityType<?> qtState = (QuantityType<?>) value;
final QuantityType<?> finalLower;
final QuantityType<?> finalUpper;
if (lower.getUnit() == Units.ONE && upper.getUnit() == Units.ONE) {
// allow bounds without unit -> implicitly assume its the same as the one from the state, but warn
// the user
finalLower = new QuantityType<>(lower.toBigDecimal(), qtState.getUnit());
finalUpper = new QuantityType<>(upper.toBigDecimal(), qtState.getUnit());
logger.warn(
"Received a QuantityType '{}' with unit, but the boundaries are defined as a plain number without units (lower={}, upper={}), please consider adding units to them.",
value, lower, upper);
} else {
finalLower = lower.toUnit(qtState.getUnit());
finalUpper = upper.toUnit(qtState.getUnit());
if (finalLower == null || finalUpper == null) {
logger.warn(
"Cannot compare state '{}' to boundaries because units (lower={}, upper={}) do not match.",
qtState, lower, upper);
return previousType;
}
}
return previousType = mapValue(finalLower.doubleValue(), finalUpper.doubleValue(), qtState.doubleValue());
} else if (value instanceof DecimalType) {
return previousType = mapValue(lower.doubleValue(), upper.doubleValue(),
((DecimalType) value).doubleValue());
}
return previousType;
}

private Type mapValue(double lower, double upper, double value) {
return lower <= value && value <= upper ? inRange : notInRange;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ public interface SystemProfiles {
ProfileTypeUID FOLLOW = new ProfileTypeUID(SYSTEM_SCOPE, "follow");
ProfileTypeUID OFFSET = new ProfileTypeUID(SYSTEM_SCOPE, "offset");
ProfileTypeUID HYSTERESIS = new ProfileTypeUID(SYSTEM_SCOPE, "hysteresis");
ProfileTypeUID RANGE = new ProfileTypeUID(SYSTEM_SCOPE, "range");
ProfileTypeUID RAWBUTTON_ON_OFF_SWITCH = new ProfileTypeUID(SYSTEM_SCOPE, "rawbutton-on-off-switch");
ProfileTypeUID RAWBUTTON_TOGGLE_PLAYER = new ProfileTypeUID(SYSTEM_SCOPE, "rawbutton-toggle-player");
ProfileTypeUID RAWBUTTON_TOGGLE_ROLLERSHUTTER = new ProfileTypeUID(SYSTEM_SCOPE, "rawbutton-toggle-rollershutter");
Expand All @@ -51,11 +52,17 @@ public interface SystemProfiles {
StateProfileType OFFSET_TYPE = ProfileTypeBuilder.newState(OFFSET, "Offset")
.withSupportedItemTypes(CoreItemFactory.NUMBER).withSupportedItemTypesOfChannel(CoreItemFactory.NUMBER)
.build();

ProfileType HYSTERESIS_TYPE = ProfileTypeBuilder.newState(HYSTERESIS, "Hysteresis") //
.withSupportedItemTypesOfChannel(CoreItemFactory.DIMMER, CoreItemFactory.NUMBER) //
.withSupportedItemTypes(CoreItemFactory.SWITCH) //
.build();

ProfileType RANGE_TYPE = ProfileTypeBuilder.newState(RANGE, "Range") //
.withSupportedItemTypesOfChannel(CoreItemFactory.DIMMER, CoreItemFactory.NUMBER) //
.withSupportedItemTypes(CoreItemFactory.SWITCH) //
.build();

TriggerProfileType RAWBUTTON_ON_OFF_SWITCH_TYPE = ProfileTypeBuilder
.newTrigger(RAWBUTTON_ON_OFF_SWITCH, "Raw Button To On Off")
.withSupportedItemTypes(CoreItemFactory.SWITCH, CoreItemFactory.DIMMER, CoreItemFactory.COLOR)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?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:system:range">
<parameter name="lower" type="text" required="true">
<label>Lower Bound</label>
<description>Maps to ON if value is between lower and upper bound (plain number or number with unit).</description>
</parameter>
<parameter name="upper" type="text" required="true">
<label>Upper Bound</label>
<description>Maps to ON if value is between lower and upper bound (plain number or number with unit).</description>
</parameter>
<parameter name="inverted" type="boolean">
<label>Inverted</label>
<description>Inverts resulting mapping of ON / OFF, if true.</description>
<default>false</default>
</parameter>
</config-description>
</config-description:config-descriptions>
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,20 @@ profile-type.system.default.label = Standard
profile-type.system.follow.label = Folgen
profile-type.system.offset.label = Versatz
profile.config.system.offset.offset.label = Versatz
profile.config.system.offset.offset.description = Versatz (Numerischer Wert oder numerischer Wert mit Einheit), welcher auf den Wert addiert wird. Ein negativer Versatz wird vom Wert abgezogen.
profile.config.system.offset.offset.description = Versatz (numerischer Wert oder numerischer Wert mit Einheit), welcher auf den Wert addiert wird. Ein negativer Versatz wird vom Wert abgezogen.
profile-type.system.hysteresis.label = Hysterese
profile.config.system.hysteresis.lower.label = Untere Grenze
profile.config.system.hysteresis.lower.description = Bildet bei Unterschreitung des unteren Grenzwertes OFF auf ein Switch Item ab (Numerischer Wert oder numerischer Wert mit Einheit).
profile.config.system.hysteresis.lower.description = Bildet bei Unterschreitung des unteren Grenzwertes OFF auf ein Switch Item ab (numerischer Wert oder numerischer Wert mit Einheit).
profile.config.system.hysteresis.upper.label = Obere Grenze
profile.config.system.hysteresis.upper.description = Bildet bei Überschreitung des oberen Grenzwertes ON auf ein Switch Item ab (Numerischer Wert oder numerischer Wert mit Einheit).
profile.config.system.hysteresis.upper.description = Bildet bei Überschreitung des oberen Grenzwertes ON auf ein Switch Item ab (numerischer Wert oder numerischer Wert mit Einheit).
profile.config.system.hysteresis.inverted.label = Invertiert
profile.config.system.hysteresis.inverted.description = Invertiert die resultierende Zuordnung von ON / OFF, falls true.
profile-type.system.timestamp-change.label = Zeitstempel bei Änderung
profile-type.system.range.label = Bereich
profile.config.system.range.lower.label = Untere Grenze
profile.config.system.range.lower.description = Bildet bei ON auf ein Switch Item ab, wenn der Wert zwischen der unteren und oberen Grenze liegt (numerischer Wert oder numerischer Wert mit Einheit).
profile.config.system.range.upper.label = Obere Grenze
profile.config.system.range.upper.description = Bildet bei ON auf ein Switch Item ab, wenn der Wert zwischen der unteren und oberen Grenze liegt (numerischer Wert oder numerischer Wert mit Einheit).
profile.config.system.range.inverted.label = Invertiert
profile.config.system.range.inverted.description = Invertiert die resultierende Zuordnung von ON / OFF, falls true.
profile-type.system.timestamp-change.label = Zeitstempel bei Änderung
profile-type.system.timestamp-update.label = Zeitstempel bei Aktualisierung
Loading

0 comments on commit 9c0302e

Please sign in to comment.