]> git.basschouten.com Git - openhab-addons.git/blob
99edb672d519fe4d040105e11982ae9784c3ddbc
[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.library.types.OnOffType;
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.ThingStatusDetail;
39 import org.openhab.core.thing.ThingTypeUID;
40 import org.openhab.core.thing.ThingUID;
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.slf4j.Logger;
47 import org.slf4j.LoggerFactory;
48
49 import com.google.gson.Gson;
50 import com.google.gson.JsonArray;
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 @NonNullByDefault
66 public abstract class MieleApplianceHandler<E extends Enum<E> & ApplianceChannelSelector> extends BaseThingHandler
67         implements ApplianceStatusListener {
68
69     private final Logger logger = LoggerFactory.getLogger(MieleApplianceHandler.class);
70
71     public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Set.of(THING_TYPE_DISHWASHER, THING_TYPE_OVEN,
72             THING_TYPE_FRIDGE, THING_TYPE_DRYER, THING_TYPE_HOB, THING_TYPE_FRIDGEFREEZER, THING_TYPE_HOOD,
73             THING_TYPE_WASHINGMACHINE, THING_TYPE_COFFEEMACHINE);
74
75     protected Gson gson = new Gson();
76
77     protected @Nullable String applianceId;
78     private @Nullable 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         this.translationProvider = new MieleTranslationProvider(i18nProvider, localeProvider);
95     }
96
97     public ApplianceChannelSelector getValueSelectorFromChannelID(String valueSelectorText)
98             throws IllegalArgumentException {
99         E[] enumConstants = selectorType.getEnumConstants();
100         if (enumConstants == null) {
101             throw new IllegalArgumentException(
102                     String.format("Could not get enum constants for value selector: %s", valueSelectorText));
103         }
104         for (ApplianceChannelSelector c : enumConstants) {
105             if (c.getChannelID().equals(valueSelectorText)) {
106                 return c;
107             }
108         }
109
110         throw new IllegalArgumentException(String.format("Not valid value selector: %s", valueSelectorText));
111     }
112
113     public ApplianceChannelSelector getValueSelectorFromMieleID(String valueSelectorText)
114             throws IllegalArgumentException {
115         E[] enumConstants = selectorType.getEnumConstants();
116         if (enumConstants == null) {
117             throw new IllegalArgumentException(
118                     String.format("Could not get enum constants for value selector: %s", valueSelectorText));
119         }
120         for (ApplianceChannelSelector c : enumConstants) {
121             if (!c.getMieleID().isEmpty() && c.getMieleID().equals(valueSelectorText)) {
122                 return c;
123             }
124         }
125
126         throw new IllegalArgumentException(String.format("Not valid value selector: %s", valueSelectorText));
127     }
128
129     @Override
130     public void initialize() {
131         logger.debug("Initializing handler for thing {}", getThing().getUID());
132         final String applianceId = (String) getThing().getConfiguration().getProperties().get(APPLIANCE_ID);
133         if (applianceId == null || applianceId.isBlank()) {
134             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR,
135                     "@text/offline.configuration-error.uid-not-set");
136             return;
137         }
138         this.applianceId = applianceId;
139         Bridge bridge = getBridge();
140         if (bridge == null) {
141             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR,
142                     "@text/offline.configuration-error.bridge-missing");
143             return;
144         }
145         initializeTranslationProvider(bridge);
146         updateStatus(ThingStatus.UNKNOWN);
147
148         MieleBridgeHandler bridgeHandler = getMieleBridgeHandler();
149         if (bridgeHandler != null) {
150             bridgeHandler.registerApplianceStatusListener(applianceId, this);
151         }
152     }
153
154     private void initializeTranslationProvider(Bridge bridge) {
155         Locale locale = null;
156         String language = (String) bridge.getConfiguration().get(LANGUAGE);
157         if (language != null && !language.isBlank()) {
158             try {
159                 locale = new Locale.Builder().setLanguageTag(language).build();
160             } catch (IllformedLocaleException e) {
161                 logger.warn("Invalid language configured: {}", e.getMessage());
162             }
163         }
164         if (locale == null) {
165             logger.debug("No language configured, using system language.");
166             this.translationProvider = new MieleTranslationProvider(i18nProvider, localeProvider);
167         } else {
168             this.translationProvider = new MieleTranslationProvider(i18nProvider, localeProvider, locale);
169         }
170     }
171
172     @Override
173     public void dispose() {
174         logger.debug("Handler disposes. Unregistering listener.");
175         String applianceId = this.applianceId;
176         if (applianceId != null) {
177             MieleBridgeHandler bridgeHandler = getMieleBridgeHandler();
178             if (bridgeHandler != null) {
179                 bridgeHandler.unregisterApplianceStatusListener(applianceId, this);
180             }
181             applianceId = null;
182         }
183     }
184
185     @Override
186     public void handleCommand(ChannelUID channelUID, Command command) {
187         // Here we could handle commands that are common to all Miele Appliances, but so far I don't know of any
188         if (command instanceof RefreshType) {
189             // Placeholder for future refinement
190             return;
191         }
192     }
193
194     @Override
195     public void onApplianceStateChanged(DeviceClassObject dco) {
196         JsonArray properties = dco.Properties;
197         if (properties == null) {
198             return;
199         }
200
201         for (JsonElement prop : properties.getAsJsonArray()) {
202             try {
203                 DeviceProperty dp = gson.fromJson(prop, DeviceProperty.class);
204                 if (dp == null) {
205                     continue;
206                 }
207                 if (!EXTENDED_DEVICE_STATE_PROPERTY_NAME.equals(dp.Name)) {
208                     dp.Value = dp.Value.trim();
209                     dp.Value = dp.Value.strip();
210                 }
211                 onAppliancePropertyChanged(dp);
212             } catch (Exception p) {
213                 // Ignore - this is due to an unrecognized and not yet reverse-engineered array property
214             }
215         }
216     }
217
218     @Override
219     public void onAppliancePropertyChanged(DeviceProperty dp) {
220         try {
221             DeviceMetaData dmd = null;
222             if (dp.Metadata == null) {
223                 String metadata = metaDataCache.get(new StringBuilder().append(dp.Name).toString().trim());
224                 if (metadata != null) {
225                     JsonObject jsonMetadata = (JsonObject) JsonParser.parseString(metadata);
226                     dmd = gson.fromJson(jsonMetadata, DeviceMetaData.class);
227                     // only keep the enum, if any - that's all we care for events we receive via multicast
228                     // all other fields are nulled
229                     if (dmd != null) {
230                         dmd.LocalizedID = null;
231                         dmd.LocalizedValue = null;
232                         dmd.Filter = null;
233                         dmd.description = null;
234                     }
235                 }
236             }
237             JsonObject jsonMetadata = dp.Metadata;
238             if (jsonMetadata != null) {
239                 String metadata = jsonMetadata.toString().replace("enum", "MieleEnum");
240                 JsonObject jsonMetaData = (JsonObject) JsonParser.parseString(metadata);
241                 dmd = gson.fromJson(jsonMetaData, DeviceMetaData.class);
242                 metaDataCache.put(new StringBuilder().append(dp.Name).toString().trim(), metadata);
243             }
244
245             if (EXTENDED_DEVICE_STATE_PROPERTY_NAME.equals(dp.Name)) {
246                 if (!dp.Value.isEmpty()) {
247                     byte[] extendedStateBytes = DeviceUtil.stringToBytes(dp.Value);
248                     logger.trace("Extended device state for {}: {}", getThing().getUID(),
249                             DeviceUtil.bytesToHex(extendedStateBytes));
250                     if (this instanceof ExtendedDeviceStateListener) {
251                         ((ExtendedDeviceStateListener) this).onApplianceExtendedStateChanged(extendedStateBytes);
252                     }
253                 }
254                 return;
255             }
256
257             ApplianceChannelSelector selector = null;
258             try {
259                 selector = getValueSelectorFromMieleID(dp.Name);
260             } catch (Exception h) {
261                 logger.trace("{} is not a valid channel for a {}", dp.Name, modelID);
262             }
263
264             String dpValue = dp.Value.strip().trim();
265
266             if (selector != null) {
267                 String channelId = selector.getChannelID();
268                 ThingUID thingUid = getThing().getUID();
269                 State state = selector.getState(dpValue, dmd, this.translationProvider);
270                 if (selector.isProperty()) {
271                     String value = state.toString();
272                     logger.trace("Updating the property '{}' of '{}' to '{}'", channelId, thingUid, value);
273                     updateProperty(channelId, value);
274                 } else {
275                     ChannelUID theChannelUID = new ChannelUID(thingUid, channelId);
276                     logger.trace("Update state of {} with getState '{}'", theChannelUID, state);
277                     updateState(theChannelUID, state);
278                     updateRawChannel(dp.Name, dpValue);
279                 }
280             }
281         } catch (IllegalArgumentException e) {
282             logger.warn("An exception occurred while processing a changed device property: '{}'", e.getMessage());
283         }
284     }
285
286     protected void updateExtendedState(String channelId, State state) {
287         ChannelUID channelUid = new ChannelUID(getThing().getUID(), channelId);
288         logger.trace("Update state of {} with extended state '{}'", channelUid, state);
289         updateState(channelUid, state);
290     }
291
292     protected void updateSwitchOnOffFromState(DeviceProperty dp) {
293         if (!STATE_PROPERTY_NAME.equals(dp.Name)) {
294             return;
295         }
296
297         // Switch is trigger channel, but current state can be deduced from state.
298         ChannelUID channelUid = new ChannelUID(getThing().getUID(), SWITCH_CHANNEL_ID);
299         State state = OnOffType.from(!dp.Value.equals(String.valueOf(STATE_OFF)));
300         logger.trace("Update state of {} to {} through '{}'", channelUid, state, dp.Name);
301         updateState(channelUid, state);
302     }
303
304     protected void updateSwitchStartStopFromState(DeviceProperty dp) {
305         if (!STATE_PROPERTY_NAME.equals(dp.Name)) {
306             return;
307         }
308
309         // Switch is trigger channel, but current state can be deduced from state.
310         ChannelUID channelUid = new ChannelUID(getThing().getUID(), SWITCH_CHANNEL_ID);
311         State state = OnOffType.from(dp.Value.equals(String.valueOf(STATE_RUNNING)));
312         logger.trace("Update state of {} to {} through '{}'", channelUid, state, dp.Name);
313         updateState(channelUid, state);
314     }
315
316     /**
317      * Update raw value channels for properties already mapped to text channels.
318      * Currently ApplianceChannelSelector only supports 1:1 mapping from property
319      * to channel.
320      */
321     private void updateRawChannel(String propertyName, String value) {
322         String channelId;
323         switch (propertyName) {
324             case STATE_PROPERTY_NAME:
325                 channelId = STATE_CHANNEL_ID;
326                 break;
327             case PROGRAM_ID_PROPERTY_NAME:
328                 channelId = PROGRAM_CHANNEL_ID;
329                 break;
330             default:
331                 return;
332         }
333         ApplianceChannelSelector selector = null;
334         try {
335             selector = getValueSelectorFromChannelID(channelId);
336         } catch (IllegalArgumentException e) {
337             logger.trace("{} is not a valid channel for a {}", channelId, modelID);
338             return;
339         }
340         ChannelUID channelUid = new ChannelUID(getThing().getUID(), channelId);
341         State state = selector.getState(value);
342         logger.trace("Update state of {} with getState '{}'", channelUid, state);
343         updateState(channelUid, state);
344     }
345
346     @Override
347     public void onApplianceRemoved() {
348         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.GONE);
349     }
350
351     @Override
352     public void onApplianceAdded(HomeDevice appliance) {
353         Map<String, String> properties = editProperties();
354         String vendor = appliance.Vendor;
355         if (vendor != null) {
356             properties.put(Thing.PROPERTY_VENDOR, vendor);
357         }
358         properties.put(Thing.PROPERTY_MODEL_ID, appliance.getApplianceModel());
359         properties.put(Thing.PROPERTY_SERIAL_NUMBER, appliance.getSerialNumber());
360         properties.put(Thing.PROPERTY_FIRMWARE_VERSION, appliance.getFirmwareVersion());
361         String protocolAdapterName = appliance.ProtocolAdapterName;
362         if (protocolAdapterName != null) {
363             properties.put(PROPERTY_PROTOCOL_ADAPTER, protocolAdapterName);
364         }
365         String deviceClass = appliance.getDeviceClass();
366         if (deviceClass != null) {
367             properties.put(PROPERTY_DEVICE_CLASS, deviceClass);
368         }
369         String connectionType = appliance.getConnectionType();
370         if (connectionType != null) {
371             properties.put(PROPERTY_CONNECTION_TYPE, connectionType);
372         }
373         String connectionBaudRate = appliance.getConnectionBaudRate();
374         if (connectionBaudRate != null) {
375             properties.put(PROPERTY_CONNECTION_BAUD_RATE, connectionBaudRate);
376         }
377         updateProperties(properties);
378         updateStatus(ThingStatus.ONLINE);
379     }
380
381     protected synchronized @Nullable MieleBridgeHandler getMieleBridgeHandler() {
382         if (this.bridgeHandler == null) {
383             Bridge bridge = getBridge();
384             if (bridge == null) {
385                 return null;
386             }
387             ThingHandler handler = bridge.getHandler();
388             if (handler instanceof MieleBridgeHandler) {
389                 this.bridgeHandler = (MieleBridgeHandler) handler;
390             }
391         }
392         return this.bridgeHandler;
393     }
394
395     protected boolean isResultProcessable(JsonElement result) {
396         return !result.isJsonNull();
397     }
398 }