]> git.basschouten.com Git - openhab-addons.git/blob
c0b4d4f9d866fd67d5312bad515f9ba5ae3f4193
[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.IllformedLocaleException;
19 import java.util.Locale;
20 import java.util.Map;
21 import java.util.Set;
22 import java.util.stream.Collectors;
23 import java.util.stream.Stream;
24
25 import org.eclipse.jdt.annotation.NonNull;
26 import org.openhab.binding.miele.internal.DeviceMetaData;
27 import org.openhab.binding.miele.internal.DeviceUtil;
28 import org.openhab.binding.miele.internal.FullyQualifiedApplianceIdentifier;
29 import org.openhab.binding.miele.internal.MieleTranslationProvider;
30 import org.openhab.binding.miele.internal.handler.MieleBridgeHandler.DeviceClassObject;
31 import org.openhab.binding.miele.internal.handler.MieleBridgeHandler.DeviceProperty;
32 import org.openhab.binding.miele.internal.handler.MieleBridgeHandler.HomeDevice;
33 import org.openhab.core.i18n.LocaleProvider;
34 import org.openhab.core.i18n.TranslationProvider;
35 import org.openhab.core.thing.Bridge;
36 import org.openhab.core.thing.ChannelUID;
37 import org.openhab.core.thing.Thing;
38 import org.openhab.core.thing.ThingStatus;
39 import org.openhab.core.thing.ThingStatusInfo;
40 import org.openhab.core.thing.ThingTypeUID;
41 import org.openhab.core.thing.binding.BaseThingHandler;
42 import org.openhab.core.thing.binding.ThingHandler;
43 import org.openhab.core.types.Command;
44 import org.openhab.core.types.RefreshType;
45 import org.openhab.core.types.State;
46 import org.openhab.core.types.UnDefType;
47 import org.slf4j.Logger;
48 import org.slf4j.LoggerFactory;
49
50 import com.google.gson.Gson;
51 import com.google.gson.JsonElement;
52 import com.google.gson.JsonObject;
53 import com.google.gson.JsonParser;
54
55 /**
56  * The {@link MieleApplianceHandler} is an abstract class
57  * responsible for handling commands, which are sent to one
58  * of the channels of the appliance that understands/"talks"
59  * the {@link ApplianceChannelSelector} datapoints
60  *
61  * @author Karel Goderis - Initial contribution
62  * @author Martin Lepsy - Added check for JsonNull result
63  * @author Jacob Laursen - Fixed multicast and protocol support (ZigBee/LAN)
64  */
65 public abstract class MieleApplianceHandler<E extends Enum<E> & ApplianceChannelSelector> extends BaseThingHandler
66         implements ApplianceStatusListener {
67
68     private final Logger logger = LoggerFactory.getLogger(MieleApplianceHandler.class);
69
70     public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Stream
71             .of(THING_TYPE_DISHWASHER, THING_TYPE_OVEN, THING_TYPE_FRIDGE, THING_TYPE_DRYER, THING_TYPE_HOB,
72                     THING_TYPE_FRIDGEFREEZER, THING_TYPE_HOOD, THING_TYPE_WASHINGMACHINE, THING_TYPE_COFFEEMACHINE)
73             .collect(Collectors.toSet());
74
75     protected Gson gson = new Gson();
76
77     protected String applianceId;
78     protected MieleBridgeHandler bridgeHandler;
79     protected TranslationProvider i18nProvider;
80     protected LocaleProvider localeProvider;
81     protected MieleTranslationProvider translationProvider;
82     private Class<E> selectorType;
83     protected String modelID;
84
85     protected Map<String, String> metaDataCache = new HashMap<>();
86
87     public MieleApplianceHandler(Thing thing, TranslationProvider i18nProvider, LocaleProvider localeProvider,
88             Class<E> selectorType, String modelID) {
89         super(thing);
90         this.i18nProvider = i18nProvider;
91         this.localeProvider = localeProvider;
92         this.selectorType = selectorType;
93         this.modelID = modelID;
94     }
95
96     public ApplianceChannelSelector getValueSelectorFromChannelID(String valueSelectorText)
97             throws IllegalArgumentException {
98         E[] enumConstants = selectorType.getEnumConstants();
99         if (enumConstants == null) {
100             throw new IllegalArgumentException(
101                     String.format("Could not get enum constants for value selector: %s", valueSelectorText));
102         }
103         for (ApplianceChannelSelector c : enumConstants) {
104             if (c != null && c.getChannelID() != null && c.getChannelID().equals(valueSelectorText)) {
105                 return c;
106             }
107         }
108
109         throw new IllegalArgumentException(String.format("Not valid value selector: %s", valueSelectorText));
110     }
111
112     public ApplianceChannelSelector getValueSelectorFromMieleID(String valueSelectorText)
113             throws IllegalArgumentException {
114         E[] enumConstants = selectorType.getEnumConstants();
115         if (enumConstants == null) {
116             throw new IllegalArgumentException(
117                     String.format("Could not get enum constants for value selector: %s", valueSelectorText));
118         }
119         for (ApplianceChannelSelector c : enumConstants) {
120             if (c != null && c.getMieleID() != null && c.getMieleID().equals(valueSelectorText)) {
121                 return c;
122             }
123         }
124
125         throw new IllegalArgumentException(String.format("Not valid value selector: %s", valueSelectorText));
126     }
127
128     @Override
129     public void initialize() {
130         logger.debug("Initializing Miele appliance handler.");
131         final String applianceId = (String) getThing().getConfiguration().getProperties().get(APPLIANCE_ID);
132         if (applianceId != null) {
133             this.applianceId = applianceId;
134             this.onBridgeConnectionResumed();
135         }
136     }
137
138     public void onBridgeConnectionResumed() {
139         Bridge bridge = getBridge();
140         if (bridge != null && getMieleBridgeHandler() != null) {
141             ThingStatusInfo statusInfo = bridge.getStatusInfo();
142             updateStatus(statusInfo.getStatus(), statusInfo.getStatusDetail(), statusInfo.getDescription());
143             initializeTranslationProvider(bridge);
144         }
145     }
146
147     private void initializeTranslationProvider(Bridge bridge) {
148         Locale locale = null;
149         String language = (String) bridge.getConfiguration().get(LANGUAGE);
150         if (language != null && !language.isBlank()) {
151             try {
152                 locale = new Locale.Builder().setLanguageTag(language).build();
153             } catch (IllformedLocaleException e) {
154                 logger.error("Invalid language configured: {}", e.getMessage());
155             }
156         }
157         if (locale == null) {
158             logger.debug("No language configured, using system language.");
159             this.translationProvider = new MieleTranslationProvider(i18nProvider, localeProvider);
160         } else {
161             this.translationProvider = new MieleTranslationProvider(i18nProvider, localeProvider, locale);
162         }
163     }
164
165     @Override
166     public void dispose() {
167         logger.debug("Handler disposes. Unregistering listener.");
168         if (applianceId != null) {
169             MieleBridgeHandler bridgeHandler = getMieleBridgeHandler();
170             if (bridgeHandler != null) {
171                 getMieleBridgeHandler().unregisterApplianceStatusListener(this);
172             }
173             applianceId = null;
174         }
175     }
176
177     @Override
178     public void handleCommand(ChannelUID channelUID, Command command) {
179         // Here we could handle commands that are common to all Miele Appliances, but so far I don't know of any
180         if (command instanceof RefreshType) {
181             // Placeholder for future refinement
182             return;
183         }
184     }
185
186     @Override
187     public void onApplianceStateChanged(FullyQualifiedApplianceIdentifier applicationIdentifier,
188             DeviceClassObject dco) {
189         String myApplianceId = (String) getThing().getConfiguration().getProperties().get(APPLIANCE_ID);
190         if (myApplianceId == null || !myApplianceId.equals(applicationIdentifier.getApplianceId())) {
191             return;
192         }
193
194         for (JsonElement prop : dco.Properties.getAsJsonArray()) {
195             try {
196                 DeviceProperty dp = gson.fromJson(prop, DeviceProperty.class);
197                 if (dp == null) {
198                     continue;
199                 }
200                 if (!EXTENDED_DEVICE_STATE_PROPERTY_NAME.equals(dp.Name)) {
201                     dp.Value = dp.Value.trim();
202                     dp.Value = dp.Value.strip();
203                 }
204                 onAppliancePropertyChanged(applicationIdentifier, dp);
205             } catch (Exception p) {
206                 // Ignore - this is due to an unrecognized and not yet reverse-engineered array property
207             }
208         }
209     }
210
211     @Override
212     public void onAppliancePropertyChanged(FullyQualifiedApplianceIdentifier applicationIdentifier, DeviceProperty dp) {
213         String myApplianceId = (String) getThing().getConfiguration().getProperties().get(APPLIANCE_ID);
214
215         if (myApplianceId == null || !myApplianceId.equals(applicationIdentifier.getApplianceId())) {
216             return;
217         }
218
219         this.onAppliancePropertyChanged(dp);
220     }
221
222     protected void onAppliancePropertyChanged(DeviceProperty dp) {
223         try {
224             DeviceMetaData dmd = null;
225             if (dp.Metadata == null) {
226                 String metadata = metaDataCache.get(new StringBuilder().append(dp.Name).toString().trim());
227                 if (metadata != null) {
228                     JsonObject jsonMetaData = (JsonObject) JsonParser.parseString(metadata);
229                     dmd = gson.fromJson(jsonMetaData, DeviceMetaData.class);
230                     // only keep the enum, if any - that's all we care for events we receive via multicast
231                     // all other fields are nulled
232                     if (dmd != null) {
233                         dmd.LocalizedID = null;
234                         dmd.LocalizedValue = null;
235                         dmd.Filter = null;
236                         dmd.description = null;
237                     }
238                 }
239             }
240             if (dp.Metadata != null) {
241                 String metadata = dp.Metadata.toString().replace("enum", "MieleEnum");
242                 JsonObject jsonMetaData = (JsonObject) JsonParser.parseString(metadata);
243                 dmd = gson.fromJson(jsonMetaData, DeviceMetaData.class);
244                 metaDataCache.put(new StringBuilder().append(dp.Name).toString().trim(), metadata);
245             }
246
247             if (dp.Name.equals(EXTENDED_DEVICE_STATE_PROPERTY_NAME)) {
248                 if (!dp.Value.isEmpty()) {
249                     byte[] extendedStateBytes = DeviceUtil.stringToBytes(dp.Value);
250                     logger.trace("Extended device state for {}: {}", getThing().getUID(),
251                             DeviceUtil.bytesToHex(extendedStateBytes));
252                     if (this instanceof ExtendedDeviceStateListener) {
253                         ((ExtendedDeviceStateListener) this).onApplianceExtendedStateChanged(extendedStateBytes);
254                     }
255                 }
256                 return;
257             }
258
259             ApplianceChannelSelector selector = null;
260             try {
261                 selector = getValueSelectorFromMieleID(dp.Name);
262             } catch (Exception h) {
263                 logger.trace("{} is not a valid channel for a {}", dp.Name, modelID);
264             }
265
266             String dpValue = dp.Value.strip().trim();
267
268             if (selector != null) {
269                 if (!selector.isProperty()) {
270                     ChannelUID theChannelUID = new ChannelUID(getThing().getUID(), selector.getChannelID());
271
272                     if (dp.Value != null) {
273                         State state = selector.getState(dpValue, dmd, this.translationProvider);
274                         logger.trace("Update state of {} with getState '{}'", theChannelUID, state);
275                         updateState(theChannelUID, state);
276                         updateRawChannel(dp.Name, dpValue);
277                     } else {
278                         updateState(theChannelUID, UnDefType.UNDEF);
279                     }
280                 } else {
281                     logger.debug("Updating the property '{}' of '{}' to '{}'", selector.getChannelID(),
282                             getThing().getUID(), selector.getState(dpValue, dmd, this.translationProvider).toString());
283                     @NonNull
284                     Map<@NonNull String, @NonNull String> properties = editProperties();
285                     properties.put(selector.getChannelID(),
286                             selector.getState(dpValue, dmd, this.translationProvider).toString());
287                     updateProperties(properties);
288                 }
289             }
290         } catch (IllegalArgumentException e) {
291             logger.error("An exception occurred while processing a changed device property :'{}'", e.getMessage());
292         }
293     }
294
295     protected void updateExtendedState(String channelId, State state) {
296         ChannelUID channelUid = new ChannelUID(getThing().getUID(), channelId);
297         logger.trace("Update state of {} with extended state '{}'", channelUid, state);
298         updateState(channelUid, state);
299     }
300
301     /**
302      * Update raw value channels for properties already mapped to text channels.
303      * Currently ApplianceChannelSelector only supports 1:1 mapping from property
304      * to channel.
305      */
306     private void updateRawChannel(String propertyName, String value) {
307         String channelId;
308         switch (propertyName) {
309             case STATE_PROPERTY_NAME:
310                 channelId = STATE_CHANNEL_ID;
311                 break;
312             case PROGRAM_ID_PROPERTY_NAME:
313                 channelId = PROGRAM_CHANNEL_ID;
314                 break;
315             default:
316                 return;
317         }
318         ApplianceChannelSelector selector = null;
319         try {
320             selector = getValueSelectorFromChannelID(channelId);
321         } catch (IllegalArgumentException e) {
322             logger.trace("{} is not a valid channel for a {}", channelId, modelID);
323             return;
324         }
325         ChannelUID channelUid = new ChannelUID(getThing().getUID(), channelId);
326         State state = selector.getState(value);
327         logger.trace("Update state of {} with getState '{}'", channelUid, state);
328         updateState(channelUid, state);
329     }
330
331     @Override
332     public void onApplianceRemoved(HomeDevice appliance) {
333         if (applianceId == null) {
334             return;
335         }
336
337         if (applianceId.equals(appliance.getApplianceIdentifier().getApplianceId())) {
338             updateStatus(ThingStatus.OFFLINE);
339         }
340     }
341
342     @Override
343     public void onApplianceAdded(HomeDevice appliance) {
344         if (applianceId == null) {
345             return;
346         }
347
348         FullyQualifiedApplianceIdentifier applianceIdentifier = appliance.getApplianceIdentifier();
349
350         if (applianceId.equals(applianceIdentifier.getApplianceId())) {
351             @NonNull
352             Map<@NonNull String, @NonNull String> properties = editProperties();
353             properties.put(MODEL_PROPERTY_NAME, appliance.getApplianceModel());
354             String deviceClass = appliance.getDeviceClass();
355             if (deviceClass != null) {
356                 properties.put(DEVICE_CLASS, deviceClass);
357             }
358             properties.put(PROTOCOL_ADAPTER_PROPERTY_NAME, appliance.ProtocolAdapterName);
359             properties.put(SERIAL_NUMBER_PROPERTY_NAME, appliance.getSerialNumber());
360             String connectionType = appliance.getConnectionType();
361             if (connectionType != null) {
362                 properties.put(CONNECTION_TYPE_PROPERTY_NAME, connectionType);
363             }
364             updateProperties(properties);
365             updateStatus(ThingStatus.ONLINE);
366         }
367     }
368
369     private synchronized MieleBridgeHandler getMieleBridgeHandler() {
370         if (this.bridgeHandler == null) {
371             Bridge bridge = getBridge();
372             if (bridge == null) {
373                 return null;
374             }
375             ThingHandler handler = bridge.getHandler();
376             if (handler instanceof MieleBridgeHandler) {
377                 this.bridgeHandler = (MieleBridgeHandler) handler;
378                 this.bridgeHandler.registerApplianceStatusListener(this);
379             } else {
380                 return null;
381             }
382         }
383         return this.bridgeHandler;
384     }
385
386     protected boolean isResultProcessable(JsonElement result) {
387         if (result == null) {
388             throw new IllegalArgumentException("Provided result is null");
389         }
390         return !result.isJsonNull();
391     }
392 }