]> git.basschouten.com Git - openhab-addons.git/blob
929c7339a57cce9f39e5ddf7151d9285f797a254
[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.MieleTranslationProvider;
27 import org.openhab.binding.miele.internal.api.dto.DeviceClassObject;
28 import org.openhab.binding.miele.internal.api.dto.DeviceMetaData;
29 import org.openhab.binding.miele.internal.api.dto.DeviceProperty;
30 import org.openhab.binding.miele.internal.api.dto.HomeDevice;
31 import org.openhab.core.i18n.LocaleProvider;
32 import org.openhab.core.i18n.TranslationProvider;
33 import org.openhab.core.thing.Bridge;
34 import org.openhab.core.thing.ChannelUID;
35 import org.openhab.core.thing.Thing;
36 import org.openhab.core.thing.ThingStatus;
37 import org.openhab.core.thing.ThingStatusDetail;
38 import org.openhab.core.thing.ThingTypeUID;
39 import org.openhab.core.thing.ThingUID;
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     private @Nullable MieleBridgeHandler bridgeHandler;
78     protected TranslationProvider i18nProvider;
79     protected LocaleProvider localeProvider;
80     protected 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         this.translationProvider = new MieleTranslationProvider(i18nProvider, localeProvider);
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.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.getMieleID().isEmpty() && 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 || applianceId.isBlank()) {
133             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR,
134                     "@text/offline.configuration-error.uid-not-set");
135             return;
136         }
137         this.applianceId = applianceId;
138         Bridge bridge = getBridge();
139         if (bridge == null) {
140             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR,
141                     "@text/offline.configuration-error.bridge-missing");
142             return;
143         }
144         initializeTranslationProvider(bridge);
145         updateStatus(ThingStatus.UNKNOWN);
146
147         MieleBridgeHandler bridgeHandler = getMieleBridgeHandler();
148         if (bridgeHandler != null) {
149             bridgeHandler.registerApplianceStatusListener(applianceId, this);
150         }
151     }
152
153     private void initializeTranslationProvider(Bridge bridge) {
154         Locale locale = null;
155         String language = (String) bridge.getConfiguration().get(LANGUAGE);
156         if (language != null && !language.isBlank()) {
157             try {
158                 locale = new Locale.Builder().setLanguageTag(language).build();
159             } catch (IllformedLocaleException e) {
160                 logger.warn("Invalid language configured: {}", e.getMessage());
161             }
162         }
163         if (locale == null) {
164             logger.debug("No language configured, using system language.");
165             this.translationProvider = new MieleTranslationProvider(i18nProvider, localeProvider);
166         } else {
167             this.translationProvider = new MieleTranslationProvider(i18nProvider, localeProvider, locale);
168         }
169     }
170
171     @Override
172     public void dispose() {
173         logger.debug("Handler disposes. Unregistering listener.");
174         String applianceId = this.applianceId;
175         if (applianceId != null) {
176             MieleBridgeHandler bridgeHandler = getMieleBridgeHandler();
177             if (bridgeHandler != null) {
178                 bridgeHandler.unregisterApplianceStatusListener(applianceId, this);
179             }
180             applianceId = null;
181         }
182     }
183
184     @Override
185     public void handleCommand(ChannelUID channelUID, Command command) {
186         // Here we could handle commands that are common to all Miele Appliances, but so far I don't know of any
187         if (command instanceof RefreshType) {
188             // Placeholder for future refinement
189             return;
190         }
191     }
192
193     @Override
194     public void onApplianceStateChanged(DeviceClassObject dco) {
195         JsonArray properties = dco.Properties;
196         if (properties == null) {
197             return;
198         }
199
200         for (JsonElement prop : properties.getAsJsonArray()) {
201             try {
202                 DeviceProperty dp = gson.fromJson(prop, DeviceProperty.class);
203                 if (dp == null) {
204                     continue;
205                 }
206                 if (!EXTENDED_DEVICE_STATE_PROPERTY_NAME.equals(dp.Name)) {
207                     dp.Value = dp.Value.trim();
208                     dp.Value = dp.Value.strip();
209                 }
210                 onAppliancePropertyChanged(dp);
211             } catch (Exception p) {
212                 // Ignore - this is due to an unrecognized and not yet reverse-engineered array property
213             }
214         }
215     }
216
217     @Override
218     public void onAppliancePropertyChanged(DeviceProperty dp) {
219         try {
220             DeviceMetaData dmd = null;
221             if (dp.Metadata == null) {
222                 String metadata = metaDataCache.get(new StringBuilder().append(dp.Name).toString().trim());
223                 if (metadata != null) {
224                     JsonObject jsonMetadata = (JsonObject) JsonParser.parseString(metadata);
225                     dmd = gson.fromJson(jsonMetadata, DeviceMetaData.class);
226                     // only keep the enum, if any - that's all we care for events we receive via multicast
227                     // all other fields are nulled
228                     if (dmd != null) {
229                         dmd.LocalizedID = null;
230                         dmd.LocalizedValue = null;
231                         dmd.Filter = null;
232                         dmd.description = null;
233                     }
234                 }
235             }
236             JsonObject jsonMetadata = dp.Metadata;
237             if (jsonMetadata != null) {
238                 String metadata = jsonMetadata.toString().replace("enum", "MieleEnum");
239                 JsonObject jsonMetaData = (JsonObject) JsonParser.parseString(metadata);
240                 dmd = gson.fromJson(jsonMetaData, DeviceMetaData.class);
241                 metaDataCache.put(new StringBuilder().append(dp.Name).toString().trim(), metadata);
242             }
243
244             if (EXTENDED_DEVICE_STATE_PROPERTY_NAME.equals(dp.Name)) {
245                 if (!dp.Value.isEmpty()) {
246                     byte[] extendedStateBytes = DeviceUtil.stringToBytes(dp.Value);
247                     logger.trace("Extended device state for {}: {}", getThing().getUID(),
248                             DeviceUtil.bytesToHex(extendedStateBytes));
249                     if (this instanceof ExtendedDeviceStateListener) {
250                         ((ExtendedDeviceStateListener) this).onApplianceExtendedStateChanged(extendedStateBytes);
251                     }
252                 }
253                 return;
254             }
255
256             ApplianceChannelSelector selector = null;
257             try {
258                 selector = getValueSelectorFromMieleID(dp.Name);
259             } catch (Exception h) {
260                 logger.trace("{} is not a valid channel for a {}", dp.Name, modelID);
261             }
262
263             String dpValue = dp.Value.strip().trim();
264
265             if (selector != null) {
266                 String channelId = selector.getChannelID();
267                 ThingUID thingUid = getThing().getUID();
268                 State state = selector.getState(dpValue, dmd, this.translationProvider);
269                 if (selector.isProperty()) {
270                     String value = state.toString();
271                     logger.trace("Updating the property '{}' of '{}' to '{}'", channelId, thingUid, value);
272                     updateProperty(channelId, value);
273                 } else {
274                     ChannelUID theChannelUID = new ChannelUID(thingUid, channelId);
275                     logger.trace("Update state of {} with getState '{}'", theChannelUID, state);
276                     updateState(theChannelUID, state);
277                     updateRawChannel(dp.Name, dpValue);
278                 }
279             }
280         } catch (IllegalArgumentException e) {
281             logger.warn("An exception occurred while processing a changed device property: '{}'", e.getMessage());
282         }
283     }
284
285     protected void updateExtendedState(String channelId, State state) {
286         ChannelUID channelUid = new ChannelUID(getThing().getUID(), channelId);
287         logger.trace("Update state of {} with extended state '{}'", channelUid, state);
288         updateState(channelUid, state);
289     }
290
291     /**
292      * Update raw value channels for properties already mapped to text channels.
293      * Currently ApplianceChannelSelector only supports 1:1 mapping from property
294      * to channel.
295      */
296     private void updateRawChannel(String propertyName, String value) {
297         String channelId;
298         switch (propertyName) {
299             case STATE_PROPERTY_NAME:
300                 channelId = STATE_CHANNEL_ID;
301                 break;
302             case PROGRAM_ID_PROPERTY_NAME:
303                 channelId = PROGRAM_CHANNEL_ID;
304                 break;
305             default:
306                 return;
307         }
308         ApplianceChannelSelector selector = null;
309         try {
310             selector = getValueSelectorFromChannelID(channelId);
311         } catch (IllegalArgumentException e) {
312             logger.trace("{} is not a valid channel for a {}", channelId, modelID);
313             return;
314         }
315         ChannelUID channelUid = new ChannelUID(getThing().getUID(), channelId);
316         State state = selector.getState(value);
317         logger.trace("Update state of {} with getState '{}'", channelUid, state);
318         updateState(channelUid, state);
319     }
320
321     @Override
322     public void onApplianceRemoved() {
323         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.GONE);
324     }
325
326     @Override
327     public void onApplianceAdded(HomeDevice appliance) {
328         Map<String, String> properties = editProperties();
329         String vendor = appliance.Vendor;
330         if (vendor != null) {
331             properties.put(Thing.PROPERTY_VENDOR, vendor);
332         }
333         properties.put(Thing.PROPERTY_MODEL_ID, appliance.getApplianceModel());
334         properties.put(Thing.PROPERTY_SERIAL_NUMBER, appliance.getSerialNumber());
335         properties.put(Thing.PROPERTY_FIRMWARE_VERSION, appliance.getFirmwareVersion());
336         String protocolAdapterName = appliance.ProtocolAdapterName;
337         if (protocolAdapterName != null) {
338             properties.put(PROPERTY_PROTOCOL_ADAPTER, protocolAdapterName);
339         }
340         String deviceClass = appliance.getDeviceClass();
341         if (deviceClass != null) {
342             properties.put(PROPERTY_DEVICE_CLASS, deviceClass);
343         }
344         String connectionType = appliance.getConnectionType();
345         if (connectionType != null) {
346             properties.put(PROPERTY_CONNECTION_TYPE, connectionType);
347         }
348         String connectionBaudRate = appliance.getConnectionBaudRate();
349         if (connectionBaudRate != null) {
350             properties.put(PROPERTY_CONNECTION_BAUD_RATE, connectionBaudRate);
351         }
352         updateProperties(properties);
353         updateStatus(ThingStatus.ONLINE);
354     }
355
356     protected synchronized @Nullable MieleBridgeHandler getMieleBridgeHandler() {
357         if (this.bridgeHandler == null) {
358             Bridge bridge = getBridge();
359             if (bridge == null) {
360                 return null;
361             }
362             ThingHandler handler = bridge.getHandler();
363             if (handler instanceof MieleBridgeHandler) {
364                 this.bridgeHandler = (MieleBridgeHandler) handler;
365             }
366         }
367         return this.bridgeHandler;
368     }
369
370     protected boolean isResultProcessable(JsonElement result) {
371         return !result.isJsonNull();
372     }
373 }