]> git.basschouten.com Git - openhab-addons.git/blob
daaba7146744ebc51e0ab878973553459dc6c6a7
[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.time.Instant;
18 import java.time.ZoneId;
19 import java.time.ZonedDateTime;
20 import java.time.temporal.ChronoUnit;
21 import java.util.HashMap;
22 import java.util.IllformedLocaleException;
23 import java.util.Locale;
24 import java.util.Map;
25 import java.util.Set;
26
27 import org.eclipse.jdt.annotation.NonNullByDefault;
28 import org.eclipse.jdt.annotation.Nullable;
29 import org.openhab.binding.miele.internal.DeviceUtil;
30 import org.openhab.binding.miele.internal.MieleTranslationProvider;
31 import org.openhab.binding.miele.internal.TimeStabilizer;
32 import org.openhab.binding.miele.internal.api.dto.DeviceClassObject;
33 import org.openhab.binding.miele.internal.api.dto.DeviceMetaData;
34 import org.openhab.binding.miele.internal.api.dto.DeviceProperty;
35 import org.openhab.binding.miele.internal.api.dto.HomeDevice;
36 import org.openhab.core.i18n.LocaleProvider;
37 import org.openhab.core.i18n.TimeZoneProvider;
38 import org.openhab.core.i18n.TranslationProvider;
39 import org.openhab.core.library.types.DateTimeType;
40 import org.openhab.core.library.types.OnOffType;
41 import org.openhab.core.thing.Bridge;
42 import org.openhab.core.thing.ChannelUID;
43 import org.openhab.core.thing.Thing;
44 import org.openhab.core.thing.ThingStatus;
45 import org.openhab.core.thing.ThingStatusDetail;
46 import org.openhab.core.thing.ThingTypeUID;
47 import org.openhab.core.thing.ThingUID;
48 import org.openhab.core.thing.binding.BaseThingHandler;
49 import org.openhab.core.thing.binding.ThingHandler;
50 import org.openhab.core.types.Command;
51 import org.openhab.core.types.RefreshType;
52 import org.openhab.core.types.State;
53 import org.openhab.core.types.UnDefType;
54 import org.slf4j.Logger;
55 import org.slf4j.LoggerFactory;
56
57 import com.google.gson.Gson;
58 import com.google.gson.JsonArray;
59 import com.google.gson.JsonElement;
60 import com.google.gson.JsonObject;
61 import com.google.gson.JsonParser;
62
63 /**
64  * The {@link MieleApplianceHandler} is an abstract class
65  * responsible for handling commands, which are sent to one
66  * of the channels of the appliance that understands/"talks"
67  * the {@link ApplianceChannelSelector} datapoints
68  *
69  * @author Karel Goderis - Initial contribution
70  * @author Martin Lepsy - Added check for JsonNull result
71  * @author Jacob Laursen - Fixed multicast and protocol support (ZigBee/LAN)
72  */
73 @NonNullByDefault
74 public abstract class MieleApplianceHandler<E extends Enum<E> & ApplianceChannelSelector> extends BaseThingHandler
75         implements ApplianceStatusListener {
76
77     private final Logger logger = LoggerFactory.getLogger(MieleApplianceHandler.class);
78
79     public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Set.of(THING_TYPE_DISHWASHER, THING_TYPE_OVEN,
80             THING_TYPE_FRIDGE, THING_TYPE_DRYER, THING_TYPE_HOB, THING_TYPE_FRIDGEFREEZER, THING_TYPE_HOOD,
81             THING_TYPE_WASHINGMACHINE, THING_TYPE_COFFEEMACHINE);
82
83     protected Gson gson = new Gson();
84
85     protected @Nullable String applianceId;
86     private @Nullable MieleBridgeHandler bridgeHandler;
87     protected TranslationProvider i18nProvider;
88     protected LocaleProvider localeProvider;
89     protected MieleTranslationProvider translationProvider;
90     private TimeZoneProvider timeZoneProvider;
91     private TimeStabilizer startTimeStabilizer;
92     private TimeStabilizer finishTimeStabilizer;
93     private Class<E> selectorType;
94     protected String modelID;
95
96     protected Map<String, String> metaDataCache = new HashMap<>();
97
98     public MieleApplianceHandler(Thing thing, TranslationProvider i18nProvider, LocaleProvider localeProvider,
99             TimeZoneProvider timeZoneProvider, Class<E> selectorType, String modelID) {
100         super(thing);
101         this.i18nProvider = i18nProvider;
102         this.localeProvider = localeProvider;
103         this.selectorType = selectorType;
104         this.modelID = modelID;
105         this.translationProvider = new MieleTranslationProvider(i18nProvider, localeProvider);
106         this.timeZoneProvider = timeZoneProvider;
107         this.startTimeStabilizer = new TimeStabilizer();
108         this.finishTimeStabilizer = new TimeStabilizer();
109     }
110
111     public ApplianceChannelSelector getValueSelectorFromChannelID(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.getChannelID().equals(valueSelectorText)) {
120                 return c;
121             }
122         }
123
124         throw new IllegalArgumentException(String.format("Not valid value selector: %s", valueSelectorText));
125     }
126
127     public ApplianceChannelSelector getValueSelectorFromMieleID(String valueSelectorText)
128             throws IllegalArgumentException {
129         E[] enumConstants = selectorType.getEnumConstants();
130         if (enumConstants == null) {
131             throw new IllegalArgumentException(
132                     String.format("Could not get enum constants for value selector: %s", valueSelectorText));
133         }
134         for (ApplianceChannelSelector c : enumConstants) {
135             if (!c.getMieleID().isEmpty() && c.getMieleID().equals(valueSelectorText)) {
136                 return c;
137             }
138         }
139
140         throw new IllegalArgumentException(String.format("Not valid value selector: %s", valueSelectorText));
141     }
142
143     @Override
144     public void initialize() {
145         logger.debug("Initializing handler for thing {}", getThing().getUID());
146         final String applianceId = (String) getThing().getConfiguration().getProperties().get(APPLIANCE_ID);
147         if (applianceId == null || applianceId.isBlank()) {
148             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR,
149                     "@text/offline.configuration-error.uid-not-set");
150             return;
151         }
152         this.applianceId = applianceId;
153         Bridge bridge = getBridge();
154         if (bridge == null) {
155             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR,
156                     "@text/offline.configuration-error.bridge-missing");
157             return;
158         }
159         initializeTranslationProvider(bridge);
160         updateStatus(ThingStatus.UNKNOWN);
161
162         MieleBridgeHandler bridgeHandler = getMieleBridgeHandler();
163         if (bridgeHandler != null) {
164             bridgeHandler.registerApplianceStatusListener(applianceId, this);
165         }
166     }
167
168     private void initializeTranslationProvider(Bridge bridge) {
169         Locale locale = null;
170         String language = (String) bridge.getConfiguration().get(LANGUAGE);
171         if (language != null && !language.isBlank()) {
172             try {
173                 locale = new Locale.Builder().setLanguageTag(language).build();
174             } catch (IllformedLocaleException e) {
175                 logger.warn("Invalid language configured: {}", e.getMessage());
176             }
177         }
178         if (locale == null) {
179             logger.debug("No language configured, using system language.");
180             this.translationProvider = new MieleTranslationProvider(i18nProvider, localeProvider);
181         } else {
182             this.translationProvider = new MieleTranslationProvider(i18nProvider, localeProvider, locale);
183         }
184     }
185
186     @Override
187     public void dispose() {
188         logger.debug("Handler disposes. Unregistering listener.");
189         String applianceId = this.applianceId;
190         if (applianceId != null) {
191             MieleBridgeHandler bridgeHandler = getMieleBridgeHandler();
192             if (bridgeHandler != null) {
193                 bridgeHandler.unregisterApplianceStatusListener(applianceId, this);
194             }
195             applianceId = null;
196         }
197         startTimeStabilizer.clear();
198         finishTimeStabilizer.clear();
199     }
200
201     @Override
202     public void handleCommand(ChannelUID channelUID, Command command) {
203         // Here we could handle commands that are common to all Miele Appliances, but so far I don't know of any
204         if (command instanceof RefreshType) {
205             // Placeholder for future refinement
206             return;
207         }
208     }
209
210     @Override
211     public void onApplianceStateChanged(DeviceClassObject dco) {
212         JsonArray properties = dco.Properties;
213         if (properties == null) {
214             return;
215         }
216
217         for (JsonElement prop : properties.getAsJsonArray()) {
218             try {
219                 DeviceProperty dp = gson.fromJson(prop, DeviceProperty.class);
220                 if (dp == null) {
221                     continue;
222                 }
223                 if (!EXTENDED_DEVICE_STATE_PROPERTY_NAME.equals(dp.Name)) {
224                     dp.Value = dp.Value.trim();
225                     dp.Value = dp.Value.strip();
226                 }
227                 onAppliancePropertyChanged(dp);
228             } catch (Exception p) {
229                 // Ignore - this is due to an unrecognized and not yet reverse-engineered array property
230             }
231         }
232     }
233
234     @Override
235     public void onAppliancePropertyChanged(DeviceProperty dp) {
236         try {
237             DeviceMetaData dmd = null;
238             if (dp.Metadata == null) {
239                 String metadata = metaDataCache.get(new StringBuilder().append(dp.Name).toString().trim());
240                 if (metadata != null) {
241                     JsonObject jsonMetadata = (JsonObject) JsonParser.parseString(metadata);
242                     dmd = gson.fromJson(jsonMetadata, DeviceMetaData.class);
243                     // only keep the enum, if any - that's all we care for events we receive via multicast
244                     // all other fields are nulled
245                     if (dmd != null) {
246                         dmd.LocalizedID = null;
247                         dmd.LocalizedValue = null;
248                         dmd.Filter = null;
249                         dmd.description = null;
250                     }
251                 }
252             }
253             JsonObject jsonMetadata = dp.Metadata;
254             if (jsonMetadata != null) {
255                 String metadata = jsonMetadata.toString().replace("enum", "MieleEnum");
256                 JsonObject jsonMetaData = (JsonObject) JsonParser.parseString(metadata);
257                 dmd = gson.fromJson(jsonMetaData, DeviceMetaData.class);
258                 metaDataCache.put(new StringBuilder().append(dp.Name).toString().trim(), metadata);
259             }
260
261             ThingUID thingUid = getThing().getUID();
262             if (EXTENDED_DEVICE_STATE_PROPERTY_NAME.equals(dp.Name)) {
263                 if (!dp.Value.isEmpty()) {
264                     byte[] extendedStateBytes = DeviceUtil.stringToBytes(dp.Value);
265                     logger.trace("Extended device state for {}: {}", getThing().getUID(),
266                             DeviceUtil.bytesToHex(extendedStateBytes));
267                     if (this instanceof ExtendedDeviceStateListener) {
268                         ((ExtendedDeviceStateListener) this).onApplianceExtendedStateChanged(extendedStateBytes);
269                     }
270                 }
271                 return;
272             } else if (START_TIME_PROPERTY_NAME.equals(dp.Name)) {
273                 updateStateFromTime(new ChannelUID(thingUid, START_CHANNEL_ID), dp.Value, startTimeStabilizer);
274                 return;
275             } else if (FINISH_TIME_PROPERTY_NAME.equals(dp.Name)) {
276                 updateDurationState(new ChannelUID(thingUid, FINISH_CHANNEL_ID), dp.Value);
277                 updateStateFromTime(new ChannelUID(thingUid, END_CHANNEL_ID), dp.Value, finishTimeStabilizer);
278                 return;
279             }
280
281             ApplianceChannelSelector selector = null;
282             try {
283                 selector = getValueSelectorFromMieleID(dp.Name);
284             } catch (Exception h) {
285                 logger.trace("{} is not a valid channel for a {}", dp.Name, modelID);
286             }
287
288             String dpValue = dp.Value.strip().trim();
289
290             if (selector != null) {
291                 String channelId = selector.getChannelID();
292                 State state = selector.getState(dpValue, dmd, this.translationProvider);
293                 if (selector.isProperty()) {
294                     String value = state.toString();
295                     logger.trace("Updating the property '{}' of '{}' to '{}'", channelId, thingUid, value);
296                     updateProperty(channelId, value);
297                 } else {
298                     ChannelUID theChannelUID = new ChannelUID(thingUid, channelId);
299                     logger.trace("Update state of {} with getState '{}'", theChannelUID, state);
300                     updateState(theChannelUID, state);
301                     updateRawChannel(dp.Name, dpValue);
302                 }
303             }
304         } catch (IllegalArgumentException e) {
305             logger.warn("An exception occurred while processing a changed device property: '{}'", e.getMessage());
306         }
307     }
308
309     protected void updateExtendedState(String channelId, State state) {
310         ChannelUID channelUid = new ChannelUID(getThing().getUID(), channelId);
311         logger.trace("Update state of {} with extended state '{}'", channelUid, state);
312         updateState(channelUid, state);
313     }
314
315     private void updateStateFromTime(ChannelUID channelUid, String value, TimeStabilizer stabilizer) {
316         try {
317             long minutesFromNow = Long.valueOf(value);
318             if (minutesFromNow > 0) {
319                 Instant rawTime = Instant.now().truncatedTo(ChronoUnit.MINUTES).plusSeconds(minutesFromNow * 60);
320                 ZonedDateTime correctedTime = stabilizer.apply(rawTime).atZone(timeZoneProvider.getTimeZone());
321                 ZonedDateTime truncatedTime = correctedTime.truncatedTo(ChronoUnit.MINUTES);
322                 logger.trace("Update state of {} from {} -> '{}' -> '{}' to '{}'", channelUid, minutesFromNow, rawTime,
323                         correctedTime, truncatedTime);
324                 updateState(channelUid, new DateTimeType(truncatedTime));
325                 return;
326             }
327         } catch (NumberFormatException e) {
328             // Fall through.
329         }
330         updateState(channelUid, UnDefType.UNDEF);
331         stabilizer.clear();
332     }
333
334     private void updateDurationState(ChannelUID channelUid, String value) {
335         try {
336             long minutesFromNow = Long.valueOf(value);
337             if (minutesFromNow > 0) {
338                 ZonedDateTime remaining = ZonedDateTime.ofInstant(Instant.ofEpochSecond(minutesFromNow * 60),
339                         ZoneId.of("UTC"));
340                 updateState(channelUid, new DateTimeType(remaining.withZoneSameLocal(timeZoneProvider.getTimeZone())));
341                 return;
342             }
343         } catch (NumberFormatException e) {
344             // Fall through.
345         }
346         updateState(channelUid, UnDefType.UNDEF);
347     }
348
349     protected void updateSwitchOnOffFromState(DeviceProperty dp) {
350         if (!STATE_PROPERTY_NAME.equals(dp.Name)) {
351             return;
352         }
353
354         // Switch is trigger channel, but current state can be deduced from state.
355         ChannelUID channelUid = new ChannelUID(getThing().getUID(), SWITCH_CHANNEL_ID);
356         State state = OnOffType.from(!dp.Value.equals(String.valueOf(STATE_OFF)));
357         logger.trace("Update state of {} to {} through '{}'", channelUid, state, dp.Name);
358         updateState(channelUid, state);
359     }
360
361     protected void updateSwitchStartStopFromState(DeviceProperty dp) {
362         if (!STATE_PROPERTY_NAME.equals(dp.Name)) {
363             return;
364         }
365
366         // Switch is trigger channel, but current state can be deduced from state.
367         ChannelUID channelUid = new ChannelUID(getThing().getUID(), SWITCH_CHANNEL_ID);
368         State state = OnOffType.from(dp.Value.equals(String.valueOf(STATE_RUNNING)));
369         logger.trace("Update state of {} to {} through '{}'", channelUid, state, dp.Name);
370         updateState(channelUid, state);
371     }
372
373     /**
374      * Update raw value channels for properties already mapped to text channels.
375      * Currently ApplianceChannelSelector only supports 1:1 mapping from property
376      * to channel.
377      */
378     private void updateRawChannel(String propertyName, String value) {
379         String channelId;
380         switch (propertyName) {
381             case STATE_PROPERTY_NAME:
382                 channelId = STATE_CHANNEL_ID;
383                 break;
384             case PROGRAM_ID_PROPERTY_NAME:
385                 channelId = PROGRAM_CHANNEL_ID;
386                 break;
387             default:
388                 return;
389         }
390         ApplianceChannelSelector selector = null;
391         try {
392             selector = getValueSelectorFromChannelID(channelId);
393         } catch (IllegalArgumentException e) {
394             logger.trace("{} is not a valid channel for a {}", channelId, modelID);
395             return;
396         }
397         ChannelUID channelUid = new ChannelUID(getThing().getUID(), channelId);
398         State state = selector.getState(value);
399         logger.trace("Update state of {} with getState '{}'", channelUid, state);
400         updateState(channelUid, state);
401     }
402
403     @Override
404     public void onApplianceRemoved() {
405         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.GONE);
406     }
407
408     @Override
409     public void onApplianceAdded(HomeDevice appliance) {
410         Map<String, String> properties = editProperties();
411         String vendor = appliance.Vendor;
412         if (vendor != null) {
413             properties.put(Thing.PROPERTY_VENDOR, vendor);
414         }
415         properties.put(Thing.PROPERTY_MODEL_ID, appliance.getApplianceModel());
416         properties.put(Thing.PROPERTY_SERIAL_NUMBER, appliance.getSerialNumber());
417         properties.put(Thing.PROPERTY_FIRMWARE_VERSION, appliance.getFirmwareVersion());
418         String protocolAdapterName = appliance.ProtocolAdapterName;
419         if (protocolAdapterName != null) {
420             properties.put(PROPERTY_PROTOCOL_ADAPTER, protocolAdapterName);
421         }
422         String deviceClass = appliance.getDeviceClass();
423         if (deviceClass != null) {
424             properties.put(PROPERTY_DEVICE_CLASS, deviceClass);
425         }
426         String connectionType = appliance.getConnectionType();
427         if (connectionType != null) {
428             properties.put(PROPERTY_CONNECTION_TYPE, connectionType);
429         }
430         String connectionBaudRate = appliance.getConnectionBaudRate();
431         if (connectionBaudRate != null) {
432             properties.put(PROPERTY_CONNECTION_BAUD_RATE, connectionBaudRate);
433         }
434         updateProperties(properties);
435         updateStatus(ThingStatus.ONLINE);
436     }
437
438     protected synchronized @Nullable MieleBridgeHandler getMieleBridgeHandler() {
439         if (this.bridgeHandler == null) {
440             Bridge bridge = getBridge();
441             if (bridge == null) {
442                 return null;
443             }
444             ThingHandler handler = bridge.getHandler();
445             if (handler instanceof MieleBridgeHandler) {
446                 this.bridgeHandler = (MieleBridgeHandler) handler;
447             }
448         }
449         return this.bridgeHandler;
450     }
451
452     protected boolean isResultProcessable(JsonElement result) {
453         return !result.isJsonNull();
454     }
455 }