From 574630205d00c14f985aa9d7c7b2dce1bf9d7eec Mon Sep 17 00:00:00 2001 From: Trinitus01 <58007280+Trinitus01@users.noreply.github.com> Date: Sat, 23 Mar 2024 15:30:47 +0100 Subject: [PATCH 1/5] [notificationsforfiretv] Missing Content-Length (smarthomej#521) (#569) * fixed: missing Content-Length header (BodyPublisher.ofByteArrays JDK 17 issue) Signed-off-by: Tom Blum trinitus01@googlemail.com --- .../internal/NotificationsForFireTVConnection.java | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/bundles/org.smarthomej.binding.notificationsforfiretv/src/main/java/org/smarthomej/binding/notificationsforfiretv/internal/NotificationsForFireTVConnection.java b/bundles/org.smarthomej.binding.notificationsforfiretv/src/main/java/org/smarthomej/binding/notificationsforfiretv/internal/NotificationsForFireTVConnection.java index 2bede8f999..970b8f5f6d 100644 --- a/bundles/org.smarthomej.binding.notificationsforfiretv/src/main/java/org/smarthomej/binding/notificationsforfiretv/internal/NotificationsForFireTVConnection.java +++ b/bundles/org.smarthomej.binding.notificationsforfiretv/src/main/java/org/smarthomej/binding/notificationsforfiretv/internal/NotificationsForFireTVConnection.java @@ -19,6 +19,7 @@ import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; +import java.net.http.HttpRequest.BodyPublisher; import java.net.http.HttpRequest.BodyPublishers; import java.net.http.HttpResponse; import java.net.http.HttpResponse.BodyHandlers; @@ -103,9 +104,15 @@ public void addFilePart(String name, File file) throws IOException { public String send() throws IOException, InterruptedException { byteArrays.add(("--" + boundary + "--").getBytes(StandardCharsets.UTF_8)); + int length = 0; + for (byte[] bytes : byteArrays) { + length += bytes.length; + } + + BodyPublisher bodyPublisher = BodyPublishers.fromPublisher(BodyPublishers.ofByteArrays(byteArrays), length); HttpRequest httpRequest = HttpRequest.newBuilder() - .header("Content-Type", "multipart/form-data;boundary=" + boundary) - .POST(BodyPublishers.ofByteArrays(byteArrays)).uri(uri).build(); + .header("Content-Type", "multipart/form-data;boundary=" + boundary).POST(bodyPublisher).uri(uri) + .build(); HttpResponse response = httpClient.send(httpRequest, BodyHandlers.ofString()); if (response.statusCode() == HttpURLConnection.HTTP_OK) { return response.body(); From c3f27fd8ce56726bdeac287106fdc178402206c2 Mon Sep 17 00:00:00 2001 From: "Jan N. Klug" Date: Sun, 24 Mar 2024 17:34:56 +0100 Subject: [PATCH 2/5] [basicprofiles] State filter profile (#531) * State filter profile Signed-off-by: Arne Seime Co-authored-by: J-N-K (cherry picked from commit 6104110682cc9c1ea6a30ce1f7d08ad473addf3c) Signed-off-by: Jan N. Klug --- .../README.md | 31 +++ .../config/StateFilterProfileConfig.java | 32 +++ .../factory/BasicProfilesFactory.java | 16 +- .../internal/profiles/StateFilterProfile.java | 219 ++++++++++++++++++ .../resources/OH-INF/config/state-filter.xml | 18 ++ .../factory/BasicProfilesFactoryTest.java | 9 +- .../profiles/StateFilterProfileTest.java | 216 +++++++++++++++++ 7 files changed, 535 insertions(+), 6 deletions(-) create mode 100644 bundles/org.smarthomej.transform.basicprofiles/src/main/java/org/smarthomej/transform/basicprofiles/internal/config/StateFilterProfileConfig.java create mode 100644 bundles/org.smarthomej.transform.basicprofiles/src/main/java/org/smarthomej/transform/basicprofiles/internal/profiles/StateFilterProfile.java create mode 100644 bundles/org.smarthomej.transform.basicprofiles/src/main/resources/OH-INF/config/state-filter.xml create mode 100644 bundles/org.smarthomej.transform.basicprofiles/src/test/java/org/smarthomej/transform/basicprofiles/internal/profiles/StateFilterProfileTest.java diff --git a/bundles/org.smarthomej.transform.basicprofiles/README.md b/bundles/org.smarthomej.transform.basicprofiles/README.md index 3d622f5751..7b5e4ee074 100644 --- a/bundles/org.smarthomej.transform.basicprofiles/README.md +++ b/bundles/org.smarthomej.transform.basicprofiles/README.md @@ -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"] +} +``` diff --git a/bundles/org.smarthomej.transform.basicprofiles/src/main/java/org/smarthomej/transform/basicprofiles/internal/config/StateFilterProfileConfig.java b/bundles/org.smarthomej.transform.basicprofiles/src/main/java/org/smarthomej/transform/basicprofiles/internal/config/StateFilterProfileConfig.java new file mode 100644 index 0000000000..77f1c81f0c --- /dev/null +++ b/bundles/org.smarthomej.transform.basicprofiles/src/main/java/org/smarthomej/transform/basicprofiles/internal/config/StateFilterProfileConfig.java @@ -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 = ","; +} diff --git a/bundles/org.smarthomej.transform.basicprofiles/src/main/java/org/smarthomej/transform/basicprofiles/internal/factory/BasicProfilesFactory.java b/bundles/org.smarthomej.transform.basicprofiles/src/main/java/org/smarthomej/transform/basicprofiles/internal/factory/BasicProfilesFactory.java index bed0cb6d86..03b92e8e2f 100644 --- a/bundles/org.smarthomej.transform.basicprofiles/src/main/java/org/smarthomej/transform/basicprofiles/internal/factory/BasicProfilesFactory.java +++ b/bundles/org.smarthomej.transform.basicprofiles/src/main/java/org/smarthomej/transform/basicprofiles/internal/factory/BasicProfilesFactory.java @@ -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; @@ -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; @@ -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") // @@ -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 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 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 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 @@ -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; } diff --git a/bundles/org.smarthomej.transform.basicprofiles/src/main/java/org/smarthomej/transform/basicprofiles/internal/profiles/StateFilterProfile.java b/bundles/org.smarthomej.transform.basicprofiles/src/main/java/org/smarthomej/transform/basicprofiles/internal/profiles/StateFilterProfile.java new file mode 100644 index 0000000000..7cc1f71deb --- /dev/null +++ b/bundles/org.smarthomej.transform.basicprofiles/src/main/java/org/smarthomej/transform/basicprofiles/internal/profiles/StateFilterProfile.java @@ -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> acceptedDataTypes; + + private List 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 parseConditions(@Nullable String config, String separator) { + if (config == null) { + return List.of(); + } + + List 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 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 + + "'}'"; + } + } +} diff --git a/bundles/org.smarthomej.transform.basicprofiles/src/main/resources/OH-INF/config/state-filter.xml b/bundles/org.smarthomej.transform.basicprofiles/src/main/resources/OH-INF/config/state-filter.xml new file mode 100644 index 0000000000..c468dcd9fd --- /dev/null +++ b/bundles/org.smarthomej.transform.basicprofiles/src/main/resources/OH-INF/config/state-filter.xml @@ -0,0 +1,18 @@ + + + + + + + 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'". + + + + State to pass to item instead if conditions are NOT met. Use quotes to treat as `StringType` + + + diff --git a/bundles/org.smarthomej.transform.basicprofiles/src/test/java/org/smarthomej/transform/basicprofiles/internal/factory/BasicProfilesFactoryTest.java b/bundles/org.smarthomej.transform.basicprofiles/src/test/java/org/smarthomej/transform/basicprofiles/internal/factory/BasicProfilesFactoryTest.java index 4e2703d5e7..ea0596bb74 100644 --- a/bundles/org.smarthomej.transform.basicprofiles/src/test/java/org/smarthomej/transform/basicprofiles/internal/factory/BasicProfilesFactoryTest.java +++ b/bundles/org.smarthomej.transform.basicprofiles/src/test/java/org/smarthomej/transform/basicprofiles/internal/factory/BasicProfilesFactoryTest.java @@ -14,7 +14,8 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.collection.IsCollectionWithSize.hasSize; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.when; import java.util.Collection; @@ -29,6 +30,7 @@ import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.quality.Strictness; import org.openhab.core.config.core.Configuration; +import org.openhab.core.items.ItemRegistry; import org.openhab.core.library.types.OnOffType; import org.openhab.core.thing.profiles.ProfileCallback; import org.openhab.core.thing.profiles.ProfileContext; @@ -51,7 +53,7 @@ @NonNullByDefault public class BasicProfilesFactoryTest { - private static final int NUMBER_OF_PROFILES = 8; + private static final int NUMBER_OF_PROFILES = 9; private static final Map PROPERTIES = Map.of(ThresholdStateProfile.PARAM_THRESHOLD, 15, RoundStateProfile.PARAM_SCALE, 2, GenericCommandTriggerProfile.PARAM_EVENTS, "1002,1003", @@ -63,12 +65,13 @@ public class BasicProfilesFactoryTest { private @Mock @NonNullByDefault({}) BundleResolver mockBundleResolver; private @Mock @NonNullByDefault({}) ProfileCallback mockCallback; private @Mock @NonNullByDefault({}) ProfileContext mockContext; + private @Mock @NonNullByDefault({}) ItemRegistry mockItemRegistry; private @NonNullByDefault({}) BasicProfilesFactory profileFactory; @BeforeEach public void setup() { - profileFactory = new BasicProfilesFactory(mockLocalizationService, mockBundleResolver); + profileFactory = new BasicProfilesFactory(mockLocalizationService, mockBundleResolver, mockItemRegistry); when(mockContext.getConfiguration()).thenReturn(CONFIG); } diff --git a/bundles/org.smarthomej.transform.basicprofiles/src/test/java/org/smarthomej/transform/basicprofiles/internal/profiles/StateFilterProfileTest.java b/bundles/org.smarthomej.transform.basicprofiles/src/test/java/org/smarthomej/transform/basicprofiles/internal/profiles/StateFilterProfileTest.java new file mode 100644 index 0000000000..b98bd23b53 --- /dev/null +++ b/bundles/org.smarthomej.transform.basicprofiles/src/test/java/org/smarthomej/transform/basicprofiles/internal/profiles/StateFilterProfileTest.java @@ -0,0 +1,216 @@ +/** + * 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.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.List; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; +import org.openhab.core.config.core.Configuration; +import org.openhab.core.items.Item; +import org.openhab.core.items.ItemNotFoundException; +import org.openhab.core.items.ItemRegistry; +import org.openhab.core.library.items.StringItem; +import org.openhab.core.library.types.OnOffType; +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.types.State; +import org.openhab.core.types.UnDefType; + +/** + * Basic unit tests for {@link StateFilterProfile}. + * + * @author Arne Seime - Initial contribution + */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.WARN) +@NonNullByDefault +public class StateFilterProfileTest { + + private @Mock @NonNullByDefault({}) ProfileCallback mockCallback; + private @Mock @NonNullByDefault({}) ProfileContext mockContext; + private @Mock @NonNullByDefault({}) ItemRegistry mockItemRegistry; + + @BeforeEach + public void setup() { + reset(mockContext); + reset(mockCallback); + } + + @Test + public void testNoConditions() { + when(mockContext.getConfiguration()).thenReturn(new Configuration(Map.of("conditions", ""))); + StateFilterProfile profile = new StateFilterProfile(mockCallback, mockContext, mockItemRegistry); + + State expectation = OnOffType.ON; + profile.onStateUpdateFromHandler(expectation); + verify(mockCallback, times(0)).sendUpdate(eq(expectation)); + } + + @Test + public void testMalformedConditions() { + when(mockContext.getConfiguration()).thenReturn(new Configuration(Map.of("conditions", "ItemName invalid"))); + StateFilterProfile profile = new StateFilterProfile(mockCallback, mockContext, mockItemRegistry); + + State expectation = OnOffType.ON; + profile.onStateUpdateFromHandler(expectation); + verify(mockCallback, times(0)).sendUpdate(eq(expectation)); + } + + @Test + public void testInvalidComparatorConditions() throws ItemNotFoundException { + when(mockContext.getConfiguration()).thenReturn(new Configuration(Map.of("conditions", "ItemName lt Value"))); + StateFilterProfile profile = new StateFilterProfile(mockCallback, mockContext, mockItemRegistry); + when(mockItemRegistry.getItem(any())).thenThrow(ItemNotFoundException.class); + + State expectation = OnOffType.ON; + profile.onStateUpdateFromHandler(expectation); + verify(mockCallback, times(0)).sendUpdate(eq(expectation)); + } + + @Test + public void testInvalidItemConditions() throws ItemNotFoundException { + when(mockContext.getConfiguration()).thenReturn(new Configuration(Map.of("conditions", "ItemName eq Value"))); + StateFilterProfile profile = new StateFilterProfile(mockCallback, mockContext, mockItemRegistry); + + when(mockItemRegistry.getItem(any())).thenThrow(ItemNotFoundException.class); + State expectation = OnOffType.ON; + profile.onStateUpdateFromHandler(expectation); + verify(mockCallback, times(0)).sendUpdate(eq(expectation)); + } + + @Test + public void testInvalidMultipleConditions() throws ItemNotFoundException { + when(mockContext.getConfiguration()) + .thenReturn(new Configuration(Map.of("conditions", "ItemName eq Value,itemname invalid"))); + StateFilterProfile profile = new StateFilterProfile(mockCallback, mockContext, mockItemRegistry); + when(mockItemRegistry.getItem(any())).thenThrow(ItemNotFoundException.class); + + State expectation = OnOffType.ON; + profile.onStateUpdateFromHandler(expectation); + verify(mockCallback, times(0)).sendUpdate(eq(expectation)); + } + + @Test + public void testSingleConditionMatch() throws ItemNotFoundException { + when(mockContext.getConfiguration()).thenReturn(new Configuration(Map.of("conditions", "ItemName eq Value"))); + when(mockItemRegistry.getItem("ItemName")).thenReturn(stringItemWithState("ItemName", "Value")); + + StateFilterProfile profile = new StateFilterProfile(mockCallback, mockContext, mockItemRegistry); + + State expectation = new StringType("NewValue"); + profile.onStateUpdateFromHandler(expectation); + verify(mockCallback, times(1)).sendUpdate(eq(expectation)); + } + + @Test + public void testSingleConditionMatchQuoted() throws ItemNotFoundException { + when(mockContext.getConfiguration()).thenReturn(new Configuration(Map.of("conditions", "ItemName eq 'Value'"))); + when(mockItemRegistry.getItem("ItemName")).thenReturn(stringItemWithState("ItemName", "Value")); + + StateFilterProfile profile = new StateFilterProfile(mockCallback, mockContext, mockItemRegistry); + + State expectation = new StringType("NewValue"); + profile.onStateUpdateFromHandler(expectation); + verify(mockCallback, times(1)).sendUpdate(eq(expectation)); + } + + private Item stringItemWithState(String itemName, String value) { + StringItem item = new StringItem(itemName); + item.setState(new StringType(value)); + return item; + } + + @Test + public void testMultipleCondition_AllMatch() throws ItemNotFoundException { + when(mockContext.getConfiguration()) + .thenReturn(new Configuration(Map.of("conditions", "ItemName eq Value, ItemName2 eq Value2"))); + when(mockItemRegistry.getItem("ItemName")).thenReturn(stringItemWithState("ItemName", "Value")); + when(mockItemRegistry.getItem("ItemName2")).thenReturn(stringItemWithState("ItemName2", "Value2")); + + StateFilterProfile profile = new StateFilterProfile(mockCallback, mockContext, mockItemRegistry); + + State expectation = new StringType("NewValue"); + profile.onStateUpdateFromHandler(expectation); + verify(mockCallback, times(1)).sendUpdate(eq(expectation)); + } + + @Test + public void testMultipleCondition_SingleMatch() throws ItemNotFoundException { + when(mockContext.getConfiguration()) + .thenReturn(new Configuration(Map.of("conditions", "ItemName eq Value, ItemName2 eq Value2"))); + when(mockItemRegistry.getItem("ItemName")).thenReturn(stringItemWithState("ItemName", "Value")); + when(mockItemRegistry.getItem("ItemName2")).thenThrow(ItemNotFoundException.class); + + StateFilterProfile profile = new StateFilterProfile(mockCallback, mockContext, mockItemRegistry); + + State expectation = new StringType("NewValue"); + profile.onStateUpdateFromHandler(expectation); + verify(mockCallback, times(0)).sendUpdate(eq(expectation)); + } + + @Test + public void testFailingConditionWithMismatchState() throws ItemNotFoundException { + when(mockContext.getConfiguration()) + .thenReturn(new Configuration(Map.of("conditions", "ItemName eq Value", "mismatchState", "UNDEF"))); + when(mockContext.getAcceptedDataTypes()).thenReturn(List.of(UnDefType.class, StringType.class)); + when(mockItemRegistry.getItem("ItemName")).thenReturn(stringItemWithState("ItemName", "Mismatch")); + + StateFilterProfile profile = new StateFilterProfile(mockCallback, mockContext, mockItemRegistry); + + profile.onStateUpdateFromHandler(new StringType("ToBeDiscarded")); + verify(mockCallback, times(1)).sendUpdate(eq(UnDefType.UNDEF)); + } + + @Test + public void testFailingConditionWithMismatchStateQuoted() throws ItemNotFoundException { + when(mockContext.getConfiguration()) + .thenReturn(new Configuration(Map.of("conditions", "ItemName eq Value", "mismatchState", "'UNDEF'"))); + when(mockContext.getAcceptedDataTypes()).thenReturn(List.of(UnDefType.class, StringType.class)); + when(mockItemRegistry.getItem("ItemName")).thenReturn(stringItemWithState("ItemName", "Mismatch")); + + StateFilterProfile profile = new StateFilterProfile(mockCallback, mockContext, mockItemRegistry); + + profile.onStateUpdateFromHandler(new StringType("ToBeDiscarded")); + verify(mockCallback, times(1)).sendUpdate(eq(new StringType("UNDEF"))); + } + + @Test + void testParseStateNonQuotes() { + when(mockContext.getAcceptedDataTypes()) + .thenReturn(List.of(UnDefType.class, OnOffType.class, StringType.class)); + when(mockContext.getConfiguration()).thenReturn(new Configuration(Map.of("conditions", ""))); + + StateFilterProfile profile = new StateFilterProfile(mockCallback, mockContext, mockItemRegistry); + assertEquals(UnDefType.UNDEF, profile.parseState("UNDEF")); + assertEquals(new StringType("UNDEF"), profile.parseState("'UNDEF'")); + assertEquals(OnOffType.ON, profile.parseState("ON")); + assertEquals(new StringType("ON"), profile.parseState("'ON'")); + } +} From a231f9a7142f31dc3dc3aee808c72e465929c513 Mon Sep 17 00:00:00 2001 From: J-N-K Date: Sun, 24 Mar 2024 17:40:42 +0100 Subject: [PATCH 3/5] [amazonechocontrol] Fix humidity sensor response (#572) Reported on the forum Signed-off-by: Jan N. Klug --- .../smarthome/HandlerHumiditySensor.java | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/bundles/org.smarthomej.binding.amazonechocontrol/src/main/java/org/smarthomej/binding/amazonechocontrol/internal/smarthome/HandlerHumiditySensor.java b/bundles/org.smarthomej.binding.amazonechocontrol/src/main/java/org/smarthomej/binding/amazonechocontrol/internal/smarthome/HandlerHumiditySensor.java index ee2e94b563..694ed792c1 100644 --- a/bundles/org.smarthomej.binding.amazonechocontrol/src/main/java/org/smarthomej/binding/amazonechocontrol/internal/smarthome/HandlerHumiditySensor.java +++ b/bundles/org.smarthomej.binding.amazonechocontrol/src/main/java/org/smarthomej/binding/amazonechocontrol/internal/smarthome/HandlerHumiditySensor.java @@ -27,11 +27,14 @@ import org.openhab.core.library.unit.Units; import org.openhab.core.types.Command; import org.openhab.core.types.UnDefType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.smarthomej.binding.amazonechocontrol.internal.connection.Connection; import org.smarthomej.binding.amazonechocontrol.internal.dto.smarthome.JsonSmartHomeCapability; import org.smarthomej.binding.amazonechocontrol.internal.dto.smarthome.JsonSmartHomeDevice; import org.smarthomej.binding.amazonechocontrol.internal.handler.SmartHomeDeviceHandler; +import com.google.gson.JsonElement; import com.google.gson.JsonObject; /** @@ -41,6 +44,7 @@ */ @NonNullByDefault public class HandlerHumiditySensor extends AbstractInterfaceHandler { + private final Logger logger = LoggerFactory.getLogger(HandlerHumiditySensor.class); public static final String INTERFACE = "Alexa.HumiditySensor"; private static final ChannelInfo HUMIDITY = new ChannelInfo("relativeHumidity", "humidity", @@ -62,13 +66,18 @@ protected Set findChannelInfos(JsonSmartHomeCapability capability, public void updateChannels(String interfaceName, List stateList, UpdateChannelResult result) { QuantityType humidityValue = null; for (JsonObject state : stateList) { - if (HUMIDITY.propertyName.equals(state.get("name").getAsString())) { - JsonObject value = state.get("value").getAsJsonObject(); - // For groups take the first - if (humidityValue == null) { - BigDecimal humidity = value.get("value").getAsBigDecimal(); - humidityValue = new QuantityType<>(humidity, Units.PERCENT); + if (HUMIDITY.propertyName.equals(state.get("name").getAsString()) && humidityValue == null) { + JsonElement value = state.get("value"); + BigDecimal humidity; + if (value.isJsonObject()) { + humidity = value.getAsJsonObject().getAsBigDecimal(); + } else if (value.isJsonPrimitive() && value.getAsJsonPrimitive().isNumber()) { + humidity = value.getAsJsonPrimitive().getAsBigDecimal(); + } else { + logger.warn("Could not properly convert {}", state); + continue; } + humidityValue = new QuantityType<>(humidity, Units.PERCENT); } } smartHomeDeviceHandler.updateState(HUMIDITY.channelId, humidityValue == null ? UnDefType.UNDEF : humidityValue); From 3761356e3a452089e6f440d6a782b48f59e1c284 Mon Sep 17 00:00:00 2001 From: "Jan N. Klug" Date: Sat, 18 May 2024 20:44:15 +0200 Subject: [PATCH 4/5] [amazonechocontrol] Fix SecurityPanel (#574) Signed-off-by: Jan N. Klug (cherry picked from commit f77345bb6c263b64964dd062b843ee5985dcaa49) Signed-off-by: Jan N. Klug --- .../HandlerSecurityPanelController.java | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/bundles/org.smarthomej.binding.amazonechocontrol/src/main/java/org/smarthomej/binding/amazonechocontrol/internal/smarthome/HandlerSecurityPanelController.java b/bundles/org.smarthomej.binding.amazonechocontrol/src/main/java/org/smarthomej/binding/amazonechocontrol/internal/smarthome/HandlerSecurityPanelController.java index 2734fee861..af650d458e 100644 --- a/bundles/org.smarthomej.binding.amazonechocontrol/src/main/java/org/smarthomej/binding/amazonechocontrol/internal/smarthome/HandlerSecurityPanelController.java +++ b/bundles/org.smarthomej.binding.amazonechocontrol/src/main/java/org/smarthomej/binding/amazonechocontrol/internal/smarthome/HandlerSecurityPanelController.java @@ -85,25 +85,27 @@ public void updateChannels(String interfaceName, List stateList, Upd Boolean fireAlarmValue = null; Boolean waterAlarmValue = null; for (JsonObject state : stateList) { - if (ARM_STATE.propertyName.equals(state.get("name").getAsString())) { + String propertyValue = state.get("value").getAsJsonObject().get("value").getAsString(); + String propertyName = state.get("name").getAsString(); + if (ARM_STATE.propertyName.equals(propertyName)) { if (armStateValue == null) { armStateValue = state.get("value").getAsString(); } - } else if (BURGLARY_ALARM.propertyName.equals(state.get("name").getAsString())) { + } else if (BURGLARY_ALARM.propertyName.equals(propertyName)) { if (burglaryAlarmValue == null) { - burglaryAlarmValue = "ALARM".equals(state.get("value").getAsString()); + burglaryAlarmValue = "ALARM".equals(propertyValue); } - } else if (CARBON_MONOXIDE_ALARM.propertyName.equals(state.get("name").getAsString())) { + } else if (CARBON_MONOXIDE_ALARM.propertyName.equals(propertyName)) { if (carbonMonoxideAlarmValue == null) { - carbonMonoxideAlarmValue = "ALARM".equals(state.get("value").getAsString()); + carbonMonoxideAlarmValue = "ALARM".equals(propertyValue); } - } else if (FIRE_ALARM.propertyName.equals(state.get("name").getAsString())) { + } else if (FIRE_ALARM.propertyName.equals(propertyName)) { if (fireAlarmValue == null) { - fireAlarmValue = "ALARM".equals(state.get("value").getAsString()); + fireAlarmValue = "ALARM".equals(propertyValue); } - } else if (WATER_ALARM.propertyName.equals(state.get("name").getAsString())) { + } else if (WATER_ALARM.propertyName.equals(propertyName)) { if (waterAlarmValue == null) { - waterAlarmValue = "ALARM".equals(state.get("value").getAsString()); + waterAlarmValue = "ALARM".equals(propertyValue); } } } From b79bf4db729a8945f9e17b7f6c8cd5746d8be2fa Mon Sep 17 00:00:00 2001 From: "Jan N. Klug" Date: Wed, 5 Jun 2024 21:03:51 +0200 Subject: [PATCH 5/5] [tuya] Fix compiler warnings and simplify code (#563) Signed-off-by: Jan N. Klug (cherry picked from commit 55af4c8a8b16148562d81b3c2ad0c90b16ca22ee) Signed-off-by: Jan N. Klug --- bundles/org.smarthomej.binding.tuya/README.md | 4 + .../internal/config/ChannelConfiguration.java | 2 +- .../internal/handler/TuyaDeviceHandler.java | 101 +++++----- .../tuya/internal/local/dto/IrCode.java | 2 +- .../binding/tuya/internal/util/IrUtils.java | 172 ++++++------------ 5 files changed, 105 insertions(+), 176 deletions(-) diff --git a/bundles/org.smarthomej.binding.tuya/README.md b/bundles/org.smarthomej.binding.tuya/README.md index 2ce05814a5..c79816ad0d 100644 --- a/bundles/org.smarthomej.binding.tuya/README.md +++ b/bundles/org.smarthomej.binding.tuya/README.md @@ -122,6 +122,7 @@ It contains a comma-separated list of command options for this channel (e.g. `wh ### Type `ir-code` IR code types: + + `Tuya DIY-mode` - use study codes from real remotes. Make a virtual remote control in DIY, learn virtual buttons. @@ -139,6 +140,7 @@ IR code types: + `Samsung` - IR Code in Samsung format. **Additional options:** + * `Active Listening` - Device will be always in learning mode. After send command with key code device stays in the learning mode * `DP Study Key` - **Advanced**. DP number for study key. Uses for receive key code in learning mode. Change it own your @@ -150,11 +152,13 @@ If linked item received a command with `Key Code` (Code Library Parameter) then #### How to use IR Code in NEC format. Example, from Tasmota you need to use **_Data_** parameter, it can be with or without **_0x_** + ```json {"Time": "2023-07-05T18:17:42", "IrReceived": {"Protocol": "NEC", "Bits": 32, "Data": "0x10EFD02F"}} ``` Another example, use **_hex_** parameter + ```json { "type": "nec", "uint32": 284151855, "address": 8, "data": 11, "hex": "10EFD02F" } ``` diff --git a/bundles/org.smarthomej.binding.tuya/src/main/java/org/smarthomej/binding/tuya/internal/config/ChannelConfiguration.java b/bundles/org.smarthomej.binding.tuya/src/main/java/org/smarthomej/binding/tuya/internal/config/ChannelConfiguration.java index 5b216403e7..cb4b33b73b 100644 --- a/bundles/org.smarthomej.binding.tuya/src/main/java/org/smarthomej/binding/tuya/internal/config/ChannelConfiguration.java +++ b/bundles/org.smarthomej.binding.tuya/src/main/java/org/smarthomej/binding/tuya/internal/config/ChannelConfiguration.java @@ -32,5 +32,5 @@ public class ChannelConfiguration { public int irSendDelay = 300; public int irCodeType = 0; public String irType = ""; - public Boolean activeListen = Boolean.FALSE; + public boolean activeListen = false; } diff --git a/bundles/org.smarthomej.binding.tuya/src/main/java/org/smarthomej/binding/tuya/internal/handler/TuyaDeviceHandler.java b/bundles/org.smarthomej.binding.tuya/src/main/java/org/smarthomej/binding/tuya/internal/handler/TuyaDeviceHandler.java index 1dff20a348..1321762ab7 100644 --- a/bundles/org.smarthomej.binding.tuya/src/main/java/org/smarthomej/binding/tuya/internal/handler/TuyaDeviceHandler.java +++ b/bundles/org.smarthomej.binding.tuya/src/main/java/org/smarthomej/binding/tuya/internal/handler/TuyaDeviceHandler.java @@ -148,17 +148,16 @@ public void processDeviceStatus(Map deviceStatus) { private void processChannelStatus(Integer dp, Object value) { String channelId = dpToChannelId.get(dp); if (channelId != null) { - ChannelConfiguration configuration = channelIdToConfiguration.get(channelId); + ChannelConfiguration channelConfiguration = channelIdToConfiguration.get(channelId); ChannelTypeUID channelTypeUID = channelIdToChannelTypeUID.get(channelId); - if (configuration == null || channelTypeUID == null) { + if (channelConfiguration == null || channelTypeUID == null) { logger.warn("Could not find configuration or type for channel '{}' in thing '{}'", channelId, thing.getUID()); return; } - ChannelConfiguration channelConfiguration = channelIdToConfiguration.get(channelId); - if (channelConfiguration != null && Boolean.FALSE.equals(deviceStatusCache.get(channelConfiguration.dp2))) { + if (Boolean.FALSE.equals(deviceStatusCache.get(channelConfiguration.dp2))) { // skip update if the channel is off! return; } @@ -173,7 +172,8 @@ private void processChannelStatus(Integer dp, Object value) { return; } else if (Double.class.isAssignableFrom(value.getClass()) && CHANNEL_TYPE_UID_DIMMER.equals(channelTypeUID)) { - updateState(channelId, ConversionUtil.brightnessDecode((double) value, 0, configuration.max)); + updateState(channelId, + ConversionUtil.brightnessDecode((double) value, 0, channelConfiguration.max)); return; } else if (Double.class.isAssignableFrom(value.getClass()) && CHANNEL_TYPE_UID_NUMBER.equals(channelTypeUID)) { @@ -186,11 +186,11 @@ private void processChannelStatus(Integer dp, Object value) { updateState(channelId, OnOffType.from((boolean) value)); return; } else if (value instanceof String && CHANNEL_TYPE_UID_IR_CODE.equals(channelTypeUID)) { - if (configuration.dp == 2) { - String decoded = convertBase64Code(configuration, (String) value); + if (channelConfiguration.dp == 2) { + String decoded = convertBase64Code(channelConfiguration, (String) value); logger.info("thing {} received ir code: {}", thing.getUID(), decoded); updateState(channelId, new StringType(decoded)); - irStartLearning(configuration.activeListen); + irStartLearning(channelConfiguration.activeListen); } return; } @@ -228,14 +228,9 @@ public void connectionStatus(boolean status) { } // start learning code if thing is online and presents 'ir-code' channel - this.getThing().getChannels().stream() - .filter(channel -> CHANNEL_TYPE_UID_IR_CODE.equals(channel.getChannelTypeUID())).findFirst() - .ifPresent(channel -> { - ChannelConfiguration config = channelIdToConfiguration.get(channel.getChannelTypeUID()); - if (config != null) { - irStartLearning(config.activeListen); - } - }); + channelIdToChannelTypeUID.entrySet().stream().filter(e -> CHANNEL_TYPE_UID_IR_CODE.equals(e.getValue())) + .map(Map.Entry::getKey).findAny().map(channelIdToConfiguration::get) + .ifPresent(irCodeChannelConfig -> irStartLearning(irCodeChannelConfig.activeListen)); } else { updateStatus(ThingStatus.OFFLINE); ScheduledFuture pollingJob = this.pollingJob; @@ -250,7 +245,9 @@ public void connectionStatus(boolean status) { if (tuyaDevice != null && !disposing && (reconnectFuture == null || reconnectFuture.isDone())) { this.reconnectFuture = scheduler.schedule(this::connectDevice, 5000, TimeUnit.MILLISECONDS); } - irStopLearning(); + if (channelIdToChannelTypeUID.containsValue(CHANNEL_TYPE_UID_IR_CODE)) { + irStopLearning(); + } } } @@ -338,29 +335,34 @@ public void handleCommand(ChannelUID channelUID, Command command) { } } else if (CHANNEL_TYPE_UID_IR_CODE.equals(channelTypeUID)) { if (command instanceof StringType) { - if (configuration.irType.equals("base64")) { - commandRequest.put(1, "study_key"); - commandRequest.put(7, command.toString()); - } else if (configuration.irType.equals("tuya-head")) { - if (configuration.irCode != null && !configuration.irCode.isEmpty()) { - commandRequest.put(1, "send_ir"); - commandRequest.put(3, configuration.irCode); - commandRequest.put(4, command.toString()); - commandRequest.put(10, configuration.irSendDelay); - commandRequest.put(13, configuration.irCodeType); - } else { - logger.warn("irCode is not set for channel {}", channelUID); + switch (configuration.irType) { + case "base64" -> { + commandRequest.put(1, "study_key"); + commandRequest.put(7, command.toString()); + } + case "tuya-head" -> { + if (!configuration.irCode.isBlank()) { + commandRequest.put(1, "send_ir"); + commandRequest.put(3, configuration.irCode); + commandRequest.put(4, command.toString()); + commandRequest.put(10, configuration.irSendDelay); + commandRequest.put(13, configuration.irCodeType); + } else { + logger.warn("irCode is not set for channel {}", channelUID); + } + } + case "nec" -> { + long code = convertHexCode(command.toString()); + String base64Code = IrUtils.necToBase64(code); + commandRequest.put(1, "study_key"); + commandRequest.put(7, base64Code); + } + case "samsung" -> { + long code = convertHexCode(command.toString()); + String base64Code = IrUtils.samsungToBase64(code); + commandRequest.put(1, "study_key"); + commandRequest.put(7, base64Code); } - } else if (configuration.irType.equals("nec")) { - long code = convertHexCode(command.toString()); - String base64Code = IrUtils.necToBase64(code); - commandRequest.put(1, "study_key"); - commandRequest.put(7, base64Code); - } else if (configuration.irType.equals("samsung")) { - long code = convertHexCode(command.toString()); - String base64Code = IrUtils.samsungToBase64(code); - commandRequest.put(1, "study_key"); - commandRequest.put(7, base64Code); } irStopLearning(); } @@ -610,7 +612,7 @@ private String convertBase64Code(ChannelConfiguration channelConfig, String enco } else { if (encoded.length() > 68) { decoded = IrUtils.base64ToNec(encoded); - if (decoded == null || decoded.isEmpty()) { + if (decoded.isEmpty()) { decoded = IrUtils.base64ToSamsung(encoded); } IrCode code = Objects.requireNonNull(gson.fromJson(decoded, IrCode.class)); @@ -620,28 +622,19 @@ private String convertBase64Code(ChannelConfiguration channelConfig, String enco } } } catch (JsonSyntaxException e) { - logger.error("Incorrect json response: {}", e.getMessage()); + logger.warn("Incorrect json response: {}", e.getMessage()); decoded = encoded; - } catch (NullPointerException e) { - logger.error("unable decode key code'{}', reason: {}", decoded, e.getMessage()); + } catch (RuntimeException e) { + logger.warn("Unable decode key code'{}', reason: {}", decoded, e.getMessage()); } return decoded; } - private void finishStudyCode() { - Map commandRequest = new HashMap<>(); - commandRequest.put(1, "study_exit"); - TuyaDevice tuyaDevice = this.tuyaDevice; - if (!commandRequest.isEmpty() && tuyaDevice != null) { - tuyaDevice.set(commandRequest); - } - } - private void repeatStudyCode() { Map commandRequest = new HashMap<>(); commandRequest.put(1, "study"); TuyaDevice tuyaDevice = this.tuyaDevice; - if (!commandRequest.isEmpty() && tuyaDevice != null) { + if (tuyaDevice != null) { tuyaDevice.set(commandRequest); } } @@ -655,7 +648,7 @@ private void irStopLearning() { } } - private void irStartLearning(Boolean available) { + private void irStartLearning(boolean available) { irStopLearning(); if (available) { logger.debug("[tuya:ir-controller] start ir learning"); diff --git a/bundles/org.smarthomej.binding.tuya/src/main/java/org/smarthomej/binding/tuya/internal/local/dto/IrCode.java b/bundles/org.smarthomej.binding.tuya/src/main/java/org/smarthomej/binding/tuya/internal/local/dto/IrCode.java index 83ced8e9ac..3b1cf497de 100644 --- a/bundles/org.smarthomej.binding.tuya/src/main/java/org/smarthomej/binding/tuya/internal/local/dto/IrCode.java +++ b/bundles/org.smarthomej.binding.tuya/src/main/java/org/smarthomej/binding/tuya/internal/local/dto/IrCode.java @@ -12,7 +12,7 @@ */ package org.smarthomej.binding.tuya.internal.local.dto; -import org.eclipse.jdt.annotation.*; +import org.eclipse.jdt.annotation.NonNullByDefault; /** * The {@link IrCode} represents the IR code decoded messages sent by Tuya devices diff --git a/bundles/org.smarthomej.binding.tuya/src/main/java/org/smarthomej/binding/tuya/internal/util/IrUtils.java b/bundles/org.smarthomej.binding.tuya/src/main/java/org/smarthomej/binding/tuya/internal/util/IrUtils.java index b786a85ef6..be08741624 100644 --- a/bundles/org.smarthomej.binding.tuya/src/main/java/org/smarthomej/binding/tuya/internal/util/IrUtils.java +++ b/bundles/org.smarthomej.binding.tuya/src/main/java/org/smarthomej/binding/tuya/internal/util/IrUtils.java @@ -17,6 +17,7 @@ import java.util.Base64; import java.util.List; +import org.eclipse.jdt.annotation.NonNullByDefault; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -27,6 +28,7 @@ * * @author Dmitry Pyatykh - Initial contribution */ +@NonNullByDefault public class IrUtils { private static final Logger logger = LoggerFactory.getLogger(IrUtils.class); @@ -40,15 +42,14 @@ private IrUtils() { * @return the nec-format code */ public static String base64ToNec(String base64Code) { - String result = null; List pulses = base64ToPulse(base64Code); - if (pulses != null && !pulses.isEmpty()) { + if (!pulses.isEmpty()) { List res = pulsesToNec(pulses); - if (res != null && !res.isEmpty()) { - result = res.get(0); + if (!res.isEmpty()) { + return res.get(0); } } - return result; + throw new IllegalArgumentException("No pulses found or conversion result is empty."); } /** @@ -58,15 +59,14 @@ public static String base64ToNec(String base64Code) { * @return the samsung-format code */ public static String base64ToSamsung(String base64Code) { - String result = null; List pulses = base64ToPulse(base64Code); - if (pulses != null && !pulses.isEmpty()) { + if (!pulses.isEmpty()) { List res = pulsesToSamsung(pulses); - if (res != null && !res.isEmpty()) { - result = res.get(0); + if (!res.isEmpty()) { + return res.get(0); } } - return result; + throw new IllegalArgumentException("No pulses found or conversion result is empty."); } private static List base64ToPulse(String base64Code) { @@ -88,78 +88,46 @@ private static List base64ToPulse(String base64Code) { } } } catch (ArrayIndexOutOfBoundsException e) { - logger.error("Failed to convert base64 key code to pulses: {}", e.getMessage()); + logger.warn("Failed to convert base64 key code to pulses: {}", e.getMessage()); } return pulses; } - private static List pulsesToWidthEncoded(List pulses, Integer startMark, Integer startSpace, - Integer pulseThreshold, Integer spaceThreshold) { + private static List pulsesToWidthEncoded(List pulses, Integer startMark) { List ret = new ArrayList<>(); if (pulses.size() < 68) { - return null; + throw new IllegalArgumentException("Not enough pulses"); } - if (pulseThreshold == null && spaceThreshold == null) { - return null; + while (pulses.size() >= 68 && (pulses.get(0) < (startMark * 0.75) || pulses.get(0) > (startMark * 1.25))) { + pulses.remove(0); } - if (startMark != null) { - while (pulses.size() >= 68 && (pulses.get(0) < (startMark * 0.75) || pulses.get(0) > (startMark * 1.25))) { - pulses.remove(0); + while (pulses.size() >= 68) { + if (pulses.get(0) < startMark * 0.75 || pulses.get(0) > startMark * 1.25) { + throw new IllegalArgumentException( + "Pulse length is less than 3/4 startMark or more than 5/4 startMark"); } - while (pulses.size() >= 68) { - if (pulses.get(0) < startMark * 0.75 || pulses.get(0) > startMark * 1.25) { - return null; - } + // remove two first elements + pulses.remove(0); + pulses.remove(0); - if (startSpace != null - && (pulses.get(1) < (startSpace * 0.75) || pulses.get(1) > (startSpace * 1.25))) { - return null; - } + int res = 0; + long x = 0L; + + for (int i = 31; i >= 0; i--) { + res = pulses.get(1) >= (Integer) 1125 ? 1 : 0; + + x |= (long) (res) << i; // remove two first elements pulses.remove(0); pulses.remove(0); + } - Integer res = 0; - long x = 0L; - - for (int i = 31; i >= 0; i--) { - Integer pulseMatch = null; - Integer spaceMatch = null; - - if (pulseThreshold != null) { - pulseMatch = pulses.get(0) >= pulseThreshold ? 1 : 0; - } - if (spaceThreshold != null) { - spaceMatch = pulses.get(1) >= spaceThreshold ? 1 : 0; - } - - if (pulseMatch != null && spaceMatch != null) { - if (!pulseMatch.equals(spaceMatch)) { - return null; - } - res = spaceMatch; - } else if (pulseMatch == null) { - res = spaceMatch; - } else { - res = pulseMatch; - } - - if (res != null) { - x |= (long) (res) << i; - } - - // remove two first elements - pulses.remove(0); - pulses.remove(0); - } - - if (!ret.contains(x)) { - ret.add(x); - } + if (!ret.contains(x)) { + ret.add(x); } } @@ -172,7 +140,7 @@ private static List widthEncodedToPulses(long data, PulseParams param) { pulses.add(param.startSpace); for (int i = 31; i >= 0; i--) { - if ((data & (1 << i)) > 0L) { + if ((data & (1L << i)) > 0L) { pulses.add(param.pulseOne); pulses.add(param.spaceOne); } else { @@ -185,11 +153,11 @@ private static List widthEncodedToPulses(long data, PulseParams param) { return pulses; } - private static long mirrorBits(long data, int bits) { - int shift = bits - 1; + private static long mirrorBits(long data) { + int shift = 8 - 1; long out = 0; - for (int i = 0; i < bits; i++) { + for (int i = 0; i < 8; i++) { if ((data & (1L << i)) > 0L) { out |= 1L << shift; } @@ -200,16 +168,15 @@ private static long mirrorBits(long data, int bits) { private static List pulsesToNec(List pulses) { List ret = new ArrayList<>(); - List res = pulsesToWidthEncoded(pulses, 9000, null, null, 1125); - if (res == null || res.isEmpty()) { - logger.warn("[tuya:ir-controller] No ir key-code detected"); - return null; + List res = pulsesToWidthEncoded(pulses, 9000); + if (res.isEmpty()) { + throw new IllegalArgumentException("[tuya:ir-controller] No ir key-code detected"); } for (Long code : res) { - long addr = mirrorBits((code >> 24) & 0xFF, 8); - long addrNot = mirrorBits((code >> 16) & 0xFF, 8); - long data = mirrorBits((code >> 8) & 0xFF, 8); - long dataNot = mirrorBits(code & 0xFF, 8); + long addr = mirrorBits((code >> 24) & 0xFF); + long addrNot = mirrorBits((code >> 16) & 0xFF); + long data = mirrorBits((code >> 8) & 0xFF); + long dataNot = mirrorBits(code & 0xFF); if (addr != (addrNot ^ 0xFF)) { addr = (addr << 8) | addrNot; @@ -227,34 +194,8 @@ private static List pulsesToNec(List pulses) { return ret; } - private static String bytesToHex(byte[] bytes) { - final char[] HEX_ARRAY = "0123456789ABCDEF".toCharArray(); - char[] hexChars = new char[bytes.length * 2]; - for (int j = 0; j < bytes.length; j++) { - int v = bytes[j] & 0xFF; - hexChars[j * 2] = HEX_ARRAY[v >>> 4]; - hexChars[j * 2 + 1] = HEX_ARRAY[v & 0x0F]; - } - return new String(hexChars); - } - - private static List necToPulses(long address, Long data) { - Long newAddress, newData; - if (data == null) { - newAddress = address; - } else { - if (address < 256) { - newAddress = mirrorBits(address, 8); - newAddress = (newAddress << 8) | (newAddress ^ 0xFF); - } else { - newAddress = (mirrorBits((address >> 8) & 0xFF, 8) << 8) | mirrorBits(address & 0xFF, 8); - } - newData = mirrorBits(data, 8); - newData = (newData << 8) | (newData & 0xFF); - newAddress = (newAddress << 16) | newData; - } - - return widthEncodedToPulses(newAddress, new PulseParams()); + private static List necToPulses(long address) { + return widthEncodedToPulses(address, new PulseParams()); } private static String pulsesToBase64(List pulses) { @@ -279,7 +220,7 @@ private static String pulsesToBase64(List pulses) { * @return the string */ public static String necToBase64(long code) { - List pulses = necToPulses(code, null); + List pulses = necToPulses(code); return pulsesToBase64(pulses); } @@ -290,26 +231,17 @@ public static String necToBase64(long code) { * @return the string */ public static String samsungToBase64(long code) { - List pulses = samsungToPulses(code, null); + List pulses = samsungToPulses(code); return pulsesToBase64(pulses); } - private static List samsungToPulses(long address, Long data) { - Long newAddress, newData; - if (data == null) { - newAddress = address; - } else { - newAddress = mirrorBits(address, 8); - newData = mirrorBits(data, 8); - newData = (newData << 8) | (newData & 0xFF); - newAddress = (newAddress << 24) + (newAddress << 16) + (newData << 8) + (newData ^ 0xFF); - } - return widthEncodedToPulses(newAddress, new PulseParams()); + private static List samsungToPulses(long address) { + return widthEncodedToPulses(address, new PulseParams()); } private static List pulsesToSamsung(List pulses) { List ret = new ArrayList<>(); - List res = pulsesToWidthEncoded(pulses, 4500, null, null, 1125); + List res = pulsesToWidthEncoded(pulses, 4500); for (Long code : res) { long addr = (code >> 24) & 0xFF; long addrNot = (code >> 16) & 0xFF; @@ -320,8 +252,8 @@ private static List pulsesToSamsung(List pulses) { "{ \"type\": \"samsung\", \"uint32\": %d, \"address\": None, \"data\": None, \"hex\": \"%08X\" }", code, code); if (addr == addrNot && data == (dataNot ^ 0xFF)) { - addr = mirrorBits(addr, 8); - data = mirrorBits(data, 8); + addr = mirrorBits(addr); + data = mirrorBits(data); d = String.format( "{ \"type\": \"samsung\", \"uint32\": %d, \"address\": %d, \"data\": %d, \"hex\": \"%08X\" }", code, addr, data, code);