diff --git a/bundles/org.connectorio.addons.profile.counter/src/main/java/org/connectorio/addons/profile/counter/internal/state/DummyStateRetriever.java b/bundles/org.connectorio.addons.profile.counter/src/main/java/org/connectorio/addons/profile/counter/internal/state/DummyStateRetriever.java index e14c5aa4..134af61a 100644 --- a/bundles/org.connectorio.addons.profile.counter/src/main/java/org/connectorio/addons/profile/counter/internal/state/DummyStateRetriever.java +++ b/bundles/org.connectorio.addons.profile.counter/src/main/java/org/connectorio/addons/profile/counter/internal/state/DummyStateRetriever.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019-2021 ConnectorIO Sp. z o.o. + * Copyright (C) 2024-2024 ConnectorIO Sp. z o.o. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/bundles/org.connectorio.addons.profile.counter/src/test/java/org/connectorio/addons/profile/counter/internal/CollectorProfileTest.java b/bundles/org.connectorio.addons.profile.counter/src/test/java/org/connectorio/addons/profile/counter/internal/CollectorProfileTest.java index af91620f..df8f4da7 100644 --- a/bundles/org.connectorio.addons.profile.counter/src/test/java/org/connectorio/addons/profile/counter/internal/CollectorProfileTest.java +++ b/bundles/org.connectorio.addons.profile.counter/src/test/java/org/connectorio/addons/profile/counter/internal/CollectorProfileTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022-2022 ConnectorIO Sp. z o.o. + * Copyright (C) 2024-2024 ConnectorIO Sp. z o.o. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/bundles/org.connectorio.addons.profile.counter/src/test/java/org/connectorio/addons/profile/counter/internal/EnergyCounterProfileTest.java b/bundles/org.connectorio.addons.profile.counter/src/test/java/org/connectorio/addons/profile/counter/internal/EnergyCounterProfileTest.java index f645c4b5..fb097cb7 100644 --- a/bundles/org.connectorio.addons.profile.counter/src/test/java/org/connectorio/addons/profile/counter/internal/EnergyCounterProfileTest.java +++ b/bundles/org.connectorio.addons.profile.counter/src/test/java/org/connectorio/addons/profile/counter/internal/EnergyCounterProfileTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022-2022 ConnectorIO Sp. z o.o. + * Copyright (C) 2024-2024 ConnectorIO Sp. z o.o. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/bundles/org.connectorio.addons.profile/src/main/java/org/connectorio/addons/profile/internal/ChainedProfileCallback.java b/bundles/org.connectorio.addons.profile/src/main/java/org/connectorio/addons/profile/internal/ChainedProfileCallback.java deleted file mode 100644 index 5a887d05..00000000 --- a/bundles/org.connectorio.addons.profile/src/main/java/org/connectorio/addons/profile/internal/ChainedProfileCallback.java +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright (C) 2019-2021 ConnectorIO Sp. z o.o. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * SPDX-License-Identifier: Apache-2.0 - */ -package org.connectorio.addons.profile.internal; - -import java.util.Iterator; -import org.openhab.core.thing.profiles.ProfileCallback; -import org.openhab.core.thing.profiles.StateProfile; -import org.openhab.core.types.Command; -import org.openhab.core.types.State; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class ChainedProfileCallback implements ProfileCallback { - - private final Logger logger = LoggerFactory.getLogger(ChainedProfileCallback.class); - private final Iterator profiles; - private final ProfileCallback delegate; - - public ChainedProfileCallback(Iterator profiles, ProfileCallback delegate) { - this.profiles = profiles; - this.delegate = delegate; - } - - @Override - public void handleCommand(Command command) { - if (profiles.hasNext()) { - StateProfile next = profiles.next(); - logger.trace("Passing command {} to next profile {}", command, next); - next.onCommandFromItem(command); - } else { - logger.trace("Passing command {} final callback", command); - delegate.handleCommand(command); - } - } - - @Override - public void sendCommand(Command command) { - if (profiles.hasNext()) { - StateProfile next = profiles.next(); - logger.trace("Sending command {} to next profile {}", command, next); - next.onCommandFromHandler(command); - } else { - logger.trace("Sending command {} to final callback", command); - delegate.sendCommand(command); - } - } - - @Override - public void sendUpdate(State state) { - if (profiles.hasNext()) { - StateProfile next = profiles.next(); - logger.trace("Sending state {} to next profile {}", state, next); - next.onStateUpdateFromHandler(state); - } else { - logger.trace("Sending state {} to final callback", state); - delegate.sendUpdate(state); - } - } - - public String toString() { - return "ChainedProfileCallback [" + profiles + ", " + delegate + "]"; - } -} diff --git a/bundles/org.connectorio.addons.profile/src/main/java/org/connectorio/addons/profile/internal/ConnectorioProfile.java b/bundles/org.connectorio.addons.profile/src/main/java/org/connectorio/addons/profile/internal/ConnectorioProfile.java index 06a69e9d..5b4e3e74 100644 --- a/bundles/org.connectorio.addons.profile/src/main/java/org/connectorio/addons/profile/internal/ConnectorioProfile.java +++ b/bundles/org.connectorio.addons.profile/src/main/java/org/connectorio/addons/profile/internal/ConnectorioProfile.java @@ -25,8 +25,7 @@ import java.util.Map.Entry; import java.util.Objects; import java.util.Optional; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; +import java.util.concurrent.Executor; import java.util.function.Consumer; import org.connectorio.addons.profile.ProfileFactoryRegistry; import org.connectorio.addons.profile.internal.util.NestedMapCreator; @@ -48,15 +47,19 @@ class ConnectorioProfile implements StateProfile { private final Logger logger = LoggerFactory.getLogger(ConnectorioProfile.class); - private final ProfileCallback callback; + private final StackedProfileCallback callback; + private final Executor executor; private final ProfileContext context; - private final LinkedList profileChain = new LinkedList<>(); + private final LinkedList callbackChain = new LinkedList<>(); private final ProfileFactoryRegistry registry; - private final ExecutorService executor = Executors.newSingleThreadExecutor(); ConnectorioProfile(ProfileCallback callback, ProfileContext context, ProfileFactoryRegistry registry) { - this.callback = callback; + this(context.getExecutorService(), callback, context, registry); + } + + ConnectorioProfile(Executor executor, ProfileCallback callback, ProfileContext context, ProfileFactoryRegistry registry) { + this.executor = executor; this.context = context; this.registry = registry; @@ -67,7 +70,8 @@ class ConnectorioProfile implements StateProfile { } ItemChannelLink link = determnineLink(callback); - StackedProfileCallback chainedCallback = new StackedProfileCallback(link); + + this.callback = new StackedProfileCallback(callback, callbackChain); for (Entry entry : config.entrySet()) { if ("profile".equals(entry.getKey())) { continue; @@ -79,7 +83,7 @@ class ConnectorioProfile implements StateProfile { Map profileCfg = (Map) entry.getValue(); String profileType = (String) profileCfg.get("profile"); logger.debug("Creating profile {} for config key {}", profileType, entry.getKey()); - Profile createdProfile = getProfileFromFactories(getConfiguredProfileTypeUID(profileType), profileCfg, chainedCallback); + Profile createdProfile = getProfileFromFactories(getConfiguredProfileTypeUID(profileType), profileCfg, new NavigableCallback(link, callbackChain.size(), this.callback)); if (createdProfile == null) { Optional supported = registry.getAll().stream() .map(ProfileFactory::getSupportedProfileTypeUIDs) @@ -91,7 +95,7 @@ class ConnectorioProfile implements StateProfile { if (!(createdProfile instanceof StateProfile)) { throw new IllegalArgumentException("Could not create profile " + profileType + " or it is not state profile"); } - profileChain.add((StateProfile) createdProfile); + callbackChain.add((StateProfile) createdProfile); } } @@ -155,36 +159,15 @@ public void onStateUpdateFromHandler(State state) { } private void handleReading(boolean incoming, Type type, Consumer head) { - context.getExecutorService().execute(new Runnable() { + executor.execute(new Runnable() { @Override public void run() { try { - Iterator delegate = incoming ? profileChain.iterator() : profileChain.descendingIterator(); - Iterator iterator = new Iterator() { - int pos = 0; - @Override - public boolean hasNext() { - return delegate.hasNext(); - } - - @Override - public StateProfile next() { - pos++; - return delegate.next(); - } - - public String toString() { - return "Iterator [" + pos + ", " + profileChain + "]"; - } - }; - ChainedProfileCallback callback = new ChainedProfileCallback(iterator, ConnectorioProfile.this.callback); - logger.trace("Setting chained callback for {} to {}", type, callback); - StackedProfileCallback.set(callback); + Iterator iterator = incoming ? callbackChain.iterator() : callbackChain.descendingIterator(); + logger.trace("Firing chained profiles for {} to {}", type, callback); head.accept(iterator.next()); } catch (Throwable e) { logger.warn("Uncaught error found while calling profile chain for {}", type, e); - } finally { - StackedProfileCallback.set(null); } } }); diff --git a/bundles/org.connectorio.addons.profile/src/main/java/org/connectorio/addons/profile/internal/NavigableCallback.java b/bundles/org.connectorio.addons.profile/src/main/java/org/connectorio/addons/profile/internal/NavigableCallback.java new file mode 100644 index 00000000..9159e1e4 --- /dev/null +++ b/bundles/org.connectorio.addons.profile/src/main/java/org/connectorio/addons/profile/internal/NavigableCallback.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2024-2024 ConnectorIO Sp. z o.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.connectorio.addons.profile.internal; + +import org.openhab.core.thing.link.ItemChannelLink; +import org.openhab.core.thing.profiles.ProfileCallback; +import org.openhab.core.types.Command; +import org.openhab.core.types.State; + +/** + * Callback implementation which is aware of its position in chain. + * + * When this callback is asked to dispatch command or sate it passes it to chain with upper (state) + * or lower (command) element index. This construction allows to move state/command information + * across entire chain without too complex logic. Finalization of the call happens n stacked profile + * callback which know chain boundaries. + */ +public class NavigableCallback implements ProfileCallback { + + private final ItemChannelLink link; + private final int index; + private final StackedProfileCallback stack; + + public NavigableCallback(ItemChannelLink link, int index, StackedProfileCallback stack) { + this.link = link; + this.index = index; + this.stack = stack; + } + + @Override + public void handleCommand(Command command) { + stack.handleCommand(index - 1, command); + } + + @Override + public void sendCommand(Command command) { + stack.sendCommand(index - 1, command); + } + + @Override + public void sendUpdate(State state) { + stack.sendUpdate(index + 1, state); + } + + @Override + public String toString() { + return "Chained Callback [" + link + " at index " + index + "]"; + } + +} diff --git a/bundles/org.connectorio.addons.profile/src/main/java/org/connectorio/addons/profile/internal/StackedProfileCallback.java b/bundles/org.connectorio.addons.profile/src/main/java/org/connectorio/addons/profile/internal/StackedProfileCallback.java index d85281e6..1548cd83 100644 --- a/bundles/org.connectorio.addons.profile/src/main/java/org/connectorio/addons/profile/internal/StackedProfileCallback.java +++ b/bundles/org.connectorio.addons.profile/src/main/java/org/connectorio/addons/profile/internal/StackedProfileCallback.java @@ -17,54 +17,63 @@ */ package org.connectorio.addons.profile.internal; +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; +import java.util.concurrent.Executor; import org.openhab.core.thing.link.ItemChannelLink; import org.openhab.core.thing.profiles.ProfileCallback; +import org.openhab.core.thing.profiles.StateProfile; import org.openhab.core.types.Command; import org.openhab.core.types.State; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -public class StackedProfileCallback implements ProfileCallback { - - private final static ThreadLocal DELEGATE = new ThreadLocal<>(); +/** + * Stacked callback is a reference point for created profiles to communicate with framework. + * + * Since profiles can call callback at any time, this instance must be present when profile is being created. + * This leads to situation that we have to bridge it into future. + * More over, because this callback can be called from handler to item and from item to handler + * it has to work in both directions, independently of the creation time. + */ +public class StackedProfileCallback { private final Logger logger = LoggerFactory.getLogger(StackedProfileCallback.class); - private final ItemChannelLink link; - public StackedProfileCallback(ItemChannelLink link) { - this.link = link; + private final ProfileCallback callback; + private final LinkedList chain; + + public StackedProfileCallback(ProfileCallback callback, LinkedList chain) { + this.callback = callback; + this.chain = chain; } - @Override - public void handleCommand(Command command) { + public void handleCommand(int index, Command command) { + if (index == -1) { + callback.handleCommand(command); + return; + } logger.trace("Passing command {} to profile chain", command); - getDelegate().handleCommand(command); + chain.get(index).onCommandFromItem(command); } - @Override - public void sendCommand(Command command) { + public void sendCommand(int index, Command command) { + if (index == -1) { + callback.handleCommand(command); + return; + } logger.trace("Sending command {} toi profile chain", command); - getDelegate().sendCommand(command); + chain.get(index).onCommandFromHandler(command); } - @Override - public void sendUpdate(State state) { - logger.trace("Sending state {} to profile chain", state); - getDelegate().sendUpdate(state); - } - - private ProfileCallback getDelegate() { - ProfileCallback callback = DELEGATE.get(); - logger.trace("Callback looked up on thread stack {}", callback); - if (callback != null) { - return callback; + public void sendUpdate(int index, State state) { + if (index >= chain.size()) { + callback.sendUpdate(state); + return; } - - throw new IllegalStateException("No callback found on thread stack"); - } - - static void set(ProfileCallback callback) { - DELEGATE.set(callback); + logger.trace("Sending state {} to profile chain", state); + chain.get(index).onStateUpdateFromHandler(state); } } diff --git a/bundles/org.connectorio.addons.profile/src/test/java/org/connectorio/addons/profile/internal/ConnectorioProfileTest.java b/bundles/org.connectorio.addons.profile/src/test/java/org/connectorio/addons/profile/internal/ConnectorioProfileTest.java index 367334d6..f3609e29 100644 --- a/bundles/org.connectorio.addons.profile/src/test/java/org/connectorio/addons/profile/internal/ConnectorioProfileTest.java +++ b/bundles/org.connectorio.addons.profile/src/test/java/org/connectorio/addons/profile/internal/ConnectorioProfileTest.java @@ -24,6 +24,11 @@ import java.util.HashMap; import java.util.HashSet; import java.util.Set; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import java.util.concurrent.ForkJoinPool; +import java.util.concurrent.ThreadFactory; import org.connectorio.addons.profile.ProfileFactoryRegistry; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; @@ -44,7 +49,6 @@ /** * Test of chained invocations between profiles and framework. */ -@Disabled @ExtendWith(MockitoExtension.class) class ConnectorioProfileTest { @@ -65,11 +69,18 @@ class ConnectorioProfileTest { ProfileFactory factory2; @Test - void checkChainedValue() { + void checkChainedValue() throws Exception { HashMap cfgMap = new HashMap<>(); - cfgMap.put("profiles", Arrays.asList(CONDITION_PROFILE, SCALE_PROFILE)); + cfgMap.put("a1.profile", CONDITION_PROFILE); + cfgMap.put("b2.profile", SCALE_PROFILE); Configuration config = new Configuration(cfgMap); + Executor executor = new Executor() { + @Override + public void execute(Runnable command) { + command.run(); + } + }; when(context.getConfiguration()).thenReturn(config); when(registry.getAll()).thenReturn(Arrays.asList(factory1, factory2)); @@ -85,11 +96,11 @@ void checkChainedValue() { inv.getArgument(2, ProfileContext.class) )); - ConnectorioProfile profile = new ConnectorioProfile(callback, context, registry); - profile.onCommandFromItem(new DecimalType(22.0)); - Mockito.verify(callback).handleCommand(new DecimalType(11.0)); + ConnectorioProfile profile = new ConnectorioProfile(executor, callback, context, registry); +// profile.onCommandFromItem(new DecimalType(22.0)); +// Mockito.verify(callback).handleCommand(new DecimalType(11.0)); - profile.onCommandFromHandler(new DecimalType(11.0)); + profile.onStateUpdateFromHandler(new DecimalType(11.0)); Mockito.verify(callback).sendUpdate(new DecimalType(22.0)); } @@ -144,7 +155,7 @@ public void onStateUpdateFromHandler(State state) { public void onCommandFromItem(Command command) { if (command instanceof DecimalType) { DecimalType dec = (DecimalType) command; - callback.sendUpdate(new DecimalType(dec.doubleValue() / 2)); + callback.handleCommand(new DecimalType(dec.doubleValue() / 2)); } } } diff --git a/bundles/org.connectorio.addons.startlevel.shell/src/main/java/org/connectorio/addons/startlevel/shell/internal/StartLevelDebugCommand.java b/bundles/org.connectorio.addons.startlevel.shell/src/main/java/org/connectorio/addons/startlevel/shell/internal/StartLevelDebugCommand.java new file mode 100755 index 00000000..ca157d03 --- /dev/null +++ b/bundles/org.connectorio.addons.startlevel.shell/src/main/java/org/connectorio/addons/startlevel/shell/internal/StartLevelDebugCommand.java @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2019-2024 ConnectorIO Sp. z o.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.connectorio.addons.startlevel.shell.internal; + +import java.lang.reflect.Field; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import org.openhab.core.io.console.Console; +import org.openhab.core.io.console.extensions.AbstractConsoleCommandExtension; +import org.openhab.core.io.console.extensions.ConsoleCommandExtension; +import org.openhab.core.service.ReadyMarker; +import org.openhab.core.service.ReadyMarkerFilter; +import org.openhab.core.service.ReadyService; +import org.openhab.core.service.ReadyService.ReadyTracker; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; + +/** + * Simple console addon which prints resolved dependencies for ready markers. + */ +@Component(immediate = true, service = ConsoleCommandExtension.class) +public class StartLevelDebugCommand extends AbstractConsoleCommandExtension { + + private final ReadyService readyService; + + @Activate + public StartLevelDebugCommand(@Reference ReadyService readyService) { + super("co7io-start-level-debug", "Debug start level and ready markers"); + this.readyService = readyService; + } + + @Override + public void execute(String[] args, Console console) { + Set ready = new LinkedHashSet<>(); + ReadyTracker readyTracker = new ReadyTracker() { + @Override + public void onReadyMarkerAdded(ReadyMarker readyMarker) { + ready.add(readyMarker); + } + + @Override + public void onReadyMarkerRemoved(ReadyMarker readyMarker) {} + }; + try { + readyService.registerTracker(readyTracker); + } finally { + readyService.unregisterTracker(readyTracker); + } + + Map trackers = trackers(console); + for (ReadyTracker tracker : trackers.keySet()) { + ReadyMarkerFilter filter = trackers.get(tracker); + Set resolved = ready.stream().filter(filter::apply).collect(Collectors.toSet()); + console.println("Tracker " + tracker); + console.println(" Filter " + inspect(filter)); + if (!resolved.isEmpty()) { + console.println(" Resolved:"); + for (ReadyMarker marker : resolved) { + console.println(" - " + marker); + } + } + } + } + + private String inspect(ReadyMarkerFilter filter) { + Class filterClass = filter.getClass(); + try { + Field type = filterClass.getDeclaredField("type"); + if (!type.isAccessible()) { + type.setAccessible(true); + } + Field identifier = filterClass.getDeclaredField("identifier"); + if (!identifier.isAccessible()) { + identifier.setAccessible(true); + } + return type.get(filter) + "=" + identifier.get(filter); + } catch (Exception e) { + return filter.toString(); + } + } + + private Map trackers(Console console) { + try { + Field trackers = readyService.getClass().getDeclaredField("trackers"); + if (!trackers.isAccessible()) { + trackers.setAccessible(true); + } + Object value = trackers.get(readyService); + if (value instanceof Map) { + return (Map) value; + } + + console.println("Unsupported tracker content " + value); + return Collections.emptyMap(); + } catch (Exception e) { + return Collections.emptyMap(); + } + } + + @Override + public List getUsages() { + return Arrays.asList("co7io-start-level-debug"); + } +}