Add new channels for water and power consumption for washing machines and dishwashers. (#11298)

Fixes #11297

Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk>
This commit is contained in:
jlaur 2021-09-25 21:09:26 +02:00 committed by GitHub
parent 8440745ceb
commit 27383fcd64
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 333 additions and 38 deletions

View File

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

View File

@ -31,6 +31,11 @@ public class MieleBindingConstants {
public static final String DEVICE_CLASS = "dc"; public static final String DEVICE_CLASS = "dc";
public static final String PROTOCOL_PROPERTY_NAME = "protocol"; public static final String PROTOCOL_PROPERTY_NAME = "protocol";
public static final String SERIAL_NUMBER_PROPERTY_NAME = "serialNumber"; 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 // List of all Thing Type UIDs
public static final ThingTypeUID THING_TYPE_XGW3000 = new ThingTypeUID(BINDING_ID, "xgw3000"); public static final ThingTypeUID THING_TYPE_XGW3000 = new ThingTypeUID(BINDING_ID, "xgw3000");

View File

@ -23,6 +23,7 @@ import org.openhab.core.types.Type;
* returned by the appliance to a compatible State * returned by the appliance to a compatible State
* *
* @author Karel Goderis - Initial contribution * @author Karel Goderis - Initial contribution
* @author Jacob Laursen - Added power/water consumption channels
*/ */
public interface ApplianceChannelSelector { public interface ApplianceChannelSelector {
@ -45,6 +46,12 @@ public interface ApplianceChannelSelector {
*/ */
boolean isProperty(); 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 * Returns a State for the given string, taking into

View File

@ -99,6 +99,11 @@ public enum CoffeeMachineChannelSelector implements ApplianceChannelSelector {
return isProperty; return isProperty;
} }
@Override
public boolean isExtendedState() {
return false;
}
@Override @Override
public State getState(String s, DeviceMetaData dmd) { public State getState(String s, DeviceMetaData dmd) {
if (dmd != null) { if (dmd != null) {

View File

@ -13,10 +13,16 @@
package org.openhab.binding.miele.internal.handler; 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.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.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.binding.miele.internal.FullyQualifiedApplianceIdentifier;
import org.openhab.core.library.types.OnOffType; 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.ChannelUID;
import org.openhab.core.thing.Thing; import org.openhab.core.thing.Thing;
import org.openhab.core.types.Command; import org.openhab.core.types.Command;
@ -33,9 +39,14 @@ import com.google.gson.JsonElement;
* @author Karel Goderis - Initial contribution * @author Karel Goderis - Initial contribution
* @author Kai Kreuzer - fixed handling of REFRESH commands * @author Kai Kreuzer - fixed handling of REFRESH commands
* @author Martin Lepsy - fixed handling of empty JSON results * @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<DishwasherChannelSelector> { public class DishWasherHandler extends MieleApplianceHandler<DishwasherChannelSelector>
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); private final Logger logger = LoggerFactory.getLogger(DishWasherHandler.class);
@ -84,4 +95,20 @@ public class DishWasherHandler extends MieleApplianceHandler<DishwasherChannelSe
channelID, command.toString()); 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);
}
} }

View File

@ -12,6 +12,10 @@
*/ */
package org.openhab.binding.miele.internal.handler; 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.lang.reflect.Method;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.Date; import java.util.Date;
@ -22,6 +26,7 @@ import org.openhab.binding.miele.internal.handler.MieleBridgeHandler.DeviceMetaD
import org.openhab.core.library.types.DateTimeType; import org.openhab.core.library.types.DateTimeType;
import org.openhab.core.library.types.OnOffType; import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.OpenClosedType; 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.library.types.StringType;
import org.openhab.core.types.State; import org.openhab.core.types.State;
import org.openhab.core.types.Type; import org.openhab.core.types.Type;
@ -36,17 +41,18 @@ import com.google.gson.JsonElement;
* *
* @author Karel Goderis - Initial contribution * @author Karel Goderis - Initial contribution
* @author Kai Kreuzer - Changed START_TIME to DateTimeType * @author Kai Kreuzer - Changed START_TIME to DateTimeType
* @author Jacob Laursen - Added power/water consumption channels
*/ */
public enum DishwasherChannelSelector implements ApplianceChannelSelector { public enum DishwasherChannelSelector implements ApplianceChannelSelector {
PRODUCT_TYPE("productTypeId", "productType", StringType.class, true), PRODUCT_TYPE("productTypeId", "productType", StringType.class, true, false),
DEVICE_TYPE("mieleDeviceType", "deviceType", StringType.class, true), DEVICE_TYPE("mieleDeviceType", "deviceType", StringType.class, true, false),
BRAND_ID("brandId", "brandId", StringType.class, true), BRAND_ID("brandId", "brandId", StringType.class, true, false),
COMPANY_ID("companyId", "companyId", StringType.class, true), COMPANY_ID("companyId", "companyId", StringType.class, true, false),
STATE("state", "state", StringType.class, false), STATE("state", "state", StringType.class, false, false),
PROGRAMID("programId", "program", StringType.class, false), PROGRAMID("programId", "program", StringType.class, false, false),
PROGRAMPHASE("phase", "phase", StringType.class, false), PROGRAMPHASE("phase", "phase", StringType.class, false, false),
START_TIME("startTime", "start", DateTimeType.class, false) { START_TIME("startTime", "start", DateTimeType.class, false, false) {
@Override @Override
public State getState(String s, DeviceMetaData dmd) { public State getState(String s, DeviceMetaData dmd) {
Date date = new Date(); Date date = new Date();
@ -60,7 +66,7 @@ public enum DishwasherChannelSelector implements ApplianceChannelSelector {
return getState(dateFormatter.format(date)); return getState(dateFormatter.format(date));
} }
}, },
DURATION("duration", "duration", DateTimeType.class, false) { DURATION("duration", "duration", DateTimeType.class, false, false) {
@Override @Override
public State getState(String s, DeviceMetaData dmd) { public State getState(String s, DeviceMetaData dmd) {
Date date = new Date(); Date date = new Date();
@ -74,7 +80,7 @@ public enum DishwasherChannelSelector implements ApplianceChannelSelector {
return getState(dateFormatter.format(date)); return getState(dateFormatter.format(date));
} }
}, },
ELAPSED_TIME("elapsedTime", "elapsed", DateTimeType.class, false) { ELAPSED_TIME("elapsedTime", "elapsed", DateTimeType.class, false, false) {
@Override @Override
public State getState(String s, DeviceMetaData dmd) { public State getState(String s, DeviceMetaData dmd) {
Date date = new Date(); Date date = new Date();
@ -88,7 +94,7 @@ public enum DishwasherChannelSelector implements ApplianceChannelSelector {
return getState(dateFormatter.format(date)); return getState(dateFormatter.format(date));
} }
}, },
FINISH_TIME("finishTime", "finish", DateTimeType.class, false) { FINISH_TIME("finishTime", "finish", DateTimeType.class, false, false) {
@Override @Override
public State getState(String s, DeviceMetaData dmd) { public State getState(String s, DeviceMetaData dmd) {
Date date = new Date(); Date date = new Date();
@ -102,7 +108,7 @@ public enum DishwasherChannelSelector implements ApplianceChannelSelector {
return getState(dateFormatter.format(date)); return getState(dateFormatter.format(date));
} }
}, },
DOOR("signalDoor", "door", OpenClosedType.class, false) { DOOR("signalDoor", "door", OpenClosedType.class, false, false) {
@Override @Override
public State getState(String s, DeviceMetaData dmd) { public State getState(String s, DeviceMetaData dmd) {
if ("true".equals(s)) { if ("true".equals(s)) {
@ -116,7 +122,11 @@ public enum DishwasherChannelSelector implements ApplianceChannelSelector {
return UnDefType.UNDEF; 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); private final Logger logger = LoggerFactory.getLogger(DishwasherChannelSelector.class);
@ -124,13 +134,15 @@ public enum DishwasherChannelSelector implements ApplianceChannelSelector {
private final String channelID; private final String channelID;
private final Class<? extends Type> typeClass; private final Class<? extends Type> typeClass;
private final boolean isProperty; private final boolean isProperty;
private final boolean isExtendedState;
DishwasherChannelSelector(String propertyID, String channelID, Class<? extends Type> typeClass, DishwasherChannelSelector(String propertyID, String channelID, Class<? extends Type> typeClass, boolean isProperty,
boolean isProperty) { boolean isExtendedState) {
this.mieleID = propertyID; this.mieleID = propertyID;
this.channelID = channelID; this.channelID = channelID;
this.typeClass = typeClass; this.typeClass = typeClass;
this.isProperty = isProperty; this.isProperty = isProperty;
this.isExtendedState = isExtendedState;
} }
@Override @Override
@ -158,6 +170,11 @@ public enum DishwasherChannelSelector implements ApplianceChannelSelector {
return isProperty; return isProperty;
} }
@Override
public boolean isExtendedState() {
return isExtendedState;
}
@Override @Override
public State getState(String s, DeviceMetaData dmd) { public State getState(String s, DeviceMetaData dmd) {
if (dmd != null) { if (dmd != null) {

View File

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

View File

@ -109,6 +109,11 @@ public enum FridgeChannelSelector implements ApplianceChannelSelector {
return isProperty; return isProperty;
} }
@Override
public boolean isExtendedState() {
return false;
}
@Override @Override
public State getState(String s, DeviceMetaData dmd) { public State getState(String s, DeviceMetaData dmd) {
if (dmd != null) { if (dmd != null) {

View File

@ -126,6 +126,11 @@ public enum FridgeFreezerChannelSelector implements ApplianceChannelSelector {
return isProperty; return isProperty;
} }
@Override
public boolean isExtendedState() {
return false;
}
@Override @Override
public State getState(String s, DeviceMetaData dmd) { public State getState(String s, DeviceMetaData dmd) {
if (dmd != null) { if (dmd != null) {

View File

@ -129,6 +129,11 @@ public enum HobChannelSelector implements ApplianceChannelSelector {
return isProperty; return isProperty;
} }
@Override
public boolean isExtendedState() {
return false;
}
@Override @Override
public State getState(String s, DeviceMetaData dmd) { public State getState(String s, DeviceMetaData dmd) {
if (dmd != null) { if (dmd != null) {

View File

@ -96,6 +96,11 @@ public enum HoodChannelSelector implements ApplianceChannelSelector {
return isProperty; return isProperty;
} }
@Override
public boolean isExtendedState() {
return false;
}
@Override @Override
public State getState(String s, DeviceMetaData dmd) { public State getState(String s, DeviceMetaData dmd) {
if (dmd != null) { if (dmd != null) {

View File

@ -21,6 +21,7 @@ import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
import org.apache.commons.lang3.StringUtils; 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.FullyQualifiedApplianceIdentifier;
import org.openhab.binding.miele.internal.handler.MieleBridgeHandler.DeviceClassObject; import org.openhab.binding.miele.internal.handler.MieleBridgeHandler.DeviceClassObject;
import org.openhab.binding.miele.internal.handler.MieleBridgeHandler.DeviceMetaData; import org.openhab.binding.miele.internal.handler.MieleBridgeHandler.DeviceMetaData;
@ -36,6 +37,7 @@ import org.openhab.core.thing.binding.BaseThingHandler;
import org.openhab.core.thing.binding.ThingHandler; import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.types.Command; import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType; import org.openhab.core.types.RefreshType;
import org.openhab.core.types.State;
import org.openhab.core.types.UnDefType; import org.openhab.core.types.UnDefType;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -155,8 +157,10 @@ public abstract class MieleApplianceHandler<E extends Enum<E> & ApplianceChannel
for (JsonElement prop : dco.Properties.getAsJsonArray()) { for (JsonElement prop : dco.Properties.getAsJsonArray()) {
try { try {
DeviceProperty dp = gson.fromJson(prop, DeviceProperty.class); DeviceProperty dp = gson.fromJson(prop, DeviceProperty.class);
dp.Value = StringUtils.trim(dp.Value); if (!dp.Name.equals(EXTENDED_DEVICE_STATE_PROPERTY_NAME)) {
dp.Value = StringUtils.strip(dp.Value); dp.Value = StringUtils.trim(dp.Value);
dp.Value = StringUtils.strip(dp.Value);
}
onAppliancePropertyChanged(applicationIdentifier, dp); onAppliancePropertyChanged(applicationIdentifier, dp);
} catch (Exception p) { } catch (Exception p) {
@ -211,6 +215,18 @@ public abstract class MieleApplianceHandler<E extends Enum<E> & ApplianceChannel
metaDataCache.put(new StringBuilder().append(dp.Name).toString().trim(), metadata); 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; ApplianceChannelSelector selector = null;
try { try {
selector = getValueSelectorFromMieleID(dp.Name); selector = getValueSelectorFromMieleID(dp.Name);
@ -244,6 +260,12 @@ public abstract class MieleApplianceHandler<E extends Enum<E> & ApplianceChannel
} }
} }
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 @Override
public void onApplianceRemoved(HomeDevice appliance) { public void onApplianceRemoved(HomeDevice appliance) {
if (applianceId == null) { if (applianceId == null) {

View File

@ -185,6 +185,11 @@ public enum OvenChannelSelector implements ApplianceChannelSelector {
return isProperty; return isProperty;
} }
@Override
public boolean isExtendedState() {
return false;
}
@Override @Override
public State getState(String s, DeviceMetaData dmd) { public State getState(String s, DeviceMetaData dmd) {
if (dmd != null) { if (dmd != null) {

View File

@ -167,6 +167,11 @@ public enum TumbleDryerChannelSelector implements ApplianceChannelSelector {
return isProperty; return isProperty;
} }
@Override
public boolean isExtendedState() {
return false;
}
@Override @Override
public State getState(String s, DeviceMetaData dmd) { public State getState(String s, DeviceMetaData dmd) {
if (dmd != null) { if (dmd != null) {

View File

@ -12,6 +12,10 @@
*/ */
package org.openhab.binding.miele.internal.handler; 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.lang.reflect.Method;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.Date; import java.util.Date;
@ -24,6 +28,7 @@ import org.openhab.core.library.types.DateTimeType;
import org.openhab.core.library.types.DecimalType; import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.OnOffType; import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.OpenClosedType; 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.library.types.StringType;
import org.openhab.core.types.State; import org.openhab.core.types.State;
import org.openhab.core.types.Type; import org.openhab.core.types.Type;
@ -38,18 +43,19 @@ import com.google.gson.JsonElement;
* *
* @author Karel Goderis - Initial contribution * @author Karel Goderis - Initial contribution
* @author Kai Kreuzer - Changed START_TIME to DateTimeType * @author Kai Kreuzer - Changed START_TIME to DateTimeType
* @author Jacob Laursen - Added power/water consumption channels
*/ */
public enum WashingMachineChannelSelector implements ApplianceChannelSelector { public enum WashingMachineChannelSelector implements ApplianceChannelSelector {
PRODUCT_TYPE("productTypeId", "productType", StringType.class, true), PRODUCT_TYPE("productTypeId", "productType", StringType.class, true, false),
DEVICE_TYPE("mieleDeviceType", "deviceType", StringType.class, true), DEVICE_TYPE("mieleDeviceType", "deviceType", StringType.class, true, false),
BRAND_ID("brandId", "brandId", StringType.class, true), BRAND_ID("brandId", "brandId", StringType.class, true, false),
COMPANY_ID("companyId", "companyId", StringType.class, true), COMPANY_ID("companyId", "companyId", StringType.class, true, false),
STATE("state", "state", StringType.class, false), STATE("state", "state", StringType.class, false, false),
PROGRAMID("programId", "program", StringType.class, false), PROGRAMID("programId", "program", StringType.class, false, false),
PROGRAMTYPE("programType", "type", StringType.class, false), PROGRAMTYPE("programType", "type", StringType.class, false, false),
PROGRAMPHASE("phase", "phase", StringType.class, false), PROGRAMPHASE("phase", "phase", StringType.class, false, false),
START_TIME("startTime", "start", DateTimeType.class, false) { START_TIME("startTime", "start", DateTimeType.class, false, false) {
@Override @Override
public State getState(String s, DeviceMetaData dmd) { public State getState(String s, DeviceMetaData dmd) {
Date date = new Date(); Date date = new Date();
@ -63,7 +69,7 @@ public enum WashingMachineChannelSelector implements ApplianceChannelSelector {
return getState(dateFormatter.format(date)); return getState(dateFormatter.format(date));
} }
}, },
DURATION("duration", "duration", DateTimeType.class, false) { DURATION("duration", "duration", DateTimeType.class, false, false) {
@Override @Override
public State getState(String s, DeviceMetaData dmd) { public State getState(String s, DeviceMetaData dmd) {
Date date = new Date(); Date date = new Date();
@ -77,7 +83,7 @@ public enum WashingMachineChannelSelector implements ApplianceChannelSelector {
return getState(dateFormatter.format(date)); return getState(dateFormatter.format(date));
} }
}, },
ELAPSED_TIME("elapsedTime", "elapsed", DateTimeType.class, false) { ELAPSED_TIME("elapsedTime", "elapsed", DateTimeType.class, false, false) {
@Override @Override
public State getState(String s, DeviceMetaData dmd) { public State getState(String s, DeviceMetaData dmd) {
Date date = new Date(); Date date = new Date();
@ -91,7 +97,7 @@ public enum WashingMachineChannelSelector implements ApplianceChannelSelector {
return getState(dateFormatter.format(date)); return getState(dateFormatter.format(date));
} }
}, },
FINISH_TIME("finishTime", "finish", DateTimeType.class, false) { FINISH_TIME("finishTime", "finish", DateTimeType.class, false, false) {
@Override @Override
public State getState(String s, DeviceMetaData dmd) { public State getState(String s, DeviceMetaData dmd) {
Date date = new Date(); Date date = new Date();
@ -105,13 +111,13 @@ public enum WashingMachineChannelSelector implements ApplianceChannelSelector {
return getState(dateFormatter.format(date)); return getState(dateFormatter.format(date));
} }
}, },
TARGET_TEMP("targetTemperature", "target", DecimalType.class, false) { TARGET_TEMP("targetTemperature", "target", DecimalType.class, false, false) {
@Override @Override
public State getState(String s, DeviceMetaData dmd) { public State getState(String s, DeviceMetaData dmd) {
return getState(s); return getState(s);
} }
}, },
SPINNING_SPEED("spinningSpeed", "spinningspeed", StringType.class, false) { SPINNING_SPEED("spinningSpeed", "spinningspeed", StringType.class, false, false) {
@Override @Override
public State getState(String s, DeviceMetaData dmd) { public State getState(String s, DeviceMetaData dmd) {
if ("0".equals(s)) { if ("0".equals(s)) {
@ -123,7 +129,7 @@ public enum WashingMachineChannelSelector implements ApplianceChannelSelector {
return getState(Integer.toString((Integer.valueOf(s) * 10))); return getState(Integer.toString((Integer.valueOf(s) * 10)));
} }
}, },
DOOR("signalDoor", "door", OpenClosedType.class, false) { DOOR("signalDoor", "door", OpenClosedType.class, false, false) {
@Override @Override
public State getState(String s, DeviceMetaData dmd) { public State getState(String s, DeviceMetaData dmd) {
@ -138,7 +144,11 @@ public enum WashingMachineChannelSelector implements ApplianceChannelSelector {
return UnDefType.UNDEF; 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); private final Logger logger = LoggerFactory.getLogger(WashingMachineChannelSelector.class);
@ -146,13 +156,15 @@ public enum WashingMachineChannelSelector implements ApplianceChannelSelector {
private final String channelID; private final String channelID;
private final Class<? extends Type> typeClass; private final Class<? extends Type> typeClass;
private final boolean isProperty; private final boolean isProperty;
private final boolean isExtendedState;
WashingMachineChannelSelector(String propertyID, String channelID, Class<? extends Type> typeClass, WashingMachineChannelSelector(String propertyID, String channelID, Class<? extends Type> typeClass,
boolean isProperty) { boolean isProperty, boolean isExtendedState) {
this.mieleID = propertyID; this.mieleID = propertyID;
this.channelID = channelID; this.channelID = channelID;
this.typeClass = typeClass; this.typeClass = typeClass;
this.isProperty = isProperty; this.isProperty = isProperty;
this.isExtendedState = isExtendedState;
} }
@Override @Override
@ -180,6 +192,11 @@ public enum WashingMachineChannelSelector implements ApplianceChannelSelector {
return isProperty; return isProperty;
} }
@Override
public boolean isExtendedState() {
return isExtendedState;
}
@Override @Override
public State getState(String s, DeviceMetaData dmd) { public State getState(String s, DeviceMetaData dmd) {
if (dmd != null) { if (dmd != null) {

View File

@ -13,10 +13,16 @@
package org.openhab.binding.miele.internal.handler; 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.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.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.binding.miele.internal.FullyQualifiedApplianceIdentifier;
import org.openhab.core.library.types.OnOffType; 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.ChannelUID;
import org.openhab.core.thing.Thing; import org.openhab.core.thing.Thing;
import org.openhab.core.types.Command; import org.openhab.core.types.Command;
@ -33,9 +39,14 @@ import com.google.gson.JsonElement;
* @author Karel Goderis - Initial contribution * @author Karel Goderis - Initial contribution
* @author Kai Kreuzer - fixed handling of REFRESH commands * @author Kai Kreuzer - fixed handling of REFRESH commands
* @author Martin Lepsy - fixed handling of empty JSON results * @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<WashingMachineChannelSelector> { public class WashingMachineHandler extends MieleApplianceHandler<WashingMachineChannelSelector>
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); private final Logger logger = LoggerFactory.getLogger(WashingMachineHandler.class);
@ -85,4 +96,20 @@ public class WashingMachineHandler extends MieleApplianceHandler<WashingMachineC
channelID, command.toString()); 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);
}
} }

View File

@ -212,4 +212,18 @@
<state readOnly="true"></state> <state readOnly="true"></state>
</channel-type> </channel-type>
<channel-type id="powerConsumption" advanced="false">
<item-type>Number:Power</item-type>
<label>Power Consumption</label>
<description>Power consumption by the currently running program on the appliance</description>
<state readOnly="true" pattern="%.1f %unit%"/>
</channel-type>
<channel-type id="waterConsumption" advanced="false">
<item-type>Number:Volume</item-type>
<label>Water Consumption</label>
<description>Water consumption by the currently running program on the appliance</description>
<state readOnly="true" pattern="%.1f %unit%"/>
</channel-type>
</thing:thing-descriptions> </thing:thing-descriptions>

View File

@ -23,6 +23,8 @@
<channel id="finish" typeId="finish"/> <channel id="finish" typeId="finish"/>
<channel id="door" typeId="door"/> <channel id="door" typeId="door"/>
<channel id="switch" typeId="switch"/> <channel id="switch" typeId="switch"/>
<channel id="powerConsumption" typeId="powerConsumption"/>
<channel id="waterConsumption" typeId="waterConsumption"/>
</channels> </channels>
<representation-property>uid</representation-property> <representation-property>uid</representation-property>

View File

@ -26,6 +26,8 @@
<channel id="switch" typeId="switch"/> <channel id="switch" typeId="switch"/>
<channel id="target" typeId="target"/> <channel id="target" typeId="target"/>
<channel id="spinningspeed" typeId="spinningspeed"/> <channel id="spinningspeed" typeId="spinningspeed"/>
<channel id="powerConsumption" typeId="powerConsumption"/>
<channel id="waterConsumption" typeId="waterConsumption"/>
</channels> </channels>
<representation-property>uid</representation-property> <representation-property>uid</representation-property>

View File

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