[mielecloud] Fix washing machine can be started channel is not updated (#12583)
* Add tests to ensure that parsing works correctly * Fetch /actions on server sent event * Refactor onServerSentEvent * Remove ActionStateFetcher * Manually construct BigDecimal Signed-off-by: Björn Lange <bjoern.lange@itemis.de>
This commit is contained in:
@@ -27,7 +27,6 @@ import org.openhab.binding.mielecloud.internal.discovery.ThingInformationExtract
|
||||
import org.openhab.binding.mielecloud.internal.handler.channel.ActionsChannelState;
|
||||
import org.openhab.binding.mielecloud.internal.handler.channel.DeviceChannelState;
|
||||
import org.openhab.binding.mielecloud.internal.handler.channel.TransitionChannelState;
|
||||
import org.openhab.binding.mielecloud.internal.webservice.ActionStateFetcher;
|
||||
import org.openhab.binding.mielecloud.internal.webservice.MieleWebservice;
|
||||
import org.openhab.binding.mielecloud.internal.webservice.UnavailableMieleWebservice;
|
||||
import org.openhab.binding.mielecloud.internal.webservice.api.ActionsState;
|
||||
@@ -59,7 +58,6 @@ import org.slf4j.LoggerFactory;
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public abstract class AbstractMieleThingHandler extends BaseThingHandler {
|
||||
protected final ActionStateFetcher actionFetcher;
|
||||
protected DeviceState latestDeviceState = new DeviceState(getDeviceId(), null);
|
||||
protected TransitionState latestTransitionState = new TransitionState(null, latestDeviceState);
|
||||
protected ActionsState latestActionsState = new ActionsState(getDeviceId(), null);
|
||||
@@ -73,7 +71,6 @@ public abstract class AbstractMieleThingHandler extends BaseThingHandler {
|
||||
*/
|
||||
public AbstractMieleThingHandler(Thing thing) {
|
||||
super(thing);
|
||||
this.actionFetcher = new ActionStateFetcher(this::getWebservice, scheduler);
|
||||
}
|
||||
|
||||
private Optional<MieleBridgeHandler> getMieleBridgeHandler() {
|
||||
@@ -170,8 +167,6 @@ public abstract class AbstractMieleThingHandler extends BaseThingHandler {
|
||||
* Invoked when a device state update for the device managed by this handler is received from the Miele cloud.
|
||||
*/
|
||||
public final void onDeviceStateUpdated(DeviceState deviceState) {
|
||||
actionFetcher.onDeviceStateUpdated(deviceState);
|
||||
|
||||
latestTransitionState = new TransitionState(latestTransitionState, deviceState);
|
||||
latestDeviceState = deviceState;
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
*/
|
||||
package org.openhab.binding.mielecloud.internal.handler.channel;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
@@ -53,14 +54,14 @@ public final class ChannelTypeUtil {
|
||||
* Converts an {@link Optional} of {@link Integer} to {@link State}.
|
||||
*/
|
||||
public static State intToState(Optional<Integer> value) {
|
||||
return value.map(v -> (State) new DecimalType(v)).orElse(UnDefType.UNDEF);
|
||||
return value.map(v -> (State) new DecimalType(new BigDecimal(v))).orElse(UnDefType.UNDEF);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an {@link Optional} of {@link Long} to {@link State}.
|
||||
*/
|
||||
public static State longToState(Optional<Long> value) {
|
||||
return value.map(v -> (State) new DecimalType(v)).orElse(UnDefType.UNDEF);
|
||||
return value.map(v -> (State) new DecimalType(new BigDecimal(v))).orElse(UnDefType.UNDEF);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2022 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.mielecloud.internal.webservice;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.mielecloud.internal.webservice.api.DeviceState;
|
||||
import org.openhab.binding.mielecloud.internal.webservice.exception.AuthorizationFailedException;
|
||||
import org.openhab.binding.mielecloud.internal.webservice.exception.MieleWebserviceException;
|
||||
import org.openhab.binding.mielecloud.internal.webservice.exception.TooManyRequestsException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The {@link ActionStateFetcher} fetches the updated actions state for a device from the {@link MieleWebservice} if
|
||||
* the state of that device changed.
|
||||
*
|
||||
* Note that an instance of this class is required for each device.
|
||||
*
|
||||
* @author Roland Edelhoff - Initial contribution
|
||||
* @author Björn Lange - Make calls to webservice asynchronous
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class ActionStateFetcher {
|
||||
private Optional<DeviceState> lastDeviceState = Optional.empty();
|
||||
private final Supplier<MieleWebservice> webserviceSupplier;
|
||||
private final ScheduledExecutorService scheduler;
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(ActionStateFetcher.class);
|
||||
|
||||
/**
|
||||
* Creates a new {@link ActionStateFetcher}.
|
||||
*
|
||||
* @param webserviceSupplier Getter function for access to the {@link MieleWebservice}.
|
||||
* @param scheduler System-wide scheduler.
|
||||
*/
|
||||
public ActionStateFetcher(Supplier<MieleWebservice> webserviceSupplier, ScheduledExecutorService scheduler) {
|
||||
this.webserviceSupplier = webserviceSupplier;
|
||||
this.scheduler = scheduler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked when the state of a device was updated.
|
||||
*/
|
||||
public void onDeviceStateUpdated(DeviceState deviceState) {
|
||||
if (hasDeviceStatusChanged(deviceState)) {
|
||||
scheduler.submit(() -> fetchActions(deviceState));
|
||||
}
|
||||
lastDeviceState = Optional.of(deviceState);
|
||||
}
|
||||
|
||||
private boolean hasDeviceStatusChanged(DeviceState newDeviceState) {
|
||||
return lastDeviceState.map(DeviceState::getStateType)
|
||||
.map(rawStatus -> !newDeviceState.getStateType().equals(rawStatus)).orElse(true);
|
||||
}
|
||||
|
||||
private void fetchActions(DeviceState deviceState) {
|
||||
try {
|
||||
webserviceSupplier.get().fetchActions(deviceState.getDeviceIdentifier());
|
||||
} catch (MieleWebserviceException e) {
|
||||
logger.warn("Failed to fetch action state for device {}: {} - {}", deviceState.getDeviceIdentifier(),
|
||||
e.getConnectionError(), e.getMessage());
|
||||
} catch (AuthorizationFailedException | TooManyRequestsException e) {
|
||||
logger.warn("Failed to fetch action state for device {}: {}", deviceState.getDeviceIdentifier(),
|
||||
e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,7 @@ import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.eclipse.jetty.client.api.ContentResponse;
|
||||
import org.eclipse.jetty.client.api.Request;
|
||||
import org.openhab.binding.mielecloud.internal.webservice.api.json.Actions;
|
||||
import org.openhab.binding.mielecloud.internal.webservice.api.json.ActionsCollection;
|
||||
import org.openhab.binding.mielecloud.internal.webservice.api.json.DeviceCollection;
|
||||
import org.openhab.binding.mielecloud.internal.webservice.api.json.Light;
|
||||
import org.openhab.binding.mielecloud.internal.webservice.api.json.MieleSyntaxException;
|
||||
@@ -64,6 +65,7 @@ public final class DefaultMieleWebservice implements MieleWebservice, SseListene
|
||||
private static final String ENDPOINT_ALL_SSE_EVENTS = ENDPOINT_DEVICES + "all/events";
|
||||
|
||||
private static final String SSE_EVENT_TYPE_DEVICES = "devices";
|
||||
public static final String SSE_EVENT_TYPE_ACTIONS = "actions";
|
||||
|
||||
private static final Gson GSON = new Gson();
|
||||
|
||||
@@ -142,12 +144,37 @@ public final class DefaultMieleWebservice implements MieleWebservice, SseListene
|
||||
public void onServerSentEvent(ServerSentEvent event) {
|
||||
fireConnectionAlive();
|
||||
|
||||
if (!SSE_EVENT_TYPE_DEVICES.equals(event.getEvent())) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
deviceStateDispatcher.dispatchDeviceStateUpdates(DeviceCollection.fromJson(event.getData()));
|
||||
switch (event.getEvent()) {
|
||||
case SSE_EVENT_TYPE_ACTIONS:
|
||||
// We could use the actions payload here directly BUT as of March 2022 there is a bug in the cloud
|
||||
// that makes the payload differ from the actual values. The /actions endpoint delivers the correct
|
||||
// data. Thus, receiving an actions update via SSE is used as a trigger to fetch the actions state
|
||||
// from the /actions endpoint as a workaround. See
|
||||
// https://github.com/openhab/openhab-addons/issues/12500
|
||||
for (String deviceIdentifier : ActionsCollection.fromJson(event.getData()).getDeviceIdentifiers()) {
|
||||
try {
|
||||
fetchActions(deviceIdentifier);
|
||||
} catch (MieleWebserviceException e) {
|
||||
logger.warn("Failed to fetch action state for device {}: {} - {}", deviceIdentifier,
|
||||
e.getConnectionError(), e.getMessage());
|
||||
} catch (AuthorizationFailedException e) {
|
||||
logger.warn("Failed to fetch action state for device {}: {}", deviceIdentifier,
|
||||
e.getMessage());
|
||||
onConnectionError(ConnectionError.AUTHORIZATION_FAILED, 0);
|
||||
break;
|
||||
} catch (TooManyRequestsException e) {
|
||||
logger.warn("Failed to fetch action state for device {}: {}", deviceIdentifier,
|
||||
e.getMessage());
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case SSE_EVENT_TYPE_DEVICES:
|
||||
deviceStateDispatcher.dispatchDeviceStateUpdates(DeviceCollection.fromJson(event.getData()));
|
||||
break;
|
||||
}
|
||||
} catch (MieleSyntaxException e) {
|
||||
logger.warn("SSE payload is not valid Json: {}", event.getData());
|
||||
}
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2022 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.mielecloud.internal.webservice.api.json;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.JsonSyntaxException;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
|
||||
/**
|
||||
* Immutable POJO representing a collection of actions queried from the Miele REST API.
|
||||
*
|
||||
* @author Björn Lange - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class ActionsCollection {
|
||||
private static final java.lang.reflect.Type STRING_ACTIONS_MAP_TYPE = new TypeToken<Map<String, Actions>>() {
|
||||
}.getType();
|
||||
|
||||
private final Map<String, Actions> actions;
|
||||
|
||||
ActionsCollection(Map<String, Actions> actions) {
|
||||
this.actions = actions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link ActionsCollection} from the given Json text.
|
||||
*
|
||||
* @param json The Json text.
|
||||
* @return The created {@link ActionsCollection}.
|
||||
* @throws MieleSyntaxException if parsing the data from {@code json} fails.
|
||||
*/
|
||||
public static ActionsCollection fromJson(String json) {
|
||||
try {
|
||||
Map<String, Actions> actions = new Gson().fromJson(json, STRING_ACTIONS_MAP_TYPE);
|
||||
if (actions == null) {
|
||||
throw new MieleSyntaxException("Failed to parse Json.");
|
||||
}
|
||||
return new ActionsCollection(actions);
|
||||
} catch (JsonSyntaxException e) {
|
||||
throw new MieleSyntaxException("Failed to parse Json.", e);
|
||||
}
|
||||
}
|
||||
|
||||
public Set<String> getDeviceIdentifiers() {
|
||||
return actions.keySet();
|
||||
}
|
||||
|
||||
public Actions getActions(String identifier) {
|
||||
Actions actions = this.actions.get(identifier);
|
||||
if (actions == null) {
|
||||
throw new IllegalArgumentException("There are no actions for identifier " + identifier);
|
||||
}
|
||||
return actions;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(actions);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(@Nullable Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
if (obj == null) {
|
||||
return false;
|
||||
}
|
||||
if (getClass() != obj.getClass()) {
|
||||
return false;
|
||||
}
|
||||
ActionsCollection other = (ActionsCollection) obj;
|
||||
return Objects.equals(actions, other.actions);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ActionsCollection [actions=" + actions + "]";
|
||||
}
|
||||
}
|
||||
@@ -27,7 +27,7 @@ public final class ServerSentEvent {
|
||||
private final String event;
|
||||
private final String data;
|
||||
|
||||
ServerSentEvent(String event, String data) {
|
||||
public ServerSentEvent(String event, String data) {
|
||||
this.event = event;
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user