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.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;
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;
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)
66 public abstract class MieleApplianceHandler<E extends Enum<E> & ApplianceChannelSelector> extends BaseThingHandler
67 implements ApplianceStatusListener {
69 private final Logger logger = LoggerFactory.getLogger(MieleApplianceHandler.class);
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);
75 protected Gson gson = new Gson();
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;
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;
94 this.translationProvider = new MieleTranslationProvider(i18nProvider, localeProvider);
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));
104 for (ApplianceChannelSelector c : enumConstants) {
105 if (c.getChannelID().equals(valueSelectorText)) {
110 throw new IllegalArgumentException(String.format("Not valid value selector: %s", valueSelectorText));
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));
120 for (ApplianceChannelSelector c : enumConstants) {
121 if (!c.getMieleID().isEmpty() && c.getMieleID().equals(valueSelectorText)) {
126 throw new IllegalArgumentException(String.format("Not valid value selector: %s", valueSelectorText));
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");
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");
145 initializeTranslationProvider(bridge);
146 updateStatus(ThingStatus.UNKNOWN);
148 MieleBridgeHandler bridgeHandler = getMieleBridgeHandler();
149 if (bridgeHandler != null) {
150 bridgeHandler.registerApplianceStatusListener(applianceId, this);
154 private void initializeTranslationProvider(Bridge bridge) {
155 Locale locale = null;
156 String language = (String) bridge.getConfiguration().get(LANGUAGE);
157 if (language != null && !language.isBlank()) {
159 locale = new Locale.Builder().setLanguageTag(language).build();
160 } catch (IllformedLocaleException e) {
161 logger.warn("Invalid language configured: {}", e.getMessage());
164 if (locale == null) {
165 logger.debug("No language configured, using system language.");
166 this.translationProvider = new MieleTranslationProvider(i18nProvider, localeProvider);
168 this.translationProvider = new MieleTranslationProvider(i18nProvider, localeProvider, locale);
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);
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
195 public void onApplianceStateChanged(DeviceClassObject dco) {
196 JsonArray properties = dco.Properties;
197 if (properties == null) {
201 for (JsonElement prop : properties.getAsJsonArray()) {
203 DeviceProperty dp = gson.fromJson(prop, DeviceProperty.class);
207 if (!EXTENDED_DEVICE_STATE_PROPERTY_NAME.equals(dp.Name)) {
208 dp.Value = dp.Value.trim();
209 dp.Value = dp.Value.strip();
211 onAppliancePropertyChanged(dp);
212 } catch (Exception p) {
213 // Ignore - this is due to an unrecognized and not yet reverse-engineered array property
219 public void onAppliancePropertyChanged(DeviceProperty dp) {
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
230 dmd.LocalizedID = null;
231 dmd.LocalizedValue = null;
233 dmd.description = null;
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);
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);
257 ApplianceChannelSelector selector = null;
259 selector = getValueSelectorFromMieleID(dp.Name);
260 } catch (Exception h) {
261 logger.trace("{} is not a valid channel for a {}", dp.Name, modelID);
264 String dpValue = dp.Value.strip().trim();
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);
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);
281 } catch (IllegalArgumentException e) {
282 logger.warn("An exception occurred while processing a changed device property: '{}'", e.getMessage());
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);
292 protected void updateSwitchOnOffFromState(DeviceProperty dp) {
293 if (!STATE_PROPERTY_NAME.equals(dp.Name)) {
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);
304 protected void updateSwitchStartStopFromState(DeviceProperty dp) {
305 if (!STATE_PROPERTY_NAME.equals(dp.Name)) {
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);
317 * Update raw value channels for properties already mapped to text channels.
318 * Currently ApplianceChannelSelector only supports 1:1 mapping from property
321 private void updateRawChannel(String propertyName, String value) {
323 switch (propertyName) {
324 case STATE_PROPERTY_NAME:
325 channelId = STATE_CHANNEL_ID;
327 case PROGRAM_ID_PROPERTY_NAME:
328 channelId = PROGRAM_CHANNEL_ID;
333 ApplianceChannelSelector selector = null;
335 selector = getValueSelectorFromChannelID(channelId);
336 } catch (IllegalArgumentException e) {
337 logger.trace("{} is not a valid channel for a {}", channelId, modelID);
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);
347 public void onApplianceRemoved() {
348 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.GONE);
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);
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);
365 String deviceClass = appliance.getDeviceClass();
366 if (deviceClass != null) {
367 properties.put(PROPERTY_DEVICE_CLASS, deviceClass);
369 String connectionType = appliance.getConnectionType();
370 if (connectionType != null) {
371 properties.put(PROPERTY_CONNECTION_TYPE, connectionType);
373 String connectionBaudRate = appliance.getConnectionBaudRate();
374 if (connectionBaudRate != null) {
375 properties.put(PROPERTY_CONNECTION_BAUD_RATE, connectionBaudRate);
377 updateProperties(properties);
378 updateStatus(ThingStatus.ONLINE);
381 protected synchronized @Nullable MieleBridgeHandler getMieleBridgeHandler() {
382 if (this.bridgeHandler == null) {
383 Bridge bridge = getBridge();
384 if (bridge == null) {
387 ThingHandler handler = bridge.getHandler();
388 if (handler instanceof MieleBridgeHandler) {
389 this.bridgeHandler = (MieleBridgeHandler) handler;
392 return this.bridgeHandler;
395 protected boolean isResultProcessable(JsonElement result) {
396 return !result.isJsonNull();