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;
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;
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;
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
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)
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 = 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);
74 protected Gson gson = new Gson();
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;
84 protected Map<String, String> metaDataCache = new HashMap<>();
86 public MieleApplianceHandler(Thing thing, TranslationProvider i18nProvider, LocaleProvider localeProvider,
87 Class<E> selectorType, String modelID) {
89 this.i18nProvider = i18nProvider;
90 this.localeProvider = localeProvider;
91 this.selectorType = selectorType;
92 this.modelID = modelID;
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));
102 for (ApplianceChannelSelector c : enumConstants) {
103 if (c.getChannelID().equals(valueSelectorText)) {
108 throw new IllegalArgumentException(String.format("Not valid value selector: %s", valueSelectorText));
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));
118 for (ApplianceChannelSelector c : enumConstants) {
119 if (!c.getMieleID().isEmpty() && c.getMieleID().equals(valueSelectorText)) {
124 throw new IllegalArgumentException(String.format("Not valid value selector: %s", valueSelectorText));
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();
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);
146 private void initializeTranslationProvider(Bridge bridge) {
147 Locale locale = null;
148 String language = (String) bridge.getConfiguration().get(LANGUAGE);
149 if (language != null && !language.isBlank()) {
151 locale = new Locale.Builder().setLanguageTag(language).build();
152 } catch (IllformedLocaleException e) {
153 logger.error("Invalid language configured: {}", e.getMessage());
156 if (locale == null) {
157 logger.debug("No language configured, using system language.");
158 this.translationProvider = new MieleTranslationProvider(i18nProvider, localeProvider);
160 this.translationProvider = new MieleTranslationProvider(i18nProvider, localeProvider, locale);
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);
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
186 public void onApplianceStateChanged(FullyQualifiedApplianceIdentifier applicationIdentifier,
187 DeviceClassObject dco) {
188 String applianceId = this.applianceId;
189 if (applianceId == null || !applianceId.equals(applicationIdentifier.getApplianceId())) {
193 JsonArray properties = dco.Properties;
194 if (properties == null) {
198 for (JsonElement prop : properties.getAsJsonArray()) {
200 DeviceProperty dp = gson.fromJson(prop, DeviceProperty.class);
204 if (!EXTENDED_DEVICE_STATE_PROPERTY_NAME.equals(dp.Name)) {
205 dp.Value = dp.Value.trim();
206 dp.Value = dp.Value.strip();
208 onAppliancePropertyChanged(applicationIdentifier, dp);
209 } catch (Exception p) {
210 // Ignore - this is due to an unrecognized and not yet reverse-engineered array property
216 public void onAppliancePropertyChanged(FullyQualifiedApplianceIdentifier applicationIdentifier, DeviceProperty dp) {
217 String applianceId = this.applianceId;
219 if (applianceId == null || !applianceId.equals(applicationIdentifier.getApplianceId())) {
223 this.onAppliancePropertyChanged(dp);
226 protected void onAppliancePropertyChanged(DeviceProperty dp) {
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
237 dmd.LocalizedID = null;
238 dmd.LocalizedValue = null;
240 dmd.description = null;
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);
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);
264 ApplianceChannelSelector selector = null;
266 selector = getValueSelectorFromMieleID(dp.Name);
267 } catch (Exception h) {
268 logger.trace("{} is not a valid channel for a {}", dp.Name, modelID);
271 String dpValue = dp.Value.strip().trim();
273 if (selector != null) {
274 if (!selector.isProperty()) {
275 ChannelUID theChannelUID = new ChannelUID(getThing().getUID(), selector.getChannelID());
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);
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);
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 String applianceId = this.applianceId;
334 if (applianceId == null) {
338 FullyQualifiedApplianceIdentifier applianceIdentifier = appliance.getApplianceIdentifier();
339 if (applianceIdentifier == null) {
343 if (applianceId.equals(applianceIdentifier.getApplianceId())) {
344 updateStatus(ThingStatus.OFFLINE);
349 public void onApplianceAdded(HomeDevice appliance) {
350 String applianceId = this.applianceId;
351 if (applianceId == null) {
355 FullyQualifiedApplianceIdentifier applianceIdentifier = appliance.getApplianceIdentifier();
356 if (applianceIdentifier == null) {
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);
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);
373 String deviceClass = appliance.getDeviceClass();
374 if (deviceClass != null) {
375 properties.put(PROPERTY_DEVICE_CLASS, deviceClass);
377 String connectionType = appliance.getConnectionType();
378 if (connectionType != null) {
379 properties.put(PROPERTY_CONNECTION_TYPE, connectionType);
381 String connectionBaudRate = appliance.getConnectionBaudRate();
382 if (connectionBaudRate != null) {
383 properties.put(PROPERTY_CONNECTION_BAUD_RATE, connectionBaudRate);
385 updateProperties(properties);
386 updateStatus(ThingStatus.ONLINE);
390 private synchronized @Nullable MieleBridgeHandler getMieleBridgeHandler() {
391 if (this.bridgeHandler == null) {
392 Bridge bridge = getBridge();
393 if (bridge == null) {
396 ThingHandler handler = bridge.getHandler();
397 if (handler instanceof MieleBridgeHandler) {
398 var bridgeHandler = (MieleBridgeHandler) handler;
399 this.bridgeHandler = bridgeHandler;
400 bridgeHandler.registerApplianceStatusListener(this);
405 return this.bridgeHandler;
408 protected boolean isResultProcessable(JsonElement result) {
409 return !result.isJsonNull();