Skip to content

Commit

Permalink
Introduce collector and energy counter profile (transform Power to En…
Browse files Browse the repository at this point in the history
…ergy reading).

Adjustment of unit tests.

Signed-off-by: Łukasz Dywicki <luke@code-house.org>
  • Loading branch information
splatch committed May 9, 2024
1 parent 64786a4 commit ccf1ff7
Show file tree
Hide file tree
Showing 13 changed files with 580 additions and 18 deletions.
Expand Up @@ -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);
Expand Down Expand Up @@ -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;

Expand Down
@@ -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 <T extends Comparable<T>> void update(T current, T previous, Consumer<T> consumer, BiFunction<T, T, T> sum) {
if (previous == null) {
// initialization
consumer.accept(current);
return;
}
T collected = sum.apply(previous, current);
consumer.accept(collected);
}

}
Expand Up @@ -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);
}
Expand All @@ -61,14 +67,18 @@ public Profile createProfile(ProfileTypeUID profileTypeUID, ProfileCallback call

@Override
public Collection<ProfileTypeUID> 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<ProfileType> 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
);
}

}
Expand Up @@ -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")
Expand Down
@@ -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<Energy> 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<Measurement> 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<QuantityType<Energy>> consumer) {
if (previousValue.compareAndSet(null, power)) {
// initialization
return;
}

previousValue.getAndAccumulate(power, new BinaryOperator<Measurement>() {
@Override
public Measurement apply(Measurement previous, Measurement current) {
long millis = current.timestamp - previous.timestamp;

QuantityType<Energy> consumption = new QuantityType<>(previous.watts.multiply(BigDecimal.valueOf(millis)), MILLISECOND_WATT);
consumer.accept(consumption.toUnit(Units.WATT_HOUR));

return current;
}
});
}

}
Expand Up @@ -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));
}
}
}
Expand All @@ -81,7 +81,7 @@ private void compare(QuantityType current, QuantityType previous, Consumer<Quant
}

public String toString() {
return "LimitCounterBottom [" + last + "]";
return "LimitCounterBottom [" + last() + "]";
}

}
Expand Down
Expand Up @@ -127,7 +127,7 @@ private BigDecimal convert(QuantityType<?> quantifiedReading, Unit<?> unit) {
}

public String toString() {
return "LimitCounterTop [" + last + " " + anomaly + "]";
return "LimitCounterTop [" + last() + " " + anomaly + "]";
}

}
Expand Down
Expand Up @@ -45,20 +45,20 @@ class SustainedCounterProfile extends BaseCounterProfile {

@Override
public ProfileTypeUID getProfileTypeUID() {
return CounterProfiles.PULSE_COUNTER;
return CounterProfiles.SUSTAINED_COUNTER;
}

@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((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
);
Expand Down
@@ -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;
}

}

0 comments on commit ccf1ff7

Please sign in to comment.