]> git.basschouten.com Git - openhab-addons.git/blob
9c84e1a912a3e20a9046a7533a088ff5dc165fbe
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2021 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.miele.internal.handler;
14
15 import static org.openhab.binding.miele.internal.MieleBindingConstants.*;
16
17 import java.util.HashMap;
18 import java.util.Map;
19 import java.util.Set;
20 import java.util.stream.Collectors;
21 import java.util.stream.Stream;
22
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;
44
45 import com.google.gson.Gson;
46 import com.google.gson.JsonElement;
47 import com.google.gson.JsonObject;
48 import com.google.gson.JsonParser;
49
50 /**
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
55  *
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)
59  */
60 public abstract class MieleApplianceHandler<E extends Enum<E> & ApplianceChannelSelector> extends BaseThingHandler
61         implements ApplianceStatusListener {
62
63     private final Logger logger = LoggerFactory.getLogger(MieleApplianceHandler.class);
64
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());
69
70     protected Gson gson = new Gson();
71
72     protected String applianceId;
73     protected MieleBridgeHandler bridgeHandler;
74     private Class<E> selectorType;
75     protected String modelID;
76
77     protected Map<String, String> metaDataCache = new HashMap<>();
78
79     public MieleApplianceHandler(Thing thing, Class<E> selectorType, String modelID) {
80         super(thing);
81         this.selectorType = selectorType;
82         this.modelID = modelID;
83     }
84
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));
91         }
92         for (ApplianceChannelSelector c : enumConstants) {
93             if (c != null && c.getChannelID() != null && c.getChannelID().equals(valueSelectorText)) {
94                 return c;
95             }
96         }
97
98         throw new IllegalArgumentException(String.format("Not valid value selector: %s", valueSelectorText));
99     }
100
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));
107         }
108         for (ApplianceChannelSelector c : enumConstants) {
109             if (c != null && c.getMieleID() != null && c.getMieleID().equals(valueSelectorText)) {
110                 return c;
111             }
112         }
113
114         throw new IllegalArgumentException(String.format("Not valid value selector: %s", valueSelectorText));
115     }
116
117     @Override
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();
124         }
125     }
126
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());
132         }
133     }
134
135     @Override
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);
142             }
143             applianceId = null;
144         }
145     }
146
147     @Override
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
152             return;
153         }
154     }
155
156     @Override
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())) {
161             return;
162         }
163
164         for (JsonElement prop : dco.Properties.getAsJsonArray()) {
165             try {
166                 DeviceProperty dp = gson.fromJson(prop, DeviceProperty.class);
167                 if (dp == null) {
168                     continue;
169                 }
170                 if (!EXTENDED_DEVICE_STATE_PROPERTY_NAME.equals(dp.Name)) {
171                     dp.Value = dp.Value.trim();
172                     dp.Value = dp.Value.strip();
173                 }
174                 onAppliancePropertyChanged(applicationIdentifier, dp);
175             } catch (Exception p) {
176                 // Ignore - this is due to an unrecognized and not yet reverse-engineered array property
177             }
178         }
179     }
180
181     @Override
182     public void onAppliancePropertyChanged(FullyQualifiedApplianceIdentifier applicationIdentifier, DeviceProperty dp) {
183         String myApplianceId = (String) getThing().getConfiguration().getProperties().get(APPLIANCE_ID);
184
185         if (myApplianceId == null || !myApplianceId.equals(applicationIdentifier.getApplianceId())) {
186             return;
187         }
188
189         this.onAppliancePropertyChanged(dp);
190     }
191
192     protected void onAppliancePropertyChanged(DeviceProperty dp) {
193         try {
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
202                     if (dmd != null) {
203                         dmd.LocalizedID = null;
204                         dmd.LocalizedValue = null;
205                         dmd.Filter = null;
206                         dmd.description = null;
207                     }
208                 }
209             }
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);
215             }
216
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);
224                     }
225                 }
226                 return;
227             }
228
229             ApplianceChannelSelector selector = null;
230             try {
231                 selector = getValueSelectorFromMieleID(dp.Name);
232             } catch (Exception h) {
233                 logger.trace("{} is not a valid channel for a {}", dp.Name, modelID);
234             }
235
236             String dpValue = dp.Value.strip().trim();
237
238             if (selector != null) {
239                 if (!selector.isProperty()) {
240                     ChannelUID theChannelUID = new ChannelUID(getThing().getUID(), selector.getChannelID());
241
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);
247                     } else {
248                         updateState(theChannelUID, UnDefType.UNDEF);
249                     }
250                 } else {
251                     logger.debug("Updating the property '{}' of '{}' to '{}'", selector.getChannelID(),
252                             getThing().getUID(), selector.getState(dpValue, dmd).toString());
253                     @NonNull
254                     Map<@NonNull String, @NonNull String> properties = editProperties();
255                     properties.put(selector.getChannelID(), selector.getState(dpValue, dmd).toString());
256                     updateProperties(properties);
257                 }
258             }
259         } catch (IllegalArgumentException e) {
260             logger.error("An exception occurred while processing a changed device property :'{}'", e.getMessage());
261         }
262     }
263
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);
268     }
269
270     /**
271      * Update raw value channels for properties already mapped to text channels.
272      * Currently ApplianceChannelSelector only supports 1:1 mapping from property
273      * to channel.
274      */
275     private void updateRawChannel(String propertyName, String value) {
276         String channelId;
277         switch (propertyName) {
278             case STATE_PROPERTY_NAME:
279                 channelId = STATE_CHANNEL_ID;
280                 break;
281             case PROGRAM_ID_PROPERTY_NAME:
282                 channelId = PROGRAM_CHANNEL_ID;
283                 break;
284             default:
285                 return;
286         }
287         ApplianceChannelSelector selector = null;
288         try {
289             selector = getValueSelectorFromChannelID(channelId);
290         } catch (IllegalArgumentException e) {
291             logger.trace("{} is not a valid channel for a {}", channelId, modelID);
292             return;
293         }
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);
298     }
299
300     @Override
301     public void onApplianceRemoved(HomeDevice appliance) {
302         if (applianceId == null) {
303             return;
304         }
305
306         if (applianceId.equals(appliance.getApplianceIdentifier().getApplianceId())) {
307             updateStatus(ThingStatus.OFFLINE);
308         }
309     }
310
311     @Override
312     public void onApplianceAdded(HomeDevice appliance) {
313         if (applianceId == null) {
314             return;
315         }
316
317         FullyQualifiedApplianceIdentifier applianceIdentifier = appliance.getApplianceIdentifier();
318
319         if (applianceId.equals(applianceIdentifier.getApplianceId())) {
320             @NonNull
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);
326             }
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);
332             }
333             updateProperties(properties);
334             updateStatus(ThingStatus.ONLINE);
335         }
336     }
337
338     private synchronized MieleBridgeHandler getMieleBridgeHandler() {
339         if (this.bridgeHandler == null) {
340             Bridge bridge = getBridge();
341             if (bridge == null) {
342                 return null;
343             }
344             ThingHandler handler = bridge.getHandler();
345             if (handler instanceof MieleBridgeHandler) {
346                 this.bridgeHandler = (MieleBridgeHandler) handler;
347                 this.bridgeHandler.registerApplianceStatusListener(this);
348             } else {
349                 return null;
350             }
351         }
352         return this.bridgeHandler;
353     }
354
355     protected boolean isResultProcessable(JsonElement result) {
356         if (result == null) {
357             throw new IllegalArgumentException("Provided result is null");
358         }
359         return !result.isJsonNull();
360     }
361 }