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