2 * Copyright (c) 2010-2021 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.miele.internal.handler;
15 import static org.openhab.binding.miele.internal.MieleBindingConstants.*;
17 import java.util.HashMap;
20 import java.util.stream.Collectors;
21 import java.util.stream.Stream;
23 import org.eclipse.jdt.annotation.NonNull;
24 import org.openhab.binding.miele.internal.DeviceUtil;
25 import org.openhab.binding.miele.internal.FullyQualifiedApplianceIdentifier;
26 import org.openhab.binding.miele.internal.handler.MieleBridgeHandler.DeviceClassObject;
27 import org.openhab.binding.miele.internal.handler.MieleBridgeHandler.DeviceMetaData;
28 import org.openhab.binding.miele.internal.handler.MieleBridgeHandler.DeviceProperty;
29 import org.openhab.binding.miele.internal.handler.MieleBridgeHandler.HomeDevice;
30 import org.openhab.core.thing.Bridge;
31 import org.openhab.core.thing.ChannelUID;
32 import org.openhab.core.thing.Thing;
33 import org.openhab.core.thing.ThingStatus;
34 import org.openhab.core.thing.ThingStatusInfo;
35 import org.openhab.core.thing.ThingTypeUID;
36 import org.openhab.core.thing.binding.BaseThingHandler;
37 import org.openhab.core.thing.binding.ThingHandler;
38 import org.openhab.core.types.Command;
39 import org.openhab.core.types.RefreshType;
40 import org.openhab.core.types.State;
41 import org.openhab.core.types.UnDefType;
42 import org.slf4j.Logger;
43 import org.slf4j.LoggerFactory;
45 import com.google.gson.Gson;
46 import com.google.gson.JsonElement;
47 import com.google.gson.JsonObject;
48 import com.google.gson.JsonParser;
51 * The {@link MieleApplianceHandler} is an abstract class
52 * responsible for handling commands, which are sent to one
53 * of the channels of the appliance that understands/"talks"
54 * the {@link ApplianceChannelSelector} datapoints
56 * @author Karel Goderis - Initial contribution
57 * @author Martin Lepsy - Added check for JsonNull result
58 * @author Jacob Laursen - Fixed multicast and protocol support (ZigBee/LAN)
60 public abstract class MieleApplianceHandler<E extends Enum<E> & ApplianceChannelSelector> extends BaseThingHandler
61 implements ApplianceStatusListener {
63 private final Logger logger = LoggerFactory.getLogger(MieleApplianceHandler.class);
65 public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Stream
66 .of(THING_TYPE_DISHWASHER, THING_TYPE_OVEN, THING_TYPE_FRIDGE, THING_TYPE_DRYER, THING_TYPE_HOB,
67 THING_TYPE_FRIDGEFREEZER, THING_TYPE_HOOD, THING_TYPE_WASHINGMACHINE, THING_TYPE_COFFEEMACHINE)
68 .collect(Collectors.toSet());
70 protected Gson gson = new Gson();
72 protected String applianceId;
73 protected MieleBridgeHandler bridgeHandler;
74 private Class<E> selectorType;
75 protected String modelID;
77 protected Map<String, String> metaDataCache = new HashMap<>();
79 public MieleApplianceHandler(Thing thing, Class<E> selectorType, String modelID) {
81 this.selectorType = selectorType;
82 this.modelID = modelID;
85 public ApplianceChannelSelector getValueSelectorFromChannelID(String valueSelectorText)
86 throws IllegalArgumentException {
87 E[] enumConstants = selectorType.getEnumConstants();
88 if (enumConstants == null) {
89 throw new IllegalArgumentException(
90 String.format("Could not get enum constants for value selector: %s", valueSelectorText));
92 for (ApplianceChannelSelector c : enumConstants) {
93 if (c != null && c.getChannelID() != null && c.getChannelID().equals(valueSelectorText)) {
98 throw new IllegalArgumentException(String.format("Not valid value selector: %s", valueSelectorText));
101 public ApplianceChannelSelector getValueSelectorFromMieleID(String valueSelectorText)
102 throws IllegalArgumentException {
103 E[] enumConstants = selectorType.getEnumConstants();
104 if (enumConstants == null) {
105 throw new IllegalArgumentException(
106 String.format("Could not get enum constants for value selector: %s", valueSelectorText));
108 for (ApplianceChannelSelector c : enumConstants) {
109 if (c != null && c.getMieleID() != null && c.getMieleID().equals(valueSelectorText)) {
114 throw new IllegalArgumentException(String.format("Not valid value selector: %s", valueSelectorText));
118 public void initialize() {
119 logger.debug("Initializing Miele appliance handler.");
120 final String applianceId = (String) getThing().getConfiguration().getProperties().get(APPLIANCE_ID);
121 if (applianceId != null) {
122 this.applianceId = applianceId;
123 this.onBridgeConnectionResumed();
127 public void onBridgeConnectionResumed() {
128 Bridge bridge = getBridge();
129 if (bridge != null && getMieleBridgeHandler() != null) {
130 ThingStatusInfo statusInfo = bridge.getStatusInfo();
131 updateStatus(statusInfo.getStatus(), statusInfo.getStatusDetail(), statusInfo.getDescription());
136 public void dispose() {
137 logger.debug("Handler disposes. Unregistering listener.");
138 if (applianceId != null) {
139 MieleBridgeHandler bridgeHandler = getMieleBridgeHandler();
140 if (bridgeHandler != null) {
141 getMieleBridgeHandler().unregisterApplianceStatusListener(this);
148 public void handleCommand(ChannelUID channelUID, Command command) {
149 // Here we could handle commands that are common to all Miele Appliances, but so far I don't know of any
150 if (command instanceof RefreshType) {
151 // Placeholder for future refinement
157 public void onApplianceStateChanged(FullyQualifiedApplianceIdentifier applicationIdentifier,
158 DeviceClassObject dco) {
159 String myApplianceId = (String) getThing().getConfiguration().getProperties().get(APPLIANCE_ID);
160 if (myApplianceId == null || !myApplianceId.equals(applicationIdentifier.getApplianceId())) {
164 for (JsonElement prop : dco.Properties.getAsJsonArray()) {
166 DeviceProperty dp = gson.fromJson(prop, DeviceProperty.class);
170 if (!EXTENDED_DEVICE_STATE_PROPERTY_NAME.equals(dp.Name)) {
171 dp.Value = dp.Value.trim();
172 dp.Value = dp.Value.strip();
174 onAppliancePropertyChanged(applicationIdentifier, dp);
175 } catch (Exception p) {
176 // Ignore - this is due to an unrecognized and not yet reverse-engineered array property
182 public void onAppliancePropertyChanged(FullyQualifiedApplianceIdentifier applicationIdentifier, DeviceProperty dp) {
183 String myApplianceId = (String) getThing().getConfiguration().getProperties().get(APPLIANCE_ID);
185 if (myApplianceId == null || !myApplianceId.equals(applicationIdentifier.getApplianceId())) {
189 this.onAppliancePropertyChanged(dp);
192 protected void onAppliancePropertyChanged(DeviceProperty dp) {
194 DeviceMetaData dmd = null;
195 if (dp.Metadata == null) {
196 String metadata = metaDataCache.get(new StringBuilder().append(dp.Name).toString().trim());
197 if (metadata != null) {
198 JsonObject jsonMetaData = (JsonObject) JsonParser.parseString(metadata);
199 dmd = gson.fromJson(jsonMetaData, DeviceMetaData.class);
200 // only keep the enum, if any - that's all we care for events we receive via multicast
201 // all other fields are nulled
203 dmd.LocalizedID = null;
204 dmd.LocalizedValue = null;
206 dmd.description = null;
210 if (dp.Metadata != null) {
211 String metadata = dp.Metadata.toString().replace("enum", "MieleEnum");
212 JsonObject jsonMetaData = (JsonObject) JsonParser.parseString(metadata);
213 dmd = gson.fromJson(jsonMetaData, DeviceMetaData.class);
214 metaDataCache.put(new StringBuilder().append(dp.Name).toString().trim(), metadata);
217 if (dp.Name.equals(EXTENDED_DEVICE_STATE_PROPERTY_NAME)) {
218 if (!dp.Value.isEmpty()) {
219 byte[] extendedStateBytes = DeviceUtil.stringToBytes(dp.Value);
220 logger.trace("Extended device state for {}: {}", getThing().getUID(),
221 DeviceUtil.bytesToHex(extendedStateBytes));
222 if (this instanceof ExtendedDeviceStateListener) {
223 ((ExtendedDeviceStateListener) this).onApplianceExtendedStateChanged(extendedStateBytes);
229 ApplianceChannelSelector selector = null;
231 selector = getValueSelectorFromMieleID(dp.Name);
232 } catch (Exception h) {
233 logger.trace("{} is not a valid channel for a {}", dp.Name, modelID);
236 String dpValue = dp.Value.strip().trim();
238 if (selector != null) {
239 if (!selector.isProperty()) {
240 ChannelUID theChannelUID = new ChannelUID(getThing().getUID(), selector.getChannelID());
242 if (dp.Value != null) {
243 State state = selector.getState(dpValue, dmd);
244 logger.trace("Update state of {} with getState '{}'", theChannelUID, state);
245 updateState(theChannelUID, state);
246 updateRawChannel(dp.Name, dpValue);
248 updateState(theChannelUID, UnDefType.UNDEF);
251 logger.debug("Updating the property '{}' of '{}' to '{}'", selector.getChannelID(),
252 getThing().getUID(), selector.getState(dpValue, dmd).toString());
254 Map<@NonNull String, @NonNull String> properties = editProperties();
255 properties.put(selector.getChannelID(), selector.getState(dpValue, dmd).toString());
256 updateProperties(properties);
259 } catch (IllegalArgumentException e) {
260 logger.error("An exception occurred while processing a changed device property :'{}'", e.getMessage());
264 protected void updateExtendedState(String channelId, State state) {
265 ChannelUID channelUid = new ChannelUID(getThing().getUID(), channelId);
266 logger.trace("Update state of {} with extended state '{}'", channelUid, state);
267 updateState(channelUid, state);
271 * Update raw value channels for properties already mapped to text channels.
272 * Currently ApplianceChannelSelector only supports 1:1 mapping from property
275 private void updateRawChannel(String propertyName, String value) {
277 switch (propertyName) {
278 case STATE_PROPERTY_NAME:
279 channelId = STATE_CHANNEL_ID;
281 case PROGRAM_ID_PROPERTY_NAME:
282 channelId = PROGRAM_CHANNEL_ID;
287 ApplianceChannelSelector selector = null;
289 selector = getValueSelectorFromChannelID(channelId);
290 } catch (IllegalArgumentException e) {
291 logger.trace("{} is not a valid channel for a {}", channelId, modelID);
294 ChannelUID channelUid = new ChannelUID(getThing().getUID(), channelId);
295 State state = selector.getState(value);
296 logger.trace("Update state of {} with getState '{}'", channelUid, state);
297 updateState(channelUid, state);
301 public void onApplianceRemoved(HomeDevice appliance) {
302 if (applianceId == null) {
306 if (applianceId.equals(appliance.getApplianceIdentifier().getApplianceId())) {
307 updateStatus(ThingStatus.OFFLINE);
312 public void onApplianceAdded(HomeDevice appliance) {
313 if (applianceId == null) {
317 FullyQualifiedApplianceIdentifier applianceIdentifier = appliance.getApplianceIdentifier();
319 if (applianceId.equals(applianceIdentifier.getApplianceId())) {
321 Map<@NonNull String, @NonNull String> properties = editProperties();
322 properties.put(MODEL_PROPERTY_NAME, appliance.getApplianceModel());
323 String deviceClass = appliance.getDeviceClass();
324 if (deviceClass != null) {
325 properties.put(DEVICE_CLASS, deviceClass);
327 properties.put(PROTOCOL_ADAPTER_PROPERTY_NAME, appliance.ProtocolAdapterName);
328 properties.put(SERIAL_NUMBER_PROPERTY_NAME, appliance.getSerialNumber());
329 String connectionType = appliance.getConnectionType();
330 if (connectionType != null) {
331 properties.put(CONNECTION_TYPE_PROPERTY_NAME, connectionType);
333 updateProperties(properties);
334 updateStatus(ThingStatus.ONLINE);
338 private synchronized MieleBridgeHandler getMieleBridgeHandler() {
339 if (this.bridgeHandler == null) {
340 Bridge bridge = getBridge();
341 if (bridge == null) {
344 ThingHandler handler = bridge.getHandler();
345 if (handler instanceof MieleBridgeHandler) {
346 this.bridgeHandler = (MieleBridgeHandler) handler;
347 this.bridgeHandler.registerApplianceStatusListener(this);
352 return this.bridgeHandler;
355 protected boolean isResultProcessable(JsonElement result) {
356 if (result == null) {
357 throw new IllegalArgumentException("Provided result is null");
359 return !result.isJsonNull();