From 706db1e5cd8f48410058b832a297fbdadd2d2037 Mon Sep 17 00:00:00 2001 From: jlaur Date: Sat, 25 Sep 2021 21:09:26 +0200 Subject: [PATCH] Add new channels for water and power consumption for washing machines and dishwashers. (#11298) Fixes #11297 Signed-off-by: Jacob Laursen --- .../internal/ExtendedDeviceStateUtil.java | 49 ++++++++++++++++++ .../miele/internal/MieleBindingConstants.java | 5 ++ .../handler/ApplianceChannelSelector.java | 7 +++ .../handler/CoffeeMachineChannelSelector.java | 5 ++ .../internal/handler/DishWasherHandler.java | 31 ++++++++++- .../handler/DishwasherChannelSelector.java | 47 +++++++++++------ .../handler/ExtendedDeviceStateListener.java | 23 +++++++++ .../handler/FridgeChannelSelector.java | 5 ++ .../handler/FridgeFreezerChannelSelector.java | 5 ++ .../internal/handler/HobChannelSelector.java | 5 ++ .../internal/handler/HoodChannelSelector.java | 5 ++ .../handler/MieleApplianceHandler.java | 26 +++++++++- .../internal/handler/OvenChannelSelector.java | 5 ++ .../handler/TumbleDryerChannelSelector.java | 5 ++ .../WashingMachineChannelSelector.java | 51 ++++++++++++------- .../handler/WashingMachineHandler.java | 31 ++++++++++- .../resources/OH-INF/thing/channeltypes.xml | 14 +++++ .../resources/OH-INF/thing/dishwasher.xml | 2 + .../resources/OH-INF/thing/washingmachine.xml | 2 + .../internal/ExtendedDeviceStateUtilTest.java | 48 +++++++++++++++++ 20 files changed, 333 insertions(+), 38 deletions(-) create mode 100644 bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/ExtendedDeviceStateUtil.java create mode 100644 bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/handler/ExtendedDeviceStateListener.java create mode 100644 bundles/org.openhab.binding.miele/src/test/java/org/openhab/binding/miele/internal/ExtendedDeviceStateUtilTest.java diff --git a/bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/ExtendedDeviceStateUtil.java b/bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/ExtendedDeviceStateUtil.java new file mode 100644 index 0000000000000..409c9fe23e4d7 --- /dev/null +++ b/bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/ExtendedDeviceStateUtil.java @@ -0,0 +1,49 @@ +/** + * Copyright (c) 2010-2021 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.binding.miele.internal; + +import java.nio.charset.StandardCharsets; + +/** + * The {@link ExtendedDeviceStateUtil} class contains utility methods for parsing + * ExtendedDeviceState information + * + * @author Jacob Laursen - Added power/water consumption channels + */ +public class ExtendedDeviceStateUtil { + private static final byte[] HEX_ARRAY = "0123456789ABCDEF".getBytes(StandardCharsets.US_ASCII); + + /** + * Convert byte array to hex representation. + */ + public static String bytesToHex(byte[] bytes) { + byte[] hexChars = new byte[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, StandardCharsets.UTF_8); + } + + /** + * Convert string consisting of 8 bit characters to byte array. + * Note: This simple operation has been extracted and pure here to document + * and ensure correct behavior for 8 bit characters that should be turned + * into single bytes without any UTF-8 encoding. + */ + public static byte[] stringToBytes(String input) { + return input.getBytes(StandardCharsets.ISO_8859_1); + } +} diff --git a/bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/MieleBindingConstants.java b/bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/MieleBindingConstants.java index df74fceaa225f..1c306cd9ee84d 100644 --- a/bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/MieleBindingConstants.java +++ b/bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/MieleBindingConstants.java @@ -31,6 +31,11 @@ public class MieleBindingConstants { public static final String DEVICE_CLASS = "dc"; public static final String PROTOCOL_PROPERTY_NAME = "protocol"; public static final String SERIAL_NUMBER_PROPERTY_NAME = "serialNumber"; + public static final String EXTENDED_DEVICE_STATE_PROPERTY_NAME = "extendedDeviceState"; + + // Shared Channel ID's + public static final String POWER_CONSUMPTION_CHANNEL_ID = "powerConsumption"; + public static final String WATER_CONSUMPTION_CHANNEL_ID = "waterConsumption"; // List of all Thing Type UIDs public static final ThingTypeUID THING_TYPE_XGW3000 = new ThingTypeUID(BINDING_ID, "xgw3000"); diff --git a/bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/handler/ApplianceChannelSelector.java b/bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/handler/ApplianceChannelSelector.java index 663ee195ebe48..260449f3b0ade 100644 --- a/bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/handler/ApplianceChannelSelector.java +++ b/bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/handler/ApplianceChannelSelector.java @@ -23,6 +23,7 @@ * returned by the appliance to a compatible State * * @author Karel Goderis - Initial contribution + * @author Jacob Laursen - Added power/water consumption channels */ public interface ApplianceChannelSelector { @@ -45,6 +46,12 @@ public interface ApplianceChannelSelector { */ boolean isProperty(); + /** + * Returns true if the given channel is extracted from extended + * state information + */ + boolean isExtendedState(); + /** * * Returns a State for the given string, taking into diff --git a/bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/handler/CoffeeMachineChannelSelector.java b/bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/handler/CoffeeMachineChannelSelector.java index 6360b1f1393d7..2395f847091e8 100644 --- a/bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/handler/CoffeeMachineChannelSelector.java +++ b/bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/handler/CoffeeMachineChannelSelector.java @@ -99,6 +99,11 @@ public boolean isProperty() { return isProperty; } + @Override + public boolean isExtendedState() { + return false; + } + @Override public State getState(String s, DeviceMetaData dmd) { if (dmd != null) { diff --git a/bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/handler/DishWasherHandler.java b/bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/handler/DishWasherHandler.java index dc06b24575fb8..b7086f89f9cd1 100644 --- a/bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/handler/DishWasherHandler.java +++ b/bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/handler/DishWasherHandler.java @@ -13,10 +13,16 @@ package org.openhab.binding.miele.internal.handler; import static org.openhab.binding.miele.internal.MieleBindingConstants.APPLIANCE_ID; +import static org.openhab.binding.miele.internal.MieleBindingConstants.POWER_CONSUMPTION_CHANNEL_ID; import static org.openhab.binding.miele.internal.MieleBindingConstants.PROTOCOL_PROPERTY_NAME; +import static org.openhab.binding.miele.internal.MieleBindingConstants.WATER_CONSUMPTION_CHANNEL_ID; + +import java.math.BigDecimal; import org.openhab.binding.miele.internal.FullyQualifiedApplianceIdentifier; import org.openhab.core.library.types.OnOffType; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.library.unit.Units; import org.openhab.core.thing.ChannelUID; import org.openhab.core.thing.Thing; import org.openhab.core.types.Command; @@ -33,9 +39,14 @@ * @author Karel Goderis - Initial contribution * @author Kai Kreuzer - fixed handling of REFRESH commands * @author Martin Lepsy - fixed handling of empty JSON results - * @author Jacob Laursen - Fixed multicast and protocol support (ZigBee/LAN) + * @author Jacob Laursen - Fixed multicast and protocol support (ZigBee/LAN), added power/water consumption channels */ -public class DishWasherHandler extends MieleApplianceHandler { +public class DishWasherHandler extends MieleApplianceHandler + implements ExtendedDeviceStateListener { + + private static final int POWER_CONSUMPTION_BYTE_POSITION = 16; + private static final int WATER_CONSUMPTION_BYTE_POSITION = 18; + private static final int EXTENDED_STATE_SIZE_BYTES = 24; private final Logger logger = LoggerFactory.getLogger(DishWasherHandler.class); @@ -84,4 +95,20 @@ public void handleCommand(ChannelUID channelUID, Command command) { channelID, command.toString()); } } + + public void onApplianceExtendedStateChanged(byte[] extendedDeviceState) { + if (extendedDeviceState.length != EXTENDED_STATE_SIZE_BYTES) { + logger.error("Unexpected size of extended state: {}", extendedDeviceState); + return; + } + + BigDecimal kiloWattHoursTenths = BigDecimal + .valueOf(extendedDeviceState[POWER_CONSUMPTION_BYTE_POSITION] & 0xff); + var kiloWattHours = new QuantityType<>(kiloWattHoursTenths.divide(BigDecimal.valueOf(10)), Units.KILOWATT_HOUR); + updateExtendedState(POWER_CONSUMPTION_CHANNEL_ID, kiloWattHours); + + BigDecimal decilitres = BigDecimal.valueOf(extendedDeviceState[WATER_CONSUMPTION_BYTE_POSITION] & 0xff); + var litres = new QuantityType<>(decilitres.divide(BigDecimal.valueOf(10)), Units.LITRE); + updateExtendedState(WATER_CONSUMPTION_CHANNEL_ID, litres); + } } diff --git a/bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/handler/DishwasherChannelSelector.java b/bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/handler/DishwasherChannelSelector.java index f44b5008436a3..d9b227bc00f41 100644 --- a/bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/handler/DishwasherChannelSelector.java +++ b/bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/handler/DishwasherChannelSelector.java @@ -12,6 +12,10 @@ */ package org.openhab.binding.miele.internal.handler; +import static org.openhab.binding.miele.internal.MieleBindingConstants.EXTENDED_DEVICE_STATE_PROPERTY_NAME; +import static org.openhab.binding.miele.internal.MieleBindingConstants.POWER_CONSUMPTION_CHANNEL_ID; +import static org.openhab.binding.miele.internal.MieleBindingConstants.WATER_CONSUMPTION_CHANNEL_ID; + import java.lang.reflect.Method; import java.text.SimpleDateFormat; import java.util.Date; @@ -22,6 +26,7 @@ import org.openhab.core.library.types.DateTimeType; import org.openhab.core.library.types.OnOffType; import org.openhab.core.library.types.OpenClosedType; +import org.openhab.core.library.types.QuantityType; import org.openhab.core.library.types.StringType; import org.openhab.core.types.State; import org.openhab.core.types.Type; @@ -36,17 +41,18 @@ * * @author Karel Goderis - Initial contribution * @author Kai Kreuzer - Changed START_TIME to DateTimeType + * @author Jacob Laursen - Added power/water consumption channels */ public enum DishwasherChannelSelector implements ApplianceChannelSelector { - PRODUCT_TYPE("productTypeId", "productType", StringType.class, true), - DEVICE_TYPE("mieleDeviceType", "deviceType", StringType.class, true), - BRAND_ID("brandId", "brandId", StringType.class, true), - COMPANY_ID("companyId", "companyId", StringType.class, true), - STATE("state", "state", StringType.class, false), - PROGRAMID("programId", "program", StringType.class, false), - PROGRAMPHASE("phase", "phase", StringType.class, false), - START_TIME("startTime", "start", DateTimeType.class, false) { + PRODUCT_TYPE("productTypeId", "productType", StringType.class, true, false), + DEVICE_TYPE("mieleDeviceType", "deviceType", StringType.class, true, false), + BRAND_ID("brandId", "brandId", StringType.class, true, false), + COMPANY_ID("companyId", "companyId", StringType.class, true, false), + STATE("state", "state", StringType.class, false, false), + PROGRAMID("programId", "program", StringType.class, false, false), + PROGRAMPHASE("phase", "phase", StringType.class, false, false), + START_TIME("startTime", "start", DateTimeType.class, false, false) { @Override public State getState(String s, DeviceMetaData dmd) { Date date = new Date(); @@ -60,7 +66,7 @@ public State getState(String s, DeviceMetaData dmd) { return getState(dateFormatter.format(date)); } }, - DURATION("duration", "duration", DateTimeType.class, false) { + DURATION("duration", "duration", DateTimeType.class, false, false) { @Override public State getState(String s, DeviceMetaData dmd) { Date date = new Date(); @@ -74,7 +80,7 @@ public State getState(String s, DeviceMetaData dmd) { return getState(dateFormatter.format(date)); } }, - ELAPSED_TIME("elapsedTime", "elapsed", DateTimeType.class, false) { + ELAPSED_TIME("elapsedTime", "elapsed", DateTimeType.class, false, false) { @Override public State getState(String s, DeviceMetaData dmd) { Date date = new Date(); @@ -88,7 +94,7 @@ public State getState(String s, DeviceMetaData dmd) { return getState(dateFormatter.format(date)); } }, - FINISH_TIME("finishTime", "finish", DateTimeType.class, false) { + FINISH_TIME("finishTime", "finish", DateTimeType.class, false, false) { @Override public State getState(String s, DeviceMetaData dmd) { Date date = new Date(); @@ -102,7 +108,7 @@ public State getState(String s, DeviceMetaData dmd) { return getState(dateFormatter.format(date)); } }, - DOOR("signalDoor", "door", OpenClosedType.class, false) { + DOOR("signalDoor", "door", OpenClosedType.class, false, false) { @Override public State getState(String s, DeviceMetaData dmd) { if ("true".equals(s)) { @@ -116,7 +122,11 @@ public State getState(String s, DeviceMetaData dmd) { return UnDefType.UNDEF; } }, - SWITCH(null, "switch", OnOffType.class, false); + SWITCH(null, "switch", OnOffType.class, false, false), + POWER_CONSUMPTION(EXTENDED_DEVICE_STATE_PROPERTY_NAME, POWER_CONSUMPTION_CHANNEL_ID, QuantityType.class, false, + true), + WATER_CONSUMPTION(EXTENDED_DEVICE_STATE_PROPERTY_NAME, WATER_CONSUMPTION_CHANNEL_ID, QuantityType.class, false, + true); private final Logger logger = LoggerFactory.getLogger(DishwasherChannelSelector.class); @@ -124,13 +134,15 @@ public State getState(String s, DeviceMetaData dmd) { private final String channelID; private final Class typeClass; private final boolean isProperty; + private final boolean isExtendedState; - DishwasherChannelSelector(String propertyID, String channelID, Class typeClass, - boolean isProperty) { + DishwasherChannelSelector(String propertyID, String channelID, Class typeClass, boolean isProperty, + boolean isExtendedState) { this.mieleID = propertyID; this.channelID = channelID; this.typeClass = typeClass; this.isProperty = isProperty; + this.isExtendedState = isExtendedState; } @Override @@ -158,6 +170,11 @@ public boolean isProperty() { return isProperty; } + @Override + public boolean isExtendedState() { + return isExtendedState; + } + @Override public State getState(String s, DeviceMetaData dmd) { if (dmd != null) { diff --git a/bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/handler/ExtendedDeviceStateListener.java b/bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/handler/ExtendedDeviceStateListener.java new file mode 100644 index 0000000000000..f34623d677890 --- /dev/null +++ b/bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/handler/ExtendedDeviceStateListener.java @@ -0,0 +1,23 @@ +/** + * Copyright (c) 2010-2021 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.binding.miele.internal.handler; + +/** + * Appliance handlers can implement the {@link ExtendedDeviceStateListener} interface + * to extract additional information from the ExtendedDeviceState property. + * + * @author Jacob Laursen - Added power/water consumption channels + */ +public interface ExtendedDeviceStateListener { + void onApplianceExtendedStateChanged(byte[] extendedDeviceState); +} diff --git a/bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/handler/FridgeChannelSelector.java b/bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/handler/FridgeChannelSelector.java index 34460ded5fb68..ceb8706356b36 100644 --- a/bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/handler/FridgeChannelSelector.java +++ b/bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/handler/FridgeChannelSelector.java @@ -109,6 +109,11 @@ public boolean isProperty() { return isProperty; } + @Override + public boolean isExtendedState() { + return false; + } + @Override public State getState(String s, DeviceMetaData dmd) { if (dmd != null) { diff --git a/bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/handler/FridgeFreezerChannelSelector.java b/bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/handler/FridgeFreezerChannelSelector.java index 10e16c8da9bb5..c2dbd7417cf28 100644 --- a/bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/handler/FridgeFreezerChannelSelector.java +++ b/bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/handler/FridgeFreezerChannelSelector.java @@ -126,6 +126,11 @@ public boolean isProperty() { return isProperty; } + @Override + public boolean isExtendedState() { + return false; + } + @Override public State getState(String s, DeviceMetaData dmd) { if (dmd != null) { diff --git a/bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/handler/HobChannelSelector.java b/bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/handler/HobChannelSelector.java index ebc86433d8dca..d588c9b11bb40 100644 --- a/bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/handler/HobChannelSelector.java +++ b/bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/handler/HobChannelSelector.java @@ -129,6 +129,11 @@ public boolean isProperty() { return isProperty; } + @Override + public boolean isExtendedState() { + return false; + } + @Override public State getState(String s, DeviceMetaData dmd) { if (dmd != null) { diff --git a/bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/handler/HoodChannelSelector.java b/bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/handler/HoodChannelSelector.java index bdb416829cdf3..c36f85711b94a 100644 --- a/bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/handler/HoodChannelSelector.java +++ b/bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/handler/HoodChannelSelector.java @@ -96,6 +96,11 @@ public boolean isProperty() { return isProperty; } + @Override + public boolean isExtendedState() { + return false; + } + @Override public State getState(String s, DeviceMetaData dmd) { if (dmd != null) { diff --git a/bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/handler/MieleApplianceHandler.java b/bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/handler/MieleApplianceHandler.java index f63002f3a2608..b743d9864a910 100644 --- a/bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/handler/MieleApplianceHandler.java +++ b/bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/handler/MieleApplianceHandler.java @@ -21,6 +21,7 @@ import java.util.stream.Stream; import org.apache.commons.lang3.StringUtils; +import org.openhab.binding.miele.internal.ExtendedDeviceStateUtil; import org.openhab.binding.miele.internal.FullyQualifiedApplianceIdentifier; import org.openhab.binding.miele.internal.handler.MieleBridgeHandler.DeviceClassObject; import org.openhab.binding.miele.internal.handler.MieleBridgeHandler.DeviceMetaData; @@ -36,6 +37,7 @@ import org.openhab.core.thing.binding.ThingHandler; import org.openhab.core.types.Command; import org.openhab.core.types.RefreshType; +import org.openhab.core.types.State; import org.openhab.core.types.UnDefType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -155,8 +157,10 @@ public void onApplianceStateChanged(FullyQualifiedApplianceIdentifier applicatio for (JsonElement prop : dco.Properties.getAsJsonArray()) { try { DeviceProperty dp = gson.fromJson(prop, DeviceProperty.class); - dp.Value = StringUtils.trim(dp.Value); - dp.Value = StringUtils.strip(dp.Value); + if (!dp.Name.equals(EXTENDED_DEVICE_STATE_PROPERTY_NAME)) { + dp.Value = StringUtils.trim(dp.Value); + dp.Value = StringUtils.strip(dp.Value); + } onAppliancePropertyChanged(applicationIdentifier, dp); } catch (Exception p) { @@ -211,6 +215,18 @@ private void onAppliancePropertyChanged(DeviceProperty dp) { metaDataCache.put(new StringBuilder().append(dp.Name).toString().trim(), metadata); } + if (dp.Name.equals(EXTENDED_DEVICE_STATE_PROPERTY_NAME)) { + if (!dp.Value.isEmpty()) { + byte[] extendedStateBytes = ExtendedDeviceStateUtil.stringToBytes(dp.Value); + logger.trace("Extended device state for {}: {}", getThing().getUID(), + ExtendedDeviceStateUtil.bytesToHex(extendedStateBytes)); + if (this instanceof ExtendedDeviceStateListener) { + ((ExtendedDeviceStateListener) this).onApplianceExtendedStateChanged(extendedStateBytes); + } + } + return; + } + ApplianceChannelSelector selector = null; try { selector = getValueSelectorFromMieleID(dp.Name); @@ -244,6 +260,12 @@ private void onAppliancePropertyChanged(DeviceProperty dp) { } } + protected void updateExtendedState(String channelId, State state) { + ChannelUID channelUid = new ChannelUID(getThing().getUID(), channelId); + logger.trace("Update state of {} with extended state '{}'", channelUid, state); + updateState(channelUid, state); + } + @Override public void onApplianceRemoved(HomeDevice appliance) { if (applianceId == null) { diff --git a/bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/handler/OvenChannelSelector.java b/bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/handler/OvenChannelSelector.java index 255275e2dee27..5749b303bc4c5 100644 --- a/bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/handler/OvenChannelSelector.java +++ b/bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/handler/OvenChannelSelector.java @@ -185,6 +185,11 @@ public boolean isProperty() { return isProperty; } + @Override + public boolean isExtendedState() { + return false; + } + @Override public State getState(String s, DeviceMetaData dmd) { if (dmd != null) { diff --git a/bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/handler/TumbleDryerChannelSelector.java b/bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/handler/TumbleDryerChannelSelector.java index c8a4f18f0b254..e6b2b5e2c0856 100644 --- a/bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/handler/TumbleDryerChannelSelector.java +++ b/bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/handler/TumbleDryerChannelSelector.java @@ -167,6 +167,11 @@ public boolean isProperty() { return isProperty; } + @Override + public boolean isExtendedState() { + return false; + } + @Override public State getState(String s, DeviceMetaData dmd) { if (dmd != null) { diff --git a/bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/handler/WashingMachineChannelSelector.java b/bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/handler/WashingMachineChannelSelector.java index 7f1e43fee6478..d54c19236b91e 100644 --- a/bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/handler/WashingMachineChannelSelector.java +++ b/bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/handler/WashingMachineChannelSelector.java @@ -12,6 +12,10 @@ */ package org.openhab.binding.miele.internal.handler; +import static org.openhab.binding.miele.internal.MieleBindingConstants.EXTENDED_DEVICE_STATE_PROPERTY_NAME; +import static org.openhab.binding.miele.internal.MieleBindingConstants.POWER_CONSUMPTION_CHANNEL_ID; +import static org.openhab.binding.miele.internal.MieleBindingConstants.WATER_CONSUMPTION_CHANNEL_ID; + import java.lang.reflect.Method; import java.text.SimpleDateFormat; import java.util.Date; @@ -24,6 +28,7 @@ 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.QuantityType; import org.openhab.core.library.types.StringType; import org.openhab.core.types.State; import org.openhab.core.types.Type; @@ -38,18 +43,19 @@ * * @author Karel Goderis - Initial contribution * @author Kai Kreuzer - Changed START_TIME to DateTimeType + * @author Jacob Laursen - Added power/water consumption channels */ public enum WashingMachineChannelSelector implements ApplianceChannelSelector { - PRODUCT_TYPE("productTypeId", "productType", StringType.class, true), - DEVICE_TYPE("mieleDeviceType", "deviceType", StringType.class, true), - BRAND_ID("brandId", "brandId", StringType.class, true), - COMPANY_ID("companyId", "companyId", StringType.class, true), - STATE("state", "state", StringType.class, false), - PROGRAMID("programId", "program", StringType.class, false), - PROGRAMTYPE("programType", "type", StringType.class, false), - PROGRAMPHASE("phase", "phase", StringType.class, false), - START_TIME("startTime", "start", DateTimeType.class, false) { + PRODUCT_TYPE("productTypeId", "productType", StringType.class, true, false), + DEVICE_TYPE("mieleDeviceType", "deviceType", StringType.class, true, false), + BRAND_ID("brandId", "brandId", StringType.class, true, false), + COMPANY_ID("companyId", "companyId", StringType.class, true, false), + STATE("state", "state", StringType.class, false, false), + PROGRAMID("programId", "program", StringType.class, false, false), + PROGRAMTYPE("programType", "type", StringType.class, false, false), + PROGRAMPHASE("phase", "phase", StringType.class, false, false), + START_TIME("startTime", "start", DateTimeType.class, false, false) { @Override public State getState(String s, DeviceMetaData dmd) { Date date = new Date(); @@ -63,7 +69,7 @@ public State getState(String s, DeviceMetaData dmd) { return getState(dateFormatter.format(date)); } }, - DURATION("duration", "duration", DateTimeType.class, false) { + DURATION("duration", "duration", DateTimeType.class, false, false) { @Override public State getState(String s, DeviceMetaData dmd) { Date date = new Date(); @@ -77,7 +83,7 @@ public State getState(String s, DeviceMetaData dmd) { return getState(dateFormatter.format(date)); } }, - ELAPSED_TIME("elapsedTime", "elapsed", DateTimeType.class, false) { + ELAPSED_TIME("elapsedTime", "elapsed", DateTimeType.class, false, false) { @Override public State getState(String s, DeviceMetaData dmd) { Date date = new Date(); @@ -91,7 +97,7 @@ public State getState(String s, DeviceMetaData dmd) { return getState(dateFormatter.format(date)); } }, - FINISH_TIME("finishTime", "finish", DateTimeType.class, false) { + FINISH_TIME("finishTime", "finish", DateTimeType.class, false, false) { @Override public State getState(String s, DeviceMetaData dmd) { Date date = new Date(); @@ -105,13 +111,13 @@ public State getState(String s, DeviceMetaData dmd) { return getState(dateFormatter.format(date)); } }, - TARGET_TEMP("targetTemperature", "target", DecimalType.class, false) { + TARGET_TEMP("targetTemperature", "target", DecimalType.class, false, false) { @Override public State getState(String s, DeviceMetaData dmd) { return getState(s); } }, - SPINNING_SPEED("spinningSpeed", "spinningspeed", StringType.class, false) { + SPINNING_SPEED("spinningSpeed", "spinningspeed", StringType.class, false, false) { @Override public State getState(String s, DeviceMetaData dmd) { if ("0".equals(s)) { @@ -123,7 +129,7 @@ public State getState(String s, DeviceMetaData dmd) { return getState(Integer.toString((Integer.valueOf(s) * 10))); } }, - DOOR("signalDoor", "door", OpenClosedType.class, false) { + DOOR("signalDoor", "door", OpenClosedType.class, false, false) { @Override public State getState(String s, DeviceMetaData dmd) { @@ -138,7 +144,11 @@ public State getState(String s, DeviceMetaData dmd) { return UnDefType.UNDEF; } }, - SWITCH(null, "switch", OnOffType.class, false); + SWITCH(null, "switch", OnOffType.class, false, false), + POWER_CONSUMPTION(EXTENDED_DEVICE_STATE_PROPERTY_NAME, POWER_CONSUMPTION_CHANNEL_ID, QuantityType.class, false, + true), + WATER_CONSUMPTION(EXTENDED_DEVICE_STATE_PROPERTY_NAME, WATER_CONSUMPTION_CHANNEL_ID, QuantityType.class, false, + true); private final Logger logger = LoggerFactory.getLogger(WashingMachineChannelSelector.class); @@ -146,13 +156,15 @@ public State getState(String s, DeviceMetaData dmd) { private final String channelID; private final Class typeClass; private final boolean isProperty; + private final boolean isExtendedState; WashingMachineChannelSelector(String propertyID, String channelID, Class typeClass, - boolean isProperty) { + boolean isProperty, boolean isExtendedState) { this.mieleID = propertyID; this.channelID = channelID; this.typeClass = typeClass; this.isProperty = isProperty; + this.isExtendedState = isExtendedState; } @Override @@ -180,6 +192,11 @@ public boolean isProperty() { return isProperty; } + @Override + public boolean isExtendedState() { + return isExtendedState; + } + @Override public State getState(String s, DeviceMetaData dmd) { if (dmd != null) { diff --git a/bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/handler/WashingMachineHandler.java b/bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/handler/WashingMachineHandler.java index 4abea606bb7fa..b7145a3196eea 100644 --- a/bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/handler/WashingMachineHandler.java +++ b/bundles/org.openhab.binding.miele/src/main/java/org/openhab/binding/miele/internal/handler/WashingMachineHandler.java @@ -13,10 +13,16 @@ package org.openhab.binding.miele.internal.handler; import static org.openhab.binding.miele.internal.MieleBindingConstants.APPLIANCE_ID; +import static org.openhab.binding.miele.internal.MieleBindingConstants.POWER_CONSUMPTION_CHANNEL_ID; import static org.openhab.binding.miele.internal.MieleBindingConstants.PROTOCOL_PROPERTY_NAME; +import static org.openhab.binding.miele.internal.MieleBindingConstants.WATER_CONSUMPTION_CHANNEL_ID; + +import java.math.BigDecimal; import org.openhab.binding.miele.internal.FullyQualifiedApplianceIdentifier; import org.openhab.core.library.types.OnOffType; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.library.unit.Units; import org.openhab.core.thing.ChannelUID; import org.openhab.core.thing.Thing; import org.openhab.core.types.Command; @@ -33,9 +39,14 @@ * @author Karel Goderis - Initial contribution * @author Kai Kreuzer - fixed handling of REFRESH commands * @author Martin Lepsy - fixed handling of empty JSON results - * @author Jacob Laursen - Fixed multicast and protocol support (ZigBee/LAN) + * @author Jacob Laursen - Fixed multicast and protocol support (ZigBee/LAN), added power/water consumption channels **/ -public class WashingMachineHandler extends MieleApplianceHandler { +public class WashingMachineHandler extends MieleApplianceHandler + implements ExtendedDeviceStateListener { + + private static final int POWER_CONSUMPTION_BYTE_POSITION = 51; + private static final int WATER_CONSUMPTION_BYTE_POSITION = 53; + private static final int EXTENDED_STATE_SIZE_BYTES = 59; private final Logger logger = LoggerFactory.getLogger(WashingMachineHandler.class); @@ -85,4 +96,20 @@ public void handleCommand(ChannelUID channelUID, Command command) { channelID, command.toString()); } } + + public void onApplianceExtendedStateChanged(byte[] extendedDeviceState) { + if (extendedDeviceState.length != EXTENDED_STATE_SIZE_BYTES) { + logger.error("Unexpected size of extended state: {}", extendedDeviceState); + return; + } + + BigDecimal kiloWattHoursTenths = BigDecimal + .valueOf(extendedDeviceState[POWER_CONSUMPTION_BYTE_POSITION] & 0xff); + var kiloWattHours = new QuantityType<>(kiloWattHoursTenths.divide(BigDecimal.valueOf(10)), Units.KILOWATT_HOUR); + updateExtendedState(POWER_CONSUMPTION_CHANNEL_ID, kiloWattHours); + + var litres = new QuantityType<>(BigDecimal.valueOf(extendedDeviceState[WATER_CONSUMPTION_BYTE_POSITION] & 0xff), + Units.LITRE); + updateExtendedState(WATER_CONSUMPTION_CHANNEL_ID, litres); + } } diff --git a/bundles/org.openhab.binding.miele/src/main/resources/OH-INF/thing/channeltypes.xml b/bundles/org.openhab.binding.miele/src/main/resources/OH-INF/thing/channeltypes.xml index f507cfe41c64b..38b136e7dbe6a 100644 --- a/bundles/org.openhab.binding.miele/src/main/resources/OH-INF/thing/channeltypes.xml +++ b/bundles/org.openhab.binding.miele/src/main/resources/OH-INF/thing/channeltypes.xml @@ -212,4 +212,18 @@ + + Number:Power + + Power consumption by the currently running program on the appliance + + + + + Number:Volume + + Water consumption by the currently running program on the appliance + + + diff --git a/bundles/org.openhab.binding.miele/src/main/resources/OH-INF/thing/dishwasher.xml b/bundles/org.openhab.binding.miele/src/main/resources/OH-INF/thing/dishwasher.xml index e87446bef61e4..f081d6146fbf2 100644 --- a/bundles/org.openhab.binding.miele/src/main/resources/OH-INF/thing/dishwasher.xml +++ b/bundles/org.openhab.binding.miele/src/main/resources/OH-INF/thing/dishwasher.xml @@ -23,6 +23,8 @@ + + uid diff --git a/bundles/org.openhab.binding.miele/src/main/resources/OH-INF/thing/washingmachine.xml b/bundles/org.openhab.binding.miele/src/main/resources/OH-INF/thing/washingmachine.xml index deafab98af24d..cb138659676ae 100644 --- a/bundles/org.openhab.binding.miele/src/main/resources/OH-INF/thing/washingmachine.xml +++ b/bundles/org.openhab.binding.miele/src/main/resources/OH-INF/thing/washingmachine.xml @@ -26,6 +26,8 @@ + + uid diff --git a/bundles/org.openhab.binding.miele/src/test/java/org/openhab/binding/miele/internal/ExtendedDeviceStateUtilTest.java b/bundles/org.openhab.binding.miele/src/test/java/org/openhab/binding/miele/internal/ExtendedDeviceStateUtilTest.java new file mode 100644 index 0000000000000..5bb52b743e4a5 --- /dev/null +++ b/bundles/org.openhab.binding.miele/src/test/java/org/openhab/binding/miele/internal/ExtendedDeviceStateUtilTest.java @@ -0,0 +1,48 @@ +/** + * Copyright (c) 2010-2021 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.binding.miele.internal; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; +import org.openhab.core.test.java.JavaTest; + +/** + * This class provides test cases for {@link + * org.openhab.binding.miele.internal.ExtendedDeviceStateUtil} + * + * @author Jacob Laursen - Added power/water consumption channels + */ + +public class ExtendedDeviceStateUtilTest extends JavaTest { + + @Test + public void bytesToHexWhenTopBitIsUsedReturnsCorrectString() { + String actual = ExtendedDeviceStateUtil + .bytesToHex(new byte[] { (byte) 0xde, (byte) 0xad, (byte) 0xbe, (byte) 0xef }); + assertEquals("DEADBEEF", actual); + } + + /** + * This test guards that the UTF-16 returned by the RPC-JSON API will be + * considered as a sequence of 8-bit characters and converted into bytes + * accordingly. Default behaviour of String.getBytes() assumes UTF-8 + * and adds a 0xc2 byte before any character out of ASCII range. + */ + @Test + public void stringToBytesWhenTopBitIsUsedReturnsSingleByte() { + byte[] expected = new byte[] { (byte) 0x00, (byte) 0x80, (byte) 0x00 }; + byte[] actual = ExtendedDeviceStateUtil.stringToBytes("\u0000\u0080\u0000"); + assertArrayEquals(expected, actual); + } +}