diff --git a/bundles/org.openhab.persistence.mongodb/src/main/java/org/openhab/persistence/mongodb/internal/MongoDBConvertUtils.java b/bundles/org.openhab.persistence.mongodb/src/main/java/org/openhab/persistence/mongodb/internal/MongoDBConvertUtils.java new file mode 100644 index 0000000000000..57d7f759a7ca0 --- /dev/null +++ b/bundles/org.openhab.persistence.mongodb/src/main/java/org/openhab/persistence/mongodb/internal/MongoDBConvertUtils.java @@ -0,0 +1,163 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB 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.openhab.persistence.mongodb.internal; + +import java.math.BigDecimal; +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.Date; +import java.util.TimeZone; + +import javax.measure.Unit; + +import org.apache.commons.lang3.StringUtils; +import org.bson.types.ObjectId; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.items.Item; +import org.openhab.core.library.items.ContactItem; +import org.openhab.core.library.items.DateTimeItem; +import org.openhab.core.library.items.DimmerItem; +import org.openhab.core.library.items.ImageItem; +import org.openhab.core.library.items.NumberItem; +import org.openhab.core.library.items.RollershutterItem; +import org.openhab.core.library.items.SwitchItem; +import org.openhab.core.library.types.DateTimeType; +import org.openhab.core.library.types.DecimalType; +import org.openhab.core.library.types.HSBType; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.library.types.OpenClosedType; +import org.openhab.core.library.types.PercentType; +import org.openhab.core.library.types.PointType; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.library.types.RawType; +import org.openhab.core.library.types.StringType; +import org.openhab.core.types.State; + +import com.mongodb.BasicDBObject; +import com.mongodb.DBObject; + +import io.micrometer.core.lang.Nullable; + +/** + * Conversion logic between openHAB {@link State} types and MongoDB store types + * + * @author Konrad Zawadka - Initial contribution, based on previous work from Joan Pujol Espinar, Theo Weiss and Dominik + * Vorreiter + */ +public class MongoDBConvertUtils { + @NonNullByDefault + static final Number DIGITAL_VALUE_OFF = 0; // Visible for testing + @NonNullByDefault + static final Number DIGITAL_VALUE_ON = 1; // Visible for testing + @NonNullByDefault + static final String FIELD_ID = "_id"; + @NonNullByDefault + static final String FIELD_ITEM = "item"; + @NonNullByDefault + static final String FIELD_REALNAME = "realName"; + @NonNullByDefault + static final String FIELD_TIMESTAMP = "timestamp"; + @NonNullByDefault + static final String FIELD_VALUE = "value"; + + protected static Object stateToObject(State state) { + Object value; + if (state instanceof HSBType) { + value = state.toString(); + } else if (state instanceof PointType) { + value = state.toString(); + } else if (state instanceof DecimalType type) { + value = type.toBigDecimal(); + } else if (state instanceof QuantityType type) { + value = type.toBigDecimal(); + } else if (state instanceof OnOffType) { + value = state == OnOffType.ON ? DIGITAL_VALUE_ON : DIGITAL_VALUE_OFF; + } else if (state instanceof OpenClosedType) { + value = state == OpenClosedType.OPEN ? DIGITAL_VALUE_ON : DIGITAL_VALUE_OFF; + } else if (state instanceof DateTimeType type) { + value = type.getZonedDateTime().toInstant().toEpochMilli(); + } else { + value = state.toFullString(); + } + return value; + } + + protected static DBObject itemToDBObject(Item item, String alias) { + String name = (alias != null) ? alias : item.getName(); + Object value = MongoDBConvertUtils.stateToObject(item.getState()); + + DBObject obj = new BasicDBObject(); + obj.put(FIELD_ID, new ObjectId()); + obj.put(FIELD_ITEM, name); + obj.put(FIELD_REALNAME, item.getName()); + obj.put(FIELD_TIMESTAMP, new Date()); + obj.put(FIELD_VALUE, value); + return obj; + } + + protected static State objectToState(BasicDBObject obj, Item itemToSetState) { + final State state; + if (itemToSetState instanceof NumberItem numberItem) { + double value = obj.getDouble(FIELD_VALUE); + Unit unit = numberItem.getUnit(); + if (unit == null) { + return new DecimalType(value); + } else { + return new QuantityType<>(value, unit); + } + } else if (itemToSetState instanceof DimmerItem) { + return new PercentType(obj.getInt(FIELD_VALUE)); + } else if (itemToSetState instanceof SwitchItem) { + return toBoolean(obj.getString(FIELD_VALUE)) ? OnOffType.ON : OnOffType.OFF; + } else if (itemToSetState instanceof ContactItem) { + return toBoolean(obj.getString(FIELD_VALUE)) ? OpenClosedType.OPEN : OpenClosedType.CLOSED; + } else if (itemToSetState instanceof RollershutterItem) { + state = new PercentType(obj.getInt(FIELD_VALUE)); + } else if (itemToSetState instanceof DateTimeItem) { + state = getDateTime(obj); + } else if (itemToSetState instanceof ImageItem) { + state = RawType.valueOf(obj.getString(FIELD_VALUE)); + } else { + state = new StringType(obj.getString(FIELD_VALUE)); + } + return state; + } + + private static State getDateTime(BasicDBObject obj) { + String valueStr = obj.getString(FIELD_VALUE); + if (StringUtils.isNumeric(valueStr)) { + Instant i = Instant.ofEpochMilli(new BigDecimal(valueStr).longValue()); + ZonedDateTime z = ZonedDateTime.ofInstant(i, TimeZone.getDefault().toZoneId()); + return new DateTimeType(z); + } else { + return new DateTimeType( + ZonedDateTime.ofInstant(obj.getDate(FIELD_VALUE).toInstant(), ZoneId.systemDefault())); + } + } + + private static boolean toBoolean(@Nullable Object object) { + if (object instanceof Boolean boolean1) { + return boolean1; + } else if (object != null) { + if ("1".equals(object) || "1.0".equals(object)) { + return true; + } else { + return Boolean.parseBoolean(String.valueOf(object)); + } + } else { + return false; + } + } +} diff --git a/bundles/org.openhab.persistence.mongodb/src/main/java/org/openhab/persistence/mongodb/internal/MongoDBPersistenceService.java b/bundles/org.openhab.persistence.mongodb/src/main/java/org/openhab/persistence/mongodb/internal/MongoDBPersistenceService.java index f683b5e1ecb1d..69cca3106ce07 100644 --- a/bundles/org.openhab.persistence.mongodb/src/main/java/org/openhab/persistence/mongodb/internal/MongoDBPersistenceService.java +++ b/bundles/org.openhab.persistence.mongodb/src/main/java/org/openhab/persistence/mongodb/internal/MongoDBPersistenceService.java @@ -12,6 +12,11 @@ */ package org.openhab.persistence.mongodb.internal; +import static org.openhab.persistence.mongodb.internal.MongoDBConvertUtils.FIELD_ITEM; +import static org.openhab.persistence.mongodb.internal.MongoDBConvertUtils.FIELD_TIMESTAMP; +import static org.openhab.persistence.mongodb.internal.MongoDBConvertUtils.FIELD_VALUE; +import static org.openhab.persistence.mongodb.internal.MongoDBConvertUtils.itemToDBObject; + import java.time.ZoneId; import java.time.ZonedDateTime; import java.util.ArrayList; @@ -22,24 +27,11 @@ import java.util.Map; import java.util.Set; -import org.bson.types.ObjectId; 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.items.ContactItem; -import org.openhab.core.library.items.DateTimeItem; -import org.openhab.core.library.items.DimmerItem; -import org.openhab.core.library.items.NumberItem; -import org.openhab.core.library.items.RollershutterItem; -import org.openhab.core.library.items.SwitchItem; -import org.openhab.core.library.types.DateTimeType; -import org.openhab.core.library.types.DecimalType; -import org.openhab.core.library.types.OnOffType; -import org.openhab.core.library.types.OpenClosedType; -import org.openhab.core.library.types.PercentType; -import org.openhab.core.library.types.StringType; import org.openhab.core.persistence.FilterCriteria; import org.openhab.core.persistence.FilterCriteria.Operator; import org.openhab.core.persistence.FilterCriteria.Ordering; @@ -77,12 +69,6 @@ QueryablePersistenceService.class }, configurationPid = "org.openhab.mongodb", configurationPolicy = ConfigurationPolicy.REQUIRE) public class MongoDBPersistenceService implements QueryablePersistenceService { - private static final String FIELD_ID = "_id"; - private static final String FIELD_ITEM = "item"; - private static final String FIELD_REALNAME = "realName"; - private static final String FIELD_TIMESTAMP = "timestamp"; - private static final String FIELD_VALUE = "value"; - private final Logger logger = LoggerFactory.getLogger(MongoDBPersistenceService.class); private String url = ""; @@ -172,8 +158,7 @@ public void store(Item item, @Nullable String alias) { return; } - String realItemName = item.getName(); - String collectionName = collectionPerItem ? realItemName : this.collection; + String collectionName = collectionPerItem ? item.getName() : this.collection; @Nullable DBCollection collection = connectToCollection(collectionName); @@ -183,32 +168,10 @@ public void store(Item item, @Nullable String alias) { return; } - String name = (alias != null) ? alias : realItemName; - Object value = this.convertValue(item.getState()); - - DBObject obj = new BasicDBObject(); - obj.put(FIELD_ID, new ObjectId()); - obj.put(FIELD_ITEM, name); - obj.put(FIELD_REALNAME, realItemName); - obj.put(FIELD_TIMESTAMP, new Date()); - obj.put(FIELD_VALUE, value); + DBObject obj = itemToDBObject(item, alias); collection.save(obj); - logger.debug("MongoDB save {}={}", name, value); - } - - private Object convertValue(State state) { - Object value; - if (state instanceof PercentType type) { - value = type.toBigDecimal().doubleValue(); - } else if (state instanceof DateTimeType type) { - value = Date.from(type.getZonedDateTime().toInstant()); - } else if (state instanceof DecimalType type) { - value = type.toBigDecimal().doubleValue(); - } else { - value = state.toString(); - } - return value; + logger.debug("MongoDB save {}={}", item.getName(), item.getState()); } @Override @@ -369,7 +332,7 @@ public Iterable query(FilterCriteria filter) { return Collections.emptyList(); } - Object value = convertValue(filterState); + Object value = MongoDBConvertUtils.stateToObject(filterState); query.put(FIELD_VALUE, new BasicDBObject(op, value)); } @@ -393,25 +356,7 @@ public Iterable query(FilterCriteria filter) { while (cursor.hasNext()) { BasicDBObject obj = (BasicDBObject) cursor.next(); - final State state; - if (item instanceof NumberItem) { - state = new DecimalType(obj.getDouble(FIELD_VALUE)); - } else if (item instanceof DimmerItem) { - state = new PercentType(obj.getInt(FIELD_VALUE)); - } else if (item instanceof SwitchItem) { - state = OnOffType.valueOf(obj.getString(FIELD_VALUE)); - } else if (item instanceof ContactItem) { - state = OpenClosedType.valueOf(obj.getString(FIELD_VALUE)); - } else if (item instanceof RollershutterItem) { - state = new PercentType(obj.getInt(FIELD_VALUE)); - } else if (item instanceof DateTimeItem) { - state = new DateTimeType( - ZonedDateTime.ofInstant(obj.getDate(FIELD_VALUE).toInstant(), ZoneId.systemDefault())); - } else { - state = new StringType(obj.getString(FIELD_VALUE)); - } - - items.add(new MongoDBItem(realItemName, state, + items.add(new MongoDBItem(realItemName, MongoDBConvertUtils.objectToState(obj, item), ZonedDateTime.ofInstant(obj.getDate(FIELD_TIMESTAMP).toInstant(), ZoneId.systemDefault()))); } diff --git a/bundles/org.openhab.persistence.mongodb/src/test/java/org/openhab/persistence/mongodb/internal/MongoDBConvertUtilsTest.java b/bundles/org.openhab.persistence.mongodb/src/test/java/org/openhab/persistence/mongodb/internal/MongoDBConvertUtilsTest.java new file mode 100644 index 0000000000000..7c6985f85fcd3 --- /dev/null +++ b/bundles/org.openhab.persistence.mongodb/src/test/java/org/openhab/persistence/mongodb/internal/MongoDBConvertUtilsTest.java @@ -0,0 +1,162 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB 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.openhab.persistence.mongodb.internal; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.openhab.persistence.mongodb.internal.MongoDBConvertUtils.FIELD_VALUE; + +import java.math.BigDecimal; +import java.sql.Date; +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZonedDateTime; + +import javax.measure.Unit; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.openhab.core.i18n.UnitProvider; +import org.openhab.core.library.items.ContactItem; +import org.openhab.core.library.items.DateTimeItem; +import org.openhab.core.library.items.ImageItem; +import org.openhab.core.library.items.NumberItem; +import org.openhab.core.library.items.SwitchItem; +import org.openhab.core.library.types.DateTimeType; +import org.openhab.core.library.types.DecimalType; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.library.types.OpenClosedType; +import org.openhab.core.library.types.PlayPauseType; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.library.types.RawType; +import org.openhab.core.library.types.RewindFastforwardType; +import org.openhab.core.library.unit.SIUnits; +import org.openhab.core.types.State; + +import com.mongodb.BasicDBObject; + +/** + * Conversion logic between openHAB {@link State} types and MongoDB store types + * + * @author Konrad Zawadka - Initial contribution, based on previous work from Joan Pujol Espinar, Theo Weiss and Dominik + * Vorreiter + */ +class MongoDBConvertUtilsTest { + + @Test + public void convertDecimalState() { + var decimalType = new DecimalType(new BigDecimal("1.12")); + assertThat(MongoDBConvertUtils.stateToObject(decimalType), is(new BigDecimal("1.12"))); + } + + @Test + public void convertQuantityTypeState() { + var temperatureState = QuantityType.valueOf("7.3 °C"); + assertThat(MongoDBConvertUtils.stateToObject(temperatureState), equalTo(new BigDecimal("7.3"))); + } + + @Test + public void convertIntegerDecimalState() { + var decimalType = new DecimalType(12L); + assertThat(MongoDBConvertUtils.stateToObject(decimalType), is(new BigDecimal("12"))); + } + + @Test + public void convertOnOffState() { + assertThat(MongoDBConvertUtils.stateToObject(OpenClosedType.OPEN), equalTo(1)); + assertThat(MongoDBConvertUtils.stateToObject(OnOffType.ON), equalTo(1)); + } + + @Test + public void convertDateTimeState() { + var now = ZonedDateTime.now(); + long nowInMillis = now.toInstant().toEpochMilli(); + DateTimeType type = new DateTimeType(now); + assertThat(MongoDBConvertUtils.stateToObject(type), equalTo(nowInMillis)); + } + + @Test + public void convertImageState() { + var type = new RawType(new byte[] { 0x64, 0x66, 0x55, 0x00, 0x34 }, RawType.DEFAULT_MIME_TYPE); + assertThat(MongoDBConvertUtils.stateToObject(type), is("data:application/octet-stream;base64,ZGZVADQ=")); + } + + @Test + public void convertPlayPauseState() { + assertThat(MongoDBConvertUtils.stateToObject(PlayPauseType.PAUSE), is("PAUSE")); + assertThat(MongoDBConvertUtils.stateToObject(PlayPauseType.PLAY), is("PLAY")); + } + + @Test + public void convertRewindFastForwardState() { + assertThat(MongoDBConvertUtils.stateToObject(RewindFastforwardType.REWIND), is("REWIND")); + assertThat(MongoDBConvertUtils.stateToObject(RewindFastforwardType.FASTFORWARD), is("FASTFORWARD")); + } + + @ParameterizedTest + @ValueSource(strings = { "1.12", "25" }) + public void convertDecimalToState(String number) { + UnitProvider unitProviderMock = mock(UnitProvider.class); + when(unitProviderMock.getUnit(any())).thenReturn((Unit) SIUnits.CELSIUS); + BigDecimal val = new BigDecimal(number); + NumberItem plainItem = new NumberItem("plain"); + NumberItem dimensionItem = new NumberItem("Number:Temperature", "dimension", unitProviderMock); + assertThat(MongoDBConvertUtils.objectToState(basicDbObjectOf(val), plainItem), equalTo(new DecimalType(val))); + assertThat(MongoDBConvertUtils.objectToState(basicDbObjectOf(val), dimensionItem), + equalTo(new QuantityType<>(new BigDecimal(number), SIUnits.CELSIUS))); + } + + @Test + public void convertOnOffToState() { + boolean val1 = true; + int val2 = 1; + double val3 = 1.0; + SwitchItem onOffItem = new SwitchItem("name"); + ContactItem contactItem = new ContactItem("name"); + assertThat(MongoDBConvertUtils.objectToState(basicDbObjectOf(val1), onOffItem), equalTo(OnOffType.ON)); + assertThat(MongoDBConvertUtils.objectToState(basicDbObjectOf(val3), onOffItem), equalTo(OnOffType.ON)); + assertThat(MongoDBConvertUtils.objectToState(basicDbObjectOf(val2), onOffItem), equalTo(OnOffType.ON)); + assertThat(MongoDBConvertUtils.objectToState(basicDbObjectOf(val1), contactItem), equalTo(OpenClosedType.OPEN)); + assertThat(MongoDBConvertUtils.objectToState(basicDbObjectOf(val2), contactItem), equalTo(OpenClosedType.OPEN)); + assertThat(MongoDBConvertUtils.objectToState(basicDbObjectOf(val3), contactItem), equalTo(OpenClosedType.OPEN)); + } + + @Test + public void convertDateTimeToState() { + long val = System.currentTimeMillis(); + DateTimeItem item = new DateTimeItem("name"); + + DateTimeType expected = new DateTimeType( + ZonedDateTime.ofInstant(Instant.ofEpochMilli(val), ZoneId.systemDefault())); + assertThat(MongoDBConvertUtils.objectToState(basicDbObjectOf(val), item), equalTo(expected)); + assertThat(MongoDBConvertUtils.objectToState(basicDbObjectOf(Date.from(Instant.ofEpochMilli(val))), item), + equalTo(expected)); + } + + @Test + public void convertImageToState() { + ImageItem item = new ImageItem("name"); + RawType type = new RawType(new byte[] { 0x64, 0x66, 0x55, 0x00, 0x34 }, RawType.DEFAULT_MIME_TYPE); + BasicDBObject obj = basicDbObjectOf("data:application/octet-stream;base64,ZGZVADQ="); + assertThat(MongoDBConvertUtils.objectToState(obj, item), is(type)); + } + + private BasicDBObject basicDbObjectOf(Object value) { + return new BasicDBObject(FIELD_VALUE, value); + } +}