diff --git a/bundles/org.connectorio.addons.profile.counter/src/main/java/org/connectorio/addons/profile/counter/internal/BaseCounterProfile.java b/bundles/org.connectorio.addons.profile.counter/src/main/java/org/connectorio/addons/profile/counter/internal/BaseCounterProfile.java index fd889a60..61ccaca2 100644 --- a/bundles/org.connectorio.addons.profile.counter/src/main/java/org/connectorio/addons/profile/counter/internal/BaseCounterProfile.java +++ b/bundles/org.connectorio.addons.profile.counter/src/main/java/org/connectorio/addons/profile/counter/internal/BaseCounterProfile.java @@ -35,7 +35,7 @@ public abstract class BaseCounterProfile implements StateProfile { protected final ProfileCallback callback; protected final UninitializedBehavior uninitializedBehavior; protected final ProfileContext context; - protected Type last; + private Type last; protected BaseCounterProfile(ProfileCallback callback, ProfileContext context, LinkedItemStateRetriever linkedItemStateRetriever) { this(false, callback, context, linkedItemStateRetriever); @@ -113,6 +113,10 @@ private void handleReading(Type val, boolean incoming) { protected abstract void handleReading(Type current, Type previous, boolean incoming); + Type last() { + return last; + } + enum UninitializedBehavior { RESTORE_FROM_ITEM, RESTORE_FROM_PERSISTENCE, RESTORE_FROM_HANDLER; diff --git a/bundles/org.connectorio.addons.profile.counter/src/main/java/org/connectorio/addons/profile/counter/internal/CollectorProfile.java b/bundles/org.connectorio.addons.profile.counter/src/main/java/org/connectorio/addons/profile/counter/internal/CollectorProfile.java new file mode 100644 index 00000000..e5007d01 --- /dev/null +++ b/bundles/org.connectorio.addons.profile.counter/src/main/java/org/connectorio/addons/profile/counter/internal/CollectorProfile.java @@ -0,0 +1,71 @@ +/* + * 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.counter.internal; + +import java.util.function.BiFunction; +import java.util.function.Consumer; +import org.connectorio.addons.profile.counter.internal.state.LinkedItemStateRetriever; +import org.openhab.core.library.types.DecimalType; +import org.openhab.core.library.types.QuantityType; +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.types.Type; + +/** + * Profile which will keep collecting received values and adding them to initial item state. + */ +class CollectorProfile extends BaseCounterProfile { + + CollectorProfile(ProfileCallback callback, ProfileContext context, LinkedItemStateRetriever linkedItemStateRetriever) { + super(true, callback, context, linkedItemStateRetriever); + } + + @Override + public ProfileTypeUID getProfileTypeUID() { + return CounterProfiles.COLLECTOR; // TODO + } + + @Override + @SuppressWarnings({"unchecked", "rawtypes"}) + protected void handleReading(Type current, Type previous, boolean incoming) { + if (incoming) { // incoming, handler to item + if (current instanceof DecimalType) { + this.update((DecimalType) current, (DecimalType) previous, update(callback::sendUpdate), + (left, right) -> new DecimalType(left.toBigDecimal().add(right.toBigDecimal())) + ); + } else if (current instanceof QuantityType) { + this.update((QuantityType) current, (QuantityType) previous, update(callback::sendUpdate), + QuantityType::add + ); + } + } + // outgoing communication is not supported + } + + private > void update(T current, T previous, Consumer consumer, BiFunction sum) { + if (previous == null) { + // initialization + consumer.accept(current); + return; + } + T collected = sum.apply(previous, current); + consumer.accept(collected); + } + +} diff --git a/bundles/org.connectorio.addons.profile.counter/src/main/java/org/connectorio/addons/profile/counter/internal/CounterProfileFactory.java b/bundles/org.connectorio.addons.profile.counter/src/main/java/org/connectorio/addons/profile/counter/internal/CounterProfileFactory.java index c997b06a..b397859d 100644 --- a/bundles/org.connectorio.addons.profile.counter/src/main/java/org/connectorio/addons/profile/counter/internal/CounterProfileFactory.java +++ b/bundles/org.connectorio.addons.profile.counter/src/main/java/org/connectorio/addons/profile/counter/internal/CounterProfileFactory.java @@ -44,6 +44,12 @@ public CounterProfileFactory(@Reference LinkedItemStateRetriever linkedItemState @Override public Profile createProfile(ProfileTypeUID profileTypeUID, ProfileCallback callback, ProfileContext profileContext) { + if (CounterProfiles.COLLECTOR.equals(profileTypeUID)) { + return new CollectorProfile(callback, profileContext, linkedItemStateRetriever); + } + if (CounterProfiles.ENERGY_COUNTER.equals(profileTypeUID)) { + return new EnergyCounterProfile(callback, profileContext); + } if (CounterProfiles.LIMIT_COUNTER_TOP.equals(profileTypeUID)) { return new LimitCounterTopProfile(callback, profileContext, linkedItemStateRetriever); } @@ -61,14 +67,18 @@ public Profile createProfile(ProfileTypeUID profileTypeUID, ProfileCallback call @Override public Collection getSupportedProfileTypeUIDs() { - return Arrays.asList(CounterProfiles.LIMIT_COUNTER_TOP, CounterProfiles.LIMIT_COUNTER_BOTTOM, - CounterProfiles.PULSE_COUNTER, CounterProfiles.SUSTAINED_COUNTER); + return Arrays.asList(CounterProfiles.COLLECTOR, CounterProfiles.ENERGY_COUNTER, + CounterProfiles.LIMIT_COUNTER_TOP, CounterProfiles.LIMIT_COUNTER_BOTTOM, + CounterProfiles.PULSE_COUNTER, CounterProfiles.SUSTAINED_COUNTER + ); } @Override public Collection getProfileTypes(Locale locale) { - return Arrays.asList(CounterProfiles.LIMIT_COUNTER_TOP_PROFILE_TYPE, CounterProfiles.LIMIT_COUNTER_BOTTOM_PROFILE_TYPE, - CounterProfiles.PULSE_PROFILE_TYPE, CounterProfiles.SUSTAINED_COUNTER_PROFILE_TYPE); + return Arrays.asList(CounterProfiles.COLLECTOR_PROFILE_TYPE, CounterProfiles.ENERGY_COUNTER_PROFILE_TYPE, + CounterProfiles.LIMIT_COUNTER_TOP_PROFILE_TYPE, CounterProfiles.LIMIT_COUNTER_BOTTOM_PROFILE_TYPE, + CounterProfiles.PULSE_PROFILE_TYPE, CounterProfiles.SUSTAINED_COUNTER_PROFILE_TYPE + ); } } diff --git a/bundles/org.connectorio.addons.profile.counter/src/main/java/org/connectorio/addons/profile/counter/internal/CounterProfiles.java b/bundles/org.connectorio.addons.profile.counter/src/main/java/org/connectorio/addons/profile/counter/internal/CounterProfiles.java index 92679007..f6e683e0 100644 --- a/bundles/org.connectorio.addons.profile.counter/src/main/java/org/connectorio/addons/profile/counter/internal/CounterProfiles.java +++ b/bundles/org.connectorio.addons.profile.counter/src/main/java/org/connectorio/addons/profile/counter/internal/CounterProfiles.java @@ -23,6 +23,18 @@ public interface CounterProfiles { + ProfileTypeUID COLLECTOR = new ProfileTypeUID("connectorio", "collector"); + StateProfileType COLLECTOR_PROFILE_TYPE = ProfileTypeBuilder.newState(COLLECTOR, "Collector") + .withSupportedItemTypes("Number") + .withSupportedItemTypesOfChannel("Number") + .build(); + + ProfileTypeUID ENERGY_COUNTER = new ProfileTypeUID("connectorio", "energy-counter"); + StateProfileType ENERGY_COUNTER_PROFILE_TYPE = ProfileTypeBuilder.newState(COLLECTOR, "Energy Counter (transform W to Wh)") + .withSupportedItemTypes("Number") + .withSupportedItemTypesOfChannel("Number") + .build(); + ProfileTypeUID LIMIT_COUNTER_TOP = new ProfileTypeUID("connectorio", "limit-counter-top"); StateProfileType LIMIT_COUNTER_TOP_PROFILE_TYPE = ProfileTypeBuilder.newState(LIMIT_COUNTER_TOP, "Filter counter upper values") .withSupportedItemTypes("Number") diff --git a/bundles/org.connectorio.addons.profile.counter/src/main/java/org/connectorio/addons/profile/counter/internal/EnergyCounterProfile.java b/bundles/org.connectorio.addons.profile.counter/src/main/java/org/connectorio/addons/profile/counter/internal/EnergyCounterProfile.java new file mode 100644 index 00000000..57be9ec4 --- /dev/null +++ b/bundles/org.connectorio.addons.profile.counter/src/main/java/org/connectorio/addons/profile/counter/internal/EnergyCounterProfile.java @@ -0,0 +1,111 @@ +/* + * 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.counter.internal; + +import java.math.BigDecimal; +import java.time.Clock; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.BinaryOperator; +import java.util.function.Consumer; +import javax.measure.Unit; +import javax.measure.quantity.Energy; +import org.connectorio.addons.profile.counter.internal.state.DummyStateRetriever; +import org.openhab.core.library.types.DecimalType; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.library.unit.Units; +import org.openhab.core.thing.profiles.ProfileCallback; +import org.openhab.core.thing.profiles.ProfileContext; +import org.openhab.core.thing.profiles.ProfileTypeUID; +import org.openhab.core.types.Type; +import tec.uom.se.unit.MetricPrefix; +import tec.uom.se.unit.ProductUnit; + +/** + * Little utility profile which, based on power reading will calculate energy consumption. + */ +class EnergyCounterProfile extends BaseCounterProfile { + + // helper unit used internally by profile + public static Unit MILLISECOND_WATT = new ProductUnit<>(Units.WATT.multiply(MetricPrefix.MILLI(Units.SECOND))); + private final Clock clock; + + static class Measurement { + final BigDecimal watts; + final long timestamp; + + Measurement(BigDecimal watts, long timestamp) { + this.watts = watts; + this.timestamp = timestamp; + } + } + + private AtomicReference previousValue = new AtomicReference<>(); + + EnergyCounterProfile(ProfileCallback callback, ProfileContext context) { + this(callback, context, Clock.systemUTC()); + } + + EnergyCounterProfile(ProfileCallback callback, ProfileContext context, Clock clock) { + super(true, callback, context, new DummyStateRetriever()); + this.clock = clock; + } + + @Override + public ProfileTypeUID getProfileTypeUID() { + return CounterProfiles.ENERGY_COUNTER; + } + + @Override + @SuppressWarnings({"unchecked", "rawtypes"}) + protected void handleReading(Type current, Type previous, boolean incoming) { + long timestamp = clock.millis(); + if (incoming) { // incoming, handler to item + if (current instanceof DecimalType) { + // assume that number we received express power in Watts + Measurement measurement = new Measurement(((DecimalType) current).toBigDecimal(), timestamp); + this.update(measurement, update(callback::sendUpdate)); + } else if (current instanceof QuantityType) { + QuantityType value = ((QuantityType) current).toUnit(Units.WATT); + if (value != null) {; + this.update(new Measurement(value.toBigDecimal(), timestamp), update(callback::sendUpdate)); + } + } + } + // outgoing communication is not supported + } + + private void update(Measurement power, Consumer> consumer) { + if (previousValue.compareAndSet(null, power)) { + // initialization + return; + } + + previousValue.getAndAccumulate(power, new BinaryOperator() { + @Override + public Measurement apply(Measurement previous, Measurement current) { + long millis = current.timestamp - previous.timestamp; + + QuantityType consumption = new QuantityType<>(previous.watts.multiply(BigDecimal.valueOf(millis)), MILLISECOND_WATT); + consumer.accept(consumption.toUnit(Units.WATT_HOUR)); + + return current; + } + }); + } + +} diff --git a/bundles/org.connectorio.addons.profile.counter/src/main/java/org/connectorio/addons/profile/counter/internal/LimitCounterBottomProfile.java b/bundles/org.connectorio.addons.profile.counter/src/main/java/org/connectorio/addons/profile/counter/internal/LimitCounterBottomProfile.java index 898d89fe..67d1e53d 100644 --- a/bundles/org.connectorio.addons.profile.counter/src/main/java/org/connectorio/addons/profile/counter/internal/LimitCounterBottomProfile.java +++ b/bundles/org.connectorio.addons.profile.counter/src/main/java/org/connectorio/addons/profile/counter/internal/LimitCounterBottomProfile.java @@ -45,18 +45,18 @@ public ProfileTypeUID getProfileTypeUID() { @Override protected void handleReading(Type current, Type previous, boolean incoming) { if (incoming) { // incoming, handler to item - if (current instanceof DecimalType && last instanceof DecimalType) { - compare((DecimalType) current, (DecimalType) last, update(callback::sendUpdate)); + if (current instanceof DecimalType && previous instanceof DecimalType) { + compare((DecimalType) current, (DecimalType) previous, update(callback::sendUpdate)); } - if (current instanceof QuantityType && last instanceof QuantityType) { - compare((QuantityType) current, (QuantityType) last, update(callback::sendUpdate)); + if (current instanceof QuantityType && previous instanceof QuantityType) { + compare((QuantityType) current, (QuantityType) previous, update(callback::sendUpdate)); } } else { // outgoing, item to handler - if (current instanceof DecimalType && last instanceof DecimalType) { - compare((DecimalType) current, (DecimalType) last, update(callback::handleCommand)); + if (current instanceof DecimalType && previous instanceof DecimalType) { + compare((DecimalType) current, (DecimalType) previous, update(callback::handleCommand)); } - if (current instanceof QuantityType && last instanceof QuantityType) { - compare((QuantityType) current, (QuantityType) last, update(callback::handleCommand)); + if (current instanceof QuantityType && previous instanceof QuantityType) { + compare((QuantityType) current, (QuantityType) previous, update(callback::handleCommand)); } } } @@ -81,7 +81,7 @@ private void compare(QuantityType current, QuantityType previous, Consumer quantifiedReading, Unit unit) { } public String toString() { - return "LimitCounterTop [" + last + " " + anomaly + "]"; + return "LimitCounterTop [" + last() + " " + anomaly + "]"; } } diff --git a/bundles/org.connectorio.addons.profile.counter/src/main/java/org/connectorio/addons/profile/counter/internal/SustainedCounterProfile.java b/bundles/org.connectorio.addons.profile.counter/src/main/java/org/connectorio/addons/profile/counter/internal/SustainedCounterProfile.java index 4ae62de5..d067d350 100644 --- a/bundles/org.connectorio.addons.profile.counter/src/main/java/org/connectorio/addons/profile/counter/internal/SustainedCounterProfile.java +++ b/bundles/org.connectorio.addons.profile.counter/src/main/java/org/connectorio/addons/profile/counter/internal/SustainedCounterProfile.java @@ -45,7 +45,7 @@ class SustainedCounterProfile extends BaseCounterProfile { @Override public ProfileTypeUID getProfileTypeUID() { - return CounterProfiles.PULSE_COUNTER; + return CounterProfiles.SUSTAINED_COUNTER; } @Override @@ -53,12 +53,12 @@ public ProfileTypeUID getProfileTypeUID() { protected void handleReading(Type current, Type previous, boolean incoming) { if (incoming) { // incoming, handler to item if (current instanceof DecimalType) { - this.update((AtomicReference) previousValue, (DecimalType) current, (DecimalType) last, update(callback::sendUpdate), + this.update((AtomicReference) previousValue, (DecimalType) current, (DecimalType) previous, update(callback::sendUpdate), (left, right) -> new DecimalType(left.toBigDecimal().subtract(right.toBigDecimal())), (left, right) -> new DecimalType(left.toBigDecimal().add(right.toBigDecimal())) ); } else if (current instanceof QuantityType) { - this.update((AtomicReference) previousValue, (QuantityType) current, (QuantityType) last, update(callback::sendUpdate), + this.update((AtomicReference) previousValue, (QuantityType) current, (QuantityType) previous, update(callback::sendUpdate), QuantityType::subtract, QuantityType::add ); 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 new file mode 100644 index 00000000..e14c5aa4 --- /dev/null +++ b/bundles/org.connectorio.addons.profile.counter/src/main/java/org/connectorio/addons/profile/counter/internal/state/DummyStateRetriever.java @@ -0,0 +1,35 @@ +/* + * 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.counter.internal.state; + +import org.openhab.core.thing.profiles.ProfileCallback; +import org.openhab.core.types.State; + +public class DummyStateRetriever implements LinkedItemStateRetriever { + + @Override + public String getItemName(ProfileCallback callback) { + return ""; + } + + @Override + public State retrieveState(String itemName) { + return null; + } + +} diff --git a/bundles/org.connectorio.addons.profile.counter/src/main/resources/OH-INF/config/collector.xml b/bundles/org.connectorio.addons.profile.counter/src/main/resources/OH-INF/config/collector.xml new file mode 100644 index 00000000..363171b7 --- /dev/null +++ b/bundles/org.connectorio.addons.profile.counter/src/main/resources/OH-INF/config/collector.xml @@ -0,0 +1,38 @@ + + + + + + + + + Defines behavior when first value arrive and what to do with it. There are two cases which needs to be considered: + + + + + + + RESTORE_FROM_ITEM + + + + 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 new file mode 100644 index 00000000..af91620f --- /dev/null +++ b/bundles/org.connectorio.addons.profile.counter/src/test/java/org/connectorio/addons/profile/counter/internal/CollectorProfileTest.java @@ -0,0 +1,167 @@ +/* + * Copyright (C) 2022-2022 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.counter.internal; + +import static org.mockito.Mockito.when; + +import java.util.HashMap; +import org.connectorio.addons.profile.counter.internal.BaseCounterProfile.UninitializedBehavior; +import org.connectorio.addons.profile.counter.internal.state.LinkedItemStateRetriever; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import org.openhab.core.config.core.Configuration; +import org.openhab.core.library.types.DecimalType; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.library.unit.Units; +import org.openhab.core.thing.profiles.ProfileCallback; +import org.openhab.core.thing.profiles.ProfileContext; + +/** + * Basic test which confirms conversion logic for collector profile. + */ +@ExtendWith(MockitoExtension.class) +class CollectorProfileTest { + + @Mock + ProfileCallback callback; + @Mock + ProfileContext context; + @Mock + LinkedItemStateRetriever itemStateRetriever; + + @Test + void checkDecimalWithPersistentStateAndIncomingUpdate() { + HashMap cfgMap = new HashMap<>(); + cfgMap.put("uninitializedBehavior", UninitializedBehavior.RESTORE_FROM_PERSISTENCE.name()); + Configuration config = new Configuration(cfgMap); + + when(itemStateRetriever.getItemName(callback)).thenReturn("foo"); + when(itemStateRetriever.retrieveState("foo")).thenReturn(new DecimalType(1000)); + when(context.getConfiguration()).thenReturn(config); + + CollectorProfile profile = new CollectorProfile(callback, context, itemStateRetriever); + + // update from item above accepted level + profile.onStateUpdateFromHandler(new DecimalType(10.0)); + profile.onStateUpdateFromHandler(new DecimalType(13.1)); + profile.onStateUpdateFromHandler(new DecimalType(10)); + + // verify results + Mockito.verify(callback).sendUpdate(new DecimalType(1010)); + Mockito.verify(callback).sendUpdate(new DecimalType(1023.1)); + Mockito.verify(callback).sendUpdate(new DecimalType(1033.1)); + } + + @Test + void checkDecimalWithNoPersistentState() { + HashMap cfgMap = new HashMap<>(); + cfgMap.put("uninitializedBehavior", UninitializedBehavior.RESTORE_FROM_PERSISTENCE.name()); + Configuration config = new Configuration(cfgMap); + + when(context.getConfiguration()).thenReturn(config); + + when(itemStateRetriever.getItemName(callback)).thenReturn("foo"); + when(itemStateRetriever.retrieveState("foo")).thenReturn(null); + + CollectorProfile profile = new CollectorProfile(callback, context, itemStateRetriever); + + // update from item above accepted level + profile.onStateUpdateFromHandler(new DecimalType(13.0)); + profile.onStateUpdateFromHandler(new DecimalType(13.1)); + profile.onStateUpdateFromHandler(new DecimalType(10)); + + Mockito.verify(callback).sendUpdate(new DecimalType(13)); + Mockito.verify(callback).sendUpdate(new DecimalType(26.1)); + Mockito.verify(callback).sendUpdate(new DecimalType(36.1)); + } + + @Test + void checkQuantityWithNoPersistentState() { + HashMap cfgMap = new HashMap<>(); + cfgMap.put("uninitializedBehavior", UninitializedBehavior.RESTORE_FROM_PERSISTENCE.name()); + Configuration config = new Configuration(cfgMap); + + when(context.getConfiguration()).thenReturn(config); + + when(itemStateRetriever.getItemName(callback)).thenReturn("foo"); + when(itemStateRetriever.retrieveState("foo")).thenReturn(null); + + // we shall start with 10.0 retrieved from persistence + CollectorProfile profile = new CollectorProfile(callback, context, itemStateRetriever); + + // update from item above accepted level + profile.onStateUpdateFromHandler(new QuantityType<>(13.0, Units.KILOWATT_HOUR)); + profile.onStateUpdateFromHandler(new QuantityType<>(13.1, Units.KILOWATT_HOUR)); + profile.onStateUpdateFromHandler(new QuantityType<>(10, Units.KILOWATT_HOUR)); + + Mockito.verify(callback).sendUpdate(new QuantityType<>(13, Units.KILOWATT_HOUR)); + Mockito.verify(callback).sendUpdate(new QuantityType<>(26.1, Units.KILOWATT_HOUR)); + Mockito.verify(callback).sendUpdate(new QuantityType<>(36.1, Units.KILOWATT_HOUR)); + } + + @Test + void checkDecimalValueFromPersistence() { + HashMap cfgMap = new HashMap<>(); + cfgMap.put("uninitializedBehavior", UninitializedBehavior.RESTORE_FROM_PERSISTENCE.name()); + Configuration config = new Configuration(cfgMap); + + when(context.getConfiguration()).thenReturn(config); + + when(itemStateRetriever.getItemName(callback)).thenReturn("foo"); + when(itemStateRetriever.retrieveState("foo")).thenReturn(new DecimalType(1000)); + + // we shall start with 10.0 retrieved from persistence + CollectorProfile profile = new CollectorProfile(callback, context, itemStateRetriever); + + // update from item above accepted level + profile.onStateUpdateFromHandler(new DecimalType(13.0)); + profile.onStateUpdateFromHandler(new DecimalType(13.1)); + profile.onStateUpdateFromHandler(new DecimalType(10)); + + Mockito.verify(callback).sendUpdate(new DecimalType(1013)); + Mockito.verify(callback).sendUpdate(new DecimalType(1026.1)); + Mockito.verify(callback).sendUpdate(new DecimalType(1036.1)); + } + + @Test + void checkQuantityValueFromPersistence() { + HashMap cfgMap = new HashMap<>(); + cfgMap.put("uninitializedBehavior", UninitializedBehavior.RESTORE_FROM_PERSISTENCE.name()); + Configuration config = new Configuration(cfgMap); + + when(context.getConfiguration()).thenReturn(config); + + when(itemStateRetriever.getItemName(callback)).thenReturn("foo"); + when(itemStateRetriever.retrieveState("foo")).thenReturn(new QuantityType<>(1000, Units.KILOWATT_HOUR)); + + // we shall start with 10.0 retrieved from persistence + CollectorProfile profile = new CollectorProfile(callback, context, itemStateRetriever); + + profile.onStateUpdateFromHandler(new QuantityType<>(13.0, Units.KILOWATT_HOUR)); + profile.onStateUpdateFromHandler(new QuantityType<>(13.1, Units.KILOWATT_HOUR)); + profile.onStateUpdateFromHandler(new QuantityType<>(10, Units.KILOWATT_HOUR)); + + Mockito.verify(callback).sendUpdate(new QuantityType(1013, Units.KILOWATT_HOUR)); + Mockito.verify(callback).sendUpdate(new QuantityType(1026.1, Units.KILOWATT_HOUR)); + Mockito.verify(callback).sendUpdate(new QuantityType<>(1036.1, Units.KILOWATT_HOUR)); + } + +} \ No newline at end of file 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 new file mode 100644 index 00000000..f645c4b5 --- /dev/null +++ b/bundles/org.connectorio.addons.profile.counter/src/test/java/org/connectorio/addons/profile/counter/internal/EnergyCounterProfileTest.java @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2022-2022 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.counter.internal; + +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.when; + +import java.time.Clock; +import java.util.HashMap; +import org.connectorio.addons.profile.counter.internal.BaseCounterProfile.UninitializedBehavior; +import org.connectorio.addons.profile.counter.internal.state.LinkedItemStateRetriever; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import org.openhab.core.config.core.Configuration; +import org.openhab.core.library.types.DecimalType; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.library.unit.Units; +import org.openhab.core.thing.profiles.ProfileCallback; +import org.openhab.core.thing.profiles.ProfileContext; + +/** + * Basic test which confirms conversion logic for energy counter. + */ +@ExtendWith(MockitoExtension.class) +class EnergyCounterProfileTest { + + @Mock + ProfileCallback callback; + @Mock + ProfileContext context; + @Mock + Clock clock; + + @Test + void checkDecimal() { + HashMap cfgMap = new HashMap<>(); + Configuration config = new Configuration(cfgMap); + when(context.getConfiguration()).thenReturn(config); + + EnergyCounterProfile profile = new EnergyCounterProfile(callback, context, clock); + + // update from item above accepted level + when(clock.millis()).thenReturn(0L); + profile.onStateUpdateFromHandler(new DecimalType(1)); + + when(clock.millis()).thenReturn(60 * 60 * 1000L); + profile.onStateUpdateFromHandler(new DecimalType(1)); + + Mockito.verify(callback).sendUpdate(new QuantityType<>(1, Units.WATT_HOUR)); + } + + @Test + void checkQuantity() { + HashMap cfgMap = new HashMap<>(); + Configuration config = new Configuration(cfgMap); + when(context.getConfiguration()).thenReturn(config); + + // we shall start with 10.0 retrieved from persistence + EnergyCounterProfile profile = new EnergyCounterProfile(callback, context, clock); + + // update from item above accepted level + when(clock.millis()).thenReturn(0L); + profile.onStateUpdateFromHandler(new QuantityType<>(1, Units.WATT)); + + when(clock.millis()).thenReturn(60 * 60 * 1000L); + profile.onStateUpdateFromHandler(new QuantityType<>(1, Units.WATT)); + + Mockito.verify(callback).sendUpdate(new QuantityType<>(1, Units.WATT_HOUR)); + } + +} \ No newline at end of file diff --git a/bundles/org.connectorio.addons.profile.counter/src/test/java/org/connectorio/addons/profile/counter/internal/SustainedCounterProfileTest.java b/bundles/org.connectorio.addons.profile.counter/src/test/java/org/connectorio/addons/profile/counter/internal/SustainedCounterProfileTest.java index b7b30322..af6c8156 100644 --- a/bundles/org.connectorio.addons.profile.counter/src/test/java/org/connectorio/addons/profile/counter/internal/SustainedCounterProfileTest.java +++ b/bundles/org.connectorio.addons.profile.counter/src/test/java/org/connectorio/addons/profile/counter/internal/SustainedCounterProfileTest.java @@ -48,6 +48,31 @@ class SustainedCounterProfileTest { @Mock LinkedItemStateRetriever itemStateRetriever; + @Test + void checkDecimalWithPersistentStateAndIncomingUpdate() { + HashMap cfgMap = new HashMap<>(); + cfgMap.put("uninitializedBehavior", UninitializedBehavior.RESTORE_FROM_PERSISTENCE.name()); + Configuration config = new Configuration(cfgMap); + + when(itemStateRetriever.getItemName(callback)).thenReturn("foo"); + when(itemStateRetriever.retrieveState("foo")).thenReturn(new DecimalType(10)); + when(context.getConfiguration()).thenReturn(config); + + // we shall start with 10.0 retrieved from persistence + SustainedCounterProfile profile = new SustainedCounterProfile(callback, context, itemStateRetriever); + + // update from item above accepted level + profile.onStateUpdateFromHandler(new DecimalType(10.0)); + profile.onStateUpdateFromHandler(new DecimalType(13.1)); + + // reset call + profile.onStateUpdateFromHandler(new DecimalType(10)); + + Mockito.verify(callback).sendUpdate(new DecimalType(20)); + Mockito.verify(callback).sendUpdate(new DecimalType(23.1)); + Mockito.verify(callback).sendUpdate(new DecimalType(33.1)); + } + @Test void checkDecimalWithNoPersistentState() { HashMap cfgMap = new HashMap<>();