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.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;
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 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;
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;
93 this.translationProvider = new MieleTranslationProvider(i18nProvider, localeProvider);
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.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.getMieleID().isEmpty() && c.getMieleID().equals(valueSelectorText)) {
125 throw new IllegalArgumentException(String.format("Not valid value selector: %s", valueSelectorText));
129 public void initialize() {
130 logger.debug("Initializing handler for thing {}", getThing().getUID());
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");
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");
144 initializeTranslationProvider(bridge);
145 updateStatus(ThingStatus.UNKNOWN);
147 MieleBridgeHandler bridgeHandler = getMieleBridgeHandler();
148 if (bridgeHandler != null) {
149 bridgeHandler.registerApplianceStatusListener(applianceId, this);
153 private void initializeTranslationProvider(Bridge bridge) {
154 Locale locale = null;
155 String language = (String) bridge.getConfiguration().get(LANGUAGE);
156 if (language != null && !language.isBlank()) {
158 locale = new Locale.Builder().setLanguageTag(language).build();
159 } catch (IllformedLocaleException e) {
160 logger.warn("Invalid language configured: {}", e.getMessage());
163 if (locale == null) {
164 logger.debug("No language configured, using system language.");
165 this.translationProvider = new MieleTranslationProvider(i18nProvider, localeProvider);
167 this.translationProvider = new MieleTranslationProvider(i18nProvider, localeProvider, locale);
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);
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
194 public void onApplianceStateChanged(DeviceClassObject dco) {
195 JsonArray properties = dco.Properties;
196 if (properties == null) {
200 for (JsonElement prop : properties.getAsJsonArray()) {
202 DeviceProperty dp = gson.fromJson(prop, DeviceProperty.class);
206 if (!EXTENDED_DEVICE_STATE_PROPERTY_NAME.equals(dp.Name)) {
207 dp.Value = dp.Value.trim();
208 dp.Value = dp.Value.strip();
210 onAppliancePropertyChanged(dp);
211 } catch (Exception p) {
212 // Ignore - this is due to an unrecognized and not yet reverse-engineered array property
218 public void onAppliancePropertyChanged(DeviceProperty dp) {
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
229 dmd.LocalizedID = null;
230 dmd.LocalizedValue = null;
232 dmd.description = null;
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);
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);
256 ApplianceChannelSelector selector = null;
258 selector = getValueSelectorFromMieleID(dp.Name);
259 } catch (Exception h) {
260 logger.trace("{} is not a valid channel for a {}", dp.Name, modelID);
263 String dpValue = dp.Value.strip().trim();
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);
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);
280 } catch (IllegalArgumentException e) {
281 logger.warn("An exception occurred while processing a changed device property: '{}'", e.getMessage());
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);
292 * Update raw value channels for properties already mapped to text channels.
293 * Currently ApplianceChannelSelector only supports 1:1 mapping from property
296 private void updateRawChannel(String propertyName, String value) {
298 switch (propertyName) {
299 case STATE_PROPERTY_NAME:
300 channelId = STATE_CHANNEL_ID;
302 case PROGRAM_ID_PROPERTY_NAME:
303 channelId = PROGRAM_CHANNEL_ID;
308 ApplianceChannelSelector selector = null;
310 selector = getValueSelectorFromChannelID(channelId);
311 } catch (IllegalArgumentException e) {
312 logger.trace("{} is not a valid channel for a {}", channelId, modelID);
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);
322 public void onApplianceRemoved() {
323 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.GONE);
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);
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);
340 String deviceClass = appliance.getDeviceClass();
341 if (deviceClass != null) {
342 properties.put(PROPERTY_DEVICE_CLASS, deviceClass);
344 String connectionType = appliance.getConnectionType();
345 if (connectionType != null) {
346 properties.put(PROPERTY_CONNECTION_TYPE, connectionType);
348 String connectionBaudRate = appliance.getConnectionBaudRate();
349 if (connectionBaudRate != null) {
350 properties.put(PROPERTY_CONNECTION_BAUD_RATE, connectionBaudRate);
352 updateProperties(properties);
353 updateStatus(ThingStatus.ONLINE);
356 protected synchronized @Nullable MieleBridgeHandler getMieleBridgeHandler() {
357 if (this.bridgeHandler == null) {
358 Bridge bridge = getBridge();
359 if (bridge == null) {
362 ThingHandler handler = bridge.getHandler();
363 if (handler instanceof MieleBridgeHandler) {
364 this.bridgeHandler = (MieleBridgeHandler) handler;
367 return this.bridgeHandler;
370 protected boolean isResultProcessable(JsonElement result) {
371 return !result.isJsonNull();