Skip to content

Commit

Permalink
[basicprofiles] State filter profile (#531)
Browse files Browse the repository at this point in the history
* State filter profile

Signed-off-by: Arne Seime <[email protected]>
Co-authored-by: J-N-K <[email protected]>
(cherry picked from commit 6104110)
Signed-off-by: Jan N. Klug <[email protected]>
  • Loading branch information
J-N-K committed Mar 24, 2024
1 parent ead5a6a commit dfd7c81
Show file tree
Hide file tree
Showing 7 changed files with 535 additions and 6 deletions.
31 changes: 31 additions & 0 deletions bundles/org.smarthomej.transform.basicprofiles/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,3 +177,34 @@ Switch motionSensorFirstFloor {
channel="deconz:colortemperaturelight:AAA:BBB:brightness" [profile="basic-profiles:time-range-command", inRangeValue=100, outOfRangeValue=15, start="08:00", end="23:00", restoreValue="PREVIOUS"]
}
```

## State Filter Profile

This filter passes on state updates from a (binding) handler to the item if and only if all listed item state conditions
are met (conditions are ANDed together).
Option to instead pass different state update in case the conditions are not met.
State values may be quoted to treat as `StringType`.

Use case: Ignore values from a binding unless some other item(s) have a specific state.

### Configuration

| Configuration Parameter | Type | Description |
|-------------------------|------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `conditions` | text | Comma separated list of expressions on the format `ITEM_NAME OPERATOR ITEM_STATE`, ie `MyItem EQ OFF`. Use quotes around `ITEM_STATE` to treat value as string ie `'OFF'` and not `OnOffType.OFF` |
| `mismatchState` | text | Optional state to pass instead if conditions are NOT met. Use single quotes to treat as `StringType`. Defaults to `UNDEF` |
| `separator` | text | Optional separator string to separate expressions when using multiple. Defaults to `,` |

Possible values for token `OPERATOR` in `conditions`:

- `EQ` - Equals
- `NEQ` - Not equals


### Full Example

```Java
Number:Temperature airconTemperature{
channel="mybinding:mything:mychannel"[profile="basic-profiles:state-filter",conditions="airconPower_item EQ ON",mismatchState="UNDEF"]
}
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/**
* Copyright (c) 2021-2023 Contributors to the SmartHome/J 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.smarthomej.transform.basicprofiles.internal.config;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.types.UnDefType;
import org.smarthomej.transform.basicprofiles.internal.profiles.StateFilterProfile;

/**
* Configuration class for {@link StateFilterProfile}.
*
* @author Arne Seime - Initial contribution
*/
@NonNullByDefault
public class StateFilterProfileConfig {

public String conditions = "";

public String mismatchState = UnDefType.UNDEF.toString();

public String separator = ",";
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.i18n.LocalizedKey;
import org.openhab.core.items.ItemRegistry;
import org.openhab.core.library.CoreItemFactory;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.DefaultSystemChannelTypeProvider;
Expand All @@ -49,6 +50,7 @@
import org.smarthomej.transform.basicprofiles.internal.profiles.GenericToggleSwitchTriggerProfile;
import org.smarthomej.transform.basicprofiles.internal.profiles.InvertStateProfile;
import org.smarthomej.transform.basicprofiles.internal.profiles.RoundStateProfile;
import org.smarthomej.transform.basicprofiles.internal.profiles.StateFilterProfile;
import org.smarthomej.transform.basicprofiles.internal.profiles.ThresholdStateProfile;
import org.smarthomej.transform.basicprofiles.internal.profiles.TimeRangeCommandProfile;

Expand All @@ -69,6 +71,7 @@ public class BasicProfilesFactory implements ProfileFactory, ProfileTypeProvider
public static final ProfileTypeUID ROUND_UID = new ProfileTypeUID(SCOPE, "round");
public static final ProfileTypeUID THRESHOLD_UID = new ProfileTypeUID(SCOPE, "threshold");
public static final ProfileTypeUID TIME_RANGE_COMMAND_UID = new ProfileTypeUID(SCOPE, "time-range-command");
public static final ProfileTypeUID STATE_FILTER_UID = new ProfileTypeUID(SCOPE, "state-filter");

private static final ProfileType PROFILE_TYPE_GENERIC_COMMAND = ProfileTypeBuilder
.newTrigger(GENERIC_COMMAND_UID, "Generic Command") //
Expand Down Expand Up @@ -102,24 +105,29 @@ public class BasicProfilesFactory implements ProfileFactory, ProfileTypeProvider
.withSupportedItemTypes(CoreItemFactory.SWITCH) //
.withSupportedChannelTypeUIDs(DefaultSystemChannelTypeProvider.SYSTEM_CHANNEL_TYPE_UID_MOTION) //
.build();
private static final ProfileType PROFILE_STATE_FILTER = ProfileTypeBuilder
.newState(STATE_FILTER_UID, "Filter handler state updates based on any item state").build();

private static final Set<ProfileTypeUID> SUPPORTED_PROFILE_TYPE_UIDS = Set.of(GENERIC_COMMAND_UID,
GENERIC_TOGGLE_SWITCH_UID, DEBOUNCE_COUNTING_UID, DEBOUNCE_TIME_UID, INVERT_UID, ROUND_UID, THRESHOLD_UID,
TIME_RANGE_COMMAND_UID);
TIME_RANGE_COMMAND_UID, STATE_FILTER_UID);
private static final Set<ProfileType> SUPPORTED_PROFILE_TYPES = Set.of(PROFILE_TYPE_GENERIC_COMMAND,
PROFILE_TYPE_GENERIC_TOGGLE_SWITCH, PROFILE_TYPE_DEBOUNCE_COUNTING, PROFILE_TYPE_DEBOUNCE_TIME,
PROFILE_TYPE_INVERT, PROFILE_TYPE_ROUND, PROFILE_TYPE_THRESHOLD, PROFILE_TYPE_TIME_RANGE_COMMAND);
PROFILE_TYPE_INVERT, PROFILE_TYPE_ROUND, PROFILE_TYPE_THRESHOLD, PROFILE_TYPE_TIME_RANGE_COMMAND,
PROFILE_STATE_FILTER);

private final Map<LocalizedKey, ProfileType> localizedProfileTypeCache = new ConcurrentHashMap<>();

private final ProfileTypeI18nLocalizationService profileTypeI18nLocalizationService;
private final Bundle bundle;
private final ItemRegistry itemRegistry;

@Activate
public BasicProfilesFactory(final @Reference ProfileTypeI18nLocalizationService profileTypeI18nLocalizationService,
final @Reference BundleResolver bundleResolver) {
final @Reference BundleResolver bundleResolver, @Reference ItemRegistry itemRegistry) {
this.profileTypeI18nLocalizationService = profileTypeI18nLocalizationService;
this.bundle = bundleResolver.resolveBundle(BasicProfilesFactory.class);
this.itemRegistry = itemRegistry;
}

@Override
Expand All @@ -141,6 +149,8 @@ public BasicProfilesFactory(final @Reference ProfileTypeI18nLocalizationService
return new ThresholdStateProfile(callback, context);
} else if (TIME_RANGE_COMMAND_UID.equals(profileTypeUID)) {
return new TimeRangeCommandProfile(callback, context);
} else if (STATE_FILTER_UID.equals(profileTypeUID)) {
return new StateFilterProfile(callback, context, itemRegistry);
}
return null;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
/**
* Copyright (c) 2021-2023 Contributors to the SmartHome/J 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.smarthomej.transform.basicprofiles.internal.profiles;

import static org.smarthomej.transform.basicprofiles.internal.factory.BasicProfilesFactory.STATE_FILTER_UID;

import java.util.ArrayList;
import java.util.List;
import java.util.Locale;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.items.Item;
import org.openhab.core.items.ItemNotFoundException;
import org.openhab.core.items.ItemRegistry;
import org.openhab.core.library.types.StringType;
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.core.types.TypeParser;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.smarthomej.transform.basicprofiles.internal.config.StateFilterProfileConfig;

/**
* Accepts updates to state as long as conditions are met. Support for sending fixed state if conditions are *not*
* met.
*
* @author Arne Seime - Initial contribution
*/
@NonNullByDefault
public class StateFilterProfile implements StateProfile {

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

private final ItemRegistry itemRegistry;
private final ProfileCallback callback;
private List<Class<? extends State>> acceptedDataTypes;

private List<StateCondition> conditions = List.of();

private @Nullable State configMismatchState = null;

public StateFilterProfile(ProfileCallback callback, ProfileContext context, ItemRegistry itemRegistry) {
this.callback = callback;
acceptedDataTypes = context.getAcceptedDataTypes();
this.itemRegistry = itemRegistry;

StateFilterProfileConfig config = context.getConfiguration().as(StateFilterProfileConfig.class);
if (config != null) {
conditions = parseConditions(config.conditions, config.separator);
configMismatchState = parseState(config.mismatchState);
}
}

private List<StateCondition> parseConditions(@Nullable String config, String separator) {
if (config == null) {
return List.of();
}

List<StateCondition> parsedConditions = new ArrayList<>();
try {
String[] expressions = config.split(separator);
for (String expression : expressions) {
String[] parts = expression.trim().split("\s");
if (parts.length == 3) {
String itemName = parts[0];
StateCondition.ComparisonType conditionType = StateCondition.ComparisonType
.valueOf(parts[1].toUpperCase(Locale.ROOT));
String value = parts[2];
parsedConditions.add(new StateCondition(itemName, conditionType, value));
} else {
logger.warn("Malformed condition expression: '{}'", expression);
}
}

return parsedConditions;
} catch (IllegalArgumentException e) {
logger.warn("Cannot parse condition {}. Expected format ITEM_NAME <EQ|NEQ> STATE_VALUE: '{}'", config,
e.getMessage());
return List.of();
}
}

@Override
public ProfileTypeUID getProfileTypeUID() {
return STATE_FILTER_UID;
}

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

@Override
public void onCommandFromItem(Command command) {
callback.handleCommand(command);
}

@Override
public void onCommandFromHandler(Command command) {
callback.sendCommand(command);
}

@Override
public void onStateUpdateFromHandler(State state) {
State resultState = checkCondition(state);
if (resultState != null) {
logger.debug("Received state update from handler: {}, forwarded as {}", state, resultState);
callback.sendUpdate(resultState);
} else {
logger.debug("Received state update from handler: {}, not forwarded to item", state);
}
}

@Nullable
private State checkCondition(State state) {
if (!conditions.isEmpty()) {
boolean allConditionsMet = true;
for (StateCondition condition : conditions) {
logger.debug("Evaluting condition: {}", condition);
try {
Item item = itemRegistry.getItem(condition.itemName);
String itemState = item.getState().toString();

if (!condition.matches(itemState)) {
allConditionsMet = false;
}
} catch (ItemNotFoundException e) {
logger.warn(
"Cannot find item '{}' in registry - check your condition expression - skipping state update",
condition.itemName);
allConditionsMet = false;
}

}
if (allConditionsMet) {
return state;
} else {
return configMismatchState;
}
} else {
logger.warn(
"No configuration defined for StateFilterProfile (check for log messages when instantiating profile) - skipping state update");
}

return null;
}

@Nullable
State parseState(@Nullable String stateString) {
// Quoted strings are parsed as StringType
if (stateString == null) {
return null;
} else if (stateString.startsWith("'") && stateString.endsWith("'")) {
return new StringType(stateString.substring(1, stateString.length() - 1));
} else {
return TypeParser.parseState(acceptedDataTypes, stateString);
}
}

class StateCondition {
String itemName;

ComparisonType comparisonType;
String value;

boolean quoted = false;

public StateCondition(String itemName, ComparisonType comparisonType, String value) {
this.itemName = itemName;
this.comparisonType = comparisonType;
this.value = value;
this.quoted = value.startsWith("'") && value.endsWith("'");
if (quoted) {
this.value = value.substring(1, value.length() - 1);
}
}

public boolean matches(String state) {
switch (comparisonType) {
case EQ:
return state.equals(value);
case NEQ: {
return !state.equals(value);
}
default:
logger.warn("Unknown condition type {}. Expected 'eq' or 'neq' - skipping state update",
comparisonType);
return false;

}
}

enum ComparisonType {
EQ,
NEQ
}

@Override
public String toString() {
return "Condition{itemName='" + itemName + "', comparisonType=" + comparisonType + ", value='" + value
+ "'}'";
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?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:basic-profiles:state-filter">
<parameter name="conditions" type="text" required="true">
<label>Conditions</label>
<description>Comma separated list of expressions on the format ITEM_NAME OPERATOR ITEM_STATE, ie "MyItem EQ OFF". Use
quotes around ITEM_STATE to treat value as string ie "'OFF'".</description>
</parameter>
<parameter name="mismatchState" type="text">
<label>State for filter rejects</label>
<description>State to pass to item instead if conditions are NOT met. Use quotes to treat as `StringType`</description>
</parameter>
</config-description>
</config-description:config-descriptions>
Loading

0 comments on commit dfd7c81

Please sign in to comment.