2 * Copyright (c) 2010-2022 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.miele.internal.handler;
15 import static org.openhab.binding.miele.internal.MieleBindingConstants.*;
17 import java.util.HashMap;
18 import java.util.IllformedLocaleException;
19 import java.util.Locale;
22 import java.util.stream.Collectors;
23 import java.util.stream.Stream;
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;
50 import com.google.gson.Gson;
51 import com.google.gson.JsonElement;
52 import com.google.gson.JsonObject;
53 import com.google.gson.JsonParser;
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
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)
65 public abstract class MieleApplianceHandler<E extends Enum<E> & ApplianceChannelSelector> extends BaseThingHandler
66 implements ApplianceStatusListener {
68 private final Logger logger = LoggerFactory.getLogger(MieleApplianceHandler.class);
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());
75 protected Gson gson = new Gson();
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;
85 protected Map<String, String> metaDataCache = new HashMap<>();
87 public MieleApplianceHandler(Thing thing, TranslationProvider i18nProvider, LocaleProvider localeProvider,
88 Class<E> selectorType, String modelID) {
90 this.i18nProvider = i18nProvider;
91 this.localeProvider = localeProvider;
92 this.selectorType = selectorType;
93 this.modelID = modelID;
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));
103 for (ApplianceChannelSelector c : enumConstants) {
104 if (c != null && c.getChannelID() != null && c.getChannelID().equals(valueSelectorText)) {
109 throw new IllegalArgumentException(String.format("Not valid value selector: %s", valueSelectorText));
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));
119 for (ApplianceChannelSelector c : enumConstants) {
120 if (c != null && c.getMieleID() != null && c.getMieleID().equals(valueSelectorText)) {
125 throw new IllegalArgumentException(String.format("Not valid value selector: %s", valueSelectorText));
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();
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);
147 private void initializeTranslationProvider(Bridge bridge) {
148 Locale locale = null;
149 String language = (String) bridge.getConfiguration().get(LANGUAGE);
150 if (language != null && !language.isBlank()) {
152 locale = new Locale.Builder().setLanguageTag(language).build();
153 } catch (IllformedLocaleException e) {
154 logger.error("Invalid language configured: {}", e.getMessage());
157 if (locale == null) {
158 logger.debug("No language configured, using system language.");
159 this.translationProvider = new MieleTranslationProvider(i18nProvider, localeProvider);
161 this.translationProvider = new MieleTranslationProvider(i18nProvider, localeProvider, locale);
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);
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
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())) {
194 for (JsonElement prop : dco.Properties.getAsJsonArray()) {
196 DeviceProperty dp = gson.fromJson(prop, DeviceProperty.class);
200 if (!EXTENDED_DEVICE_STATE_PROPERTY_NAME.equals(dp.Name)) {
201 dp.Value = dp.Value.trim();
202 dp.Value = dp.Value.strip();
204 onAppliancePropertyChanged(applicationIdentifier, dp);
205 } catch (Exception p) {
206 // Ignore - this is due to an unrecognized and not yet reverse-engineered array property
212 public void onAppliancePropertyChanged(FullyQualifiedApplianceIdentifier applicationIdentifier, DeviceProperty dp) {
213 String myApplianceId = (String) getThing().getConfiguration().getProperties().get(APPLIANCE_ID);
215 if (myApplianceId == null || !myApplianceId.equals(applicationIdentifier.getApplianceId())) {
219 this.onAppliancePropertyChanged(dp);
222 protected void onAppliancePropertyChanged(DeviceProperty dp) {
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
233 dmd.LocalizedID = null;
234 dmd.LocalizedValue = null;
236 dmd.description = null;
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);
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);
259 ApplianceChannelSelector selector = null;
261 selector = getValueSelectorFromMieleID(dp.Name);
262 } catch (Exception h) {
263 logger.trace("{} is not a valid channel for a {}", dp.Name, modelID);
266 String dpValue = dp.Value.strip().trim();
268 if (selector != null) {
269 if (!selector.isProperty()) {
270 ChannelUID theChannelUID = new ChannelUID(getThing().getUID(), selector.getChannelID());
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);
278 updateState(theChannelUID, UnDefType.UNDEF);
281 logger.debug("Updating the property '{}' of '{}' to '{}'", selector.getChannelID(),
282 getThing().getUID(), selector.getState(dpValue, dmd, this.translationProvider).toString());
284 Map<@NonNull String, @NonNull String> properties = editProperties();
285 properties.put(selector.getChannelID(),
286 selector.getState(dpValue, dmd, this.translationProvider).toString());
287 updateProperties(properties);
290 } catch (IllegalArgumentException e) {
291 logger.error("An exception occurred while processing a changed device property :'{}'", e.getMessage());
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);
302 * Update raw value channels for properties already mapped to text channels.
303 * Currently ApplianceChannelSelector only supports 1:1 mapping from property
306 private void updateRawChannel(String propertyName, String value) {
308 switch (propertyName) {
309 case STATE_PROPERTY_NAME:
310 channelId = STATE_CHANNEL_ID;
312 case PROGRAM_ID_PROPERTY_NAME:
313 channelId = PROGRAM_CHANNEL_ID;
318 ApplianceChannelSelector selector = null;
320 selector = getValueSelectorFromChannelID(channelId);
321 } catch (IllegalArgumentException e) {
322 logger.trace("{} is not a valid channel for a {}", channelId, modelID);
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);
332 public void onApplianceRemoved(HomeDevice appliance) {
333 if (applianceId == null) {
337 if (applianceId.equals(appliance.getApplianceIdentifier().getApplianceId())) {
338 updateStatus(ThingStatus.OFFLINE);
343 public void onApplianceAdded(HomeDevice appliance) {
344 if (applianceId == null) {
348 FullyQualifiedApplianceIdentifier applianceIdentifier = appliance.getApplianceIdentifier();
350 if (applianceId.equals(applianceIdentifier.getApplianceId())) {
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);
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);
364 updateProperties(properties);
365 updateStatus(ThingStatus.ONLINE);
369 private synchronized MieleBridgeHandler getMieleBridgeHandler() {
370 if (this.bridgeHandler == null) {
371 Bridge bridge = getBridge();
372 if (bridge == null) {
375 ThingHandler handler = bridge.getHandler();
376 if (handler instanceof MieleBridgeHandler) {
377 this.bridgeHandler = (MieleBridgeHandler) handler;
378 this.bridgeHandler.registerApplianceStatusListener(this);
383 return this.bridgeHandler;
386 protected boolean isResultProcessable(JsonElement result) {
387 if (result == null) {
388 throw new IllegalArgumentException("Provided result is null");
390 return !result.isJsonNull();