From aa60331a8e154e49ccd7f11b90c0e6d1e7b44a74 Mon Sep 17 00:00:00 2001 From: "Philipp S." <16479847+nixoso@users.noreply.github.com> Date: Sun, 1 Jun 2025 20:59:54 +0200 Subject: [PATCH] [homeconnect] Add power state support for the washing machines (#18634) * Add power state support for the washing machines.(#18633) Signed-off-by: Philipp Schneider --- .../org.openhab.binding.homeconnect/README.md | 24 +++---- .../internal/client/HomeConnectApiClient.java | 50 ++++++++++++++ .../client/model/PowerStateAccess.java | 41 ++++++++++++ .../AbstractHomeConnectThingHandler.java | 12 +++- .../HomeConnectWasherDryerHandler.java | 5 ++ .../handler/HomeConnectWasherHandler.java | 5 ++ ...onnectDynamicStateDescriptionProvider.java | 66 +++++++++++++++++++ .../resources/OH-INF/thing/thing-types.xml | 8 +++ .../main/resources/OH-INF/update/update.xml | 22 +++++++ 9 files changed, 220 insertions(+), 13 deletions(-) create mode 100644 bundles/org.openhab.binding.homeconnect/src/main/java/org/openhab/binding/homeconnect/internal/client/model/PowerStateAccess.java create mode 100644 bundles/org.openhab.binding.homeconnect/src/main/resources/OH-INF/update/update.xml diff --git a/bundles/org.openhab.binding.homeconnect/README.md b/bundles/org.openhab.binding.homeconnect/README.md index 6645929e206..942c6f2fbc4 100644 --- a/bundles/org.openhab.binding.homeconnect/README.md +++ b/bundles/org.openhab.binding.homeconnect/README.md @@ -17,17 +17,17 @@ Supported devices: dishwasher, washer, washer / dryer combination, dryer, oven, #### experimental support -| Home appliance | Thing Type ID | -| --------------- | ------------ | -| Dishwasher | dishwasher | -| Washer | washer | -| Washer / Dryer combination | washerdryer | -| Dryer | dryer | -| Oven | oven | -| Hood | hood | -| Cooktop | hob | -| Refrigerator Freezer | fridgefreezer | -| Coffee Machine | coffeemaker | +| Home appliance | Thing Type ID | +| -------------------------- | ------------- | +| Dishwasher | dishwasher | +| Washer | washer | +| Washer / Dryer combination | washerdryer | +| Dryer | dryer | +| Oven | oven | +| Hood | hood | +| Cooktop | hob | +| Refrigerator Freezer | fridgefreezer | +| Coffee Machine | coffeemaker | > **INFO:** Currently the Home Connect API does not support all appliance programs. Please check if your desired program is available (e.g. ). @@ -39,7 +39,7 @@ After the bridge has been added and authorized, devices are discovered automatic | Channel Type ID | Item Type | Read only | Description | Available on thing | | --------------- | --------- | --------- | ----------- | ------------------ | -| power_state | Switch | false | This setting describes the current power state of the home appliance. | dishwasher, oven, coffeemaker, hood, hob | +| power_state | Switch | false | This setting describes the current power state of the home appliance. | dishwasher, oven, coffeemaker, hood, hob, washer, washerdryer | | door_state | Contact | true | This status describes the door state of a home appliance. A status change is either triggered by the user operating the home appliance locally (i.e. opening/closing door) or automatically by the home appliance (i.e. locking the door). | dishwasher, washer, washerdryer, dryer, oven, fridgefreezer | | operation_state | String | true | This status describes the operation state of the home appliance. | dishwasher, washer, washerdryer, dryer, oven, hood, hob, coffeemaker | | remote_start_allowance_state | Switch | true | This status indicates whether the remote program start is enabled. This can happen due to a programmatic change (only disabling), or manually by the user changing the flag locally on the home appliance, or automatically after a certain duration - usually in 24 hours. | dishwasher, washer, washerdryer, dryer, oven, hood, coffeemaker | diff --git a/bundles/org.openhab.binding.homeconnect/src/main/java/org/openhab/binding/homeconnect/internal/client/HomeConnectApiClient.java b/bundles/org.openhab.binding.homeconnect/src/main/java/org/openhab/binding/homeconnect/internal/client/HomeConnectApiClient.java index 6bdb07e9a8f..b2f9fd847f0 100644 --- a/bundles/org.openhab.binding.homeconnect/src/main/java/org/openhab/binding/homeconnect/internal/client/HomeConnectApiClient.java +++ b/bundles/org.openhab.binding.homeconnect/src/main/java/org/openhab/binding/homeconnect/internal/client/HomeConnectApiClient.java @@ -46,6 +46,7 @@ import org.openhab.binding.homeconnect.internal.client.model.HomeAppliance; import org.openhab.binding.homeconnect.internal.client.model.HomeConnectRequest; import org.openhab.binding.homeconnect.internal.client.model.HomeConnectResponse; import org.openhab.binding.homeconnect.internal.client.model.Option; +import org.openhab.binding.homeconnect.internal.client.model.PowerStateAccess; import org.openhab.binding.homeconnect.internal.client.model.Program; import org.openhab.binding.homeconnect.internal.configuration.ApiBridgeConfiguration; import org.openhab.core.auth.client.oauth2.OAuthClientService; @@ -53,6 +54,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.gson.JsonArray; +import com.google.gson.JsonElement; import com.google.gson.JsonObject; /** @@ -324,6 +326,54 @@ public class HomeConnectApiClient { return getSetting(haId, SETTING_POWER_STATE); } + /** + * Provides information on whether the power state of device can be set or only read. + * + * @param haId home appliance id + * @return {@link PowerStateAccess} + * @throws CommunicationException API communication exception + * @throws AuthorizationException oAuth authorization exception + * @throws ApplianceOfflineException appliance is not connected to the cloud + */ + public PowerStateAccess getPowerStateAccess(String haId) + throws CommunicationException, AuthorizationException, ApplianceOfflineException { + + String powerStateSettings = getRaw(haId, BASE_PATH + haId + "/settings/" + SETTING_POWER_STATE); + + /*** + * Example response: + * { + * "data": { + * "key": "BSH.Common.Setting.PowerState", + * "value": "BSH.Common.EnumType.PowerState.Off", + * "type": "BSH.Common.EnumType.PowerState", + * "constraints": { + * "allowedvalues": [ + * "BSH.Common.EnumType.PowerState.Off", + * "BSH.Common.EnumType.PowerState.On" + * ], + * "default": "BSH.Common.EnumType.PowerState.On", + * "access": "readWrite" + * } + * } + * } + */ + + if (powerStateSettings != null) { + JsonObject responseObject = parseString(powerStateSettings).getAsJsonObject(); + JsonObject data = responseObject.getAsJsonObject("data"); + JsonElement jsonConstraints = data.get("constraints"); + if (jsonConstraints.isJsonObject()) { + JsonElement jsonAccess = jsonConstraints.getAsJsonObject().get("access"); + if (jsonAccess.isJsonPrimitive()) { + return PowerStateAccess.fromString(jsonAccess.getAsString()); + } + } + } + + return PowerStateAccess.READ_ONLY; + } + /** * Set power state of device. * diff --git a/bundles/org.openhab.binding.homeconnect/src/main/java/org/openhab/binding/homeconnect/internal/client/model/PowerStateAccess.java b/bundles/org.openhab.binding.homeconnect/src/main/java/org/openhab/binding/homeconnect/internal/client/model/PowerStateAccess.java new file mode 100644 index 00000000000..77694c716f2 --- /dev/null +++ b/bundles/org.openhab.binding.homeconnect/src/main/java/org/openhab/binding/homeconnect/internal/client/model/PowerStateAccess.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2010-2025 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.homeconnect.internal.client.model; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link PowerStateAccess} enum defines the access types for the power state of the device. + * + * @author Philipp Schneider - Initial contribution + * + */ +@NonNullByDefault +public enum PowerStateAccess { + + READ_ONLY, + + READ_WRITE; + + public static PowerStateAccess fromString(String access) { + switch (access.toLowerCase()) { + case "read": + return READ_ONLY; + case "readwrite": + return READ_WRITE; + default: + // Default to READ_ONLY if the access type is not recognized + return READ_ONLY; + } + } +} diff --git a/bundles/org.openhab.binding.homeconnect/src/main/java/org/openhab/binding/homeconnect/internal/handler/AbstractHomeConnectThingHandler.java b/bundles/org.openhab.binding.homeconnect/src/main/java/org/openhab/binding/homeconnect/internal/handler/AbstractHomeConnectThingHandler.java index 1b49b9087d0..a9f65916a9f 100644 --- a/bundles/org.openhab.binding.homeconnect/src/main/java/org/openhab/binding/homeconnect/internal/handler/AbstractHomeConnectThingHandler.java +++ b/bundles/org.openhab.binding.homeconnect/src/main/java/org/openhab/binding/homeconnect/internal/handler/AbstractHomeConnectThingHandler.java @@ -52,6 +52,7 @@ import org.openhab.binding.homeconnect.internal.client.model.Data; import org.openhab.binding.homeconnect.internal.client.model.Event; import org.openhab.binding.homeconnect.internal.client.model.HomeAppliance; import org.openhab.binding.homeconnect.internal.client.model.Option; +import org.openhab.binding.homeconnect.internal.client.model.PowerStateAccess; import org.openhab.binding.homeconnect.internal.client.model.Program; import org.openhab.binding.homeconnect.internal.handler.cache.ExpiringStateMap; import org.openhab.binding.homeconnect.internal.type.HomeConnectDynamicStateDescriptionProvider; @@ -1025,6 +1026,14 @@ public abstract class AbstractHomeConnectThingHandler extends BaseThingHandler i return (channelUID, cache) -> updateState(channelUID, cache.putIfAbsentAndGet(channelUID, () -> { Optional apiClient = getApiClient(); if (apiClient.isPresent()) { + + // set read-only state description, if device has read-only power state option + Optional powerStateChannel = getThingChannel(CHANNEL_POWER_STATE); + if (powerStateChannel.isPresent()) { + dynamicStateDescriptionProvider.withReadOnly(powerStateChannel.get().getUID(), + apiClient.get().getPowerStateAccess(getThingHaId()) == PowerStateAccess.READ_ONLY); + } + Data data = apiClient.get().getPowerState(getThingHaId()); if (data.getValue() != null) { return OnOffType.from(STATE_POWER_ON.equals(data.getValue())); @@ -1320,7 +1329,8 @@ public abstract class AbstractHomeConnectThingHandler extends BaseThingHandler i protected void handlePowerCommand(final ChannelUID channelUID, final Command command, final HomeConnectApiClient apiClient, String stateNotOn) throws CommunicationException, AuthorizationException, ApplianceOfflineException { - if (command instanceof OnOffType && CHANNEL_POWER_STATE.equals(channelUID.getId())) { + if (command instanceof OnOffType && CHANNEL_POWER_STATE.equals(channelUID.getId()) + && apiClient.getPowerStateAccess(getThingHaId()) == PowerStateAccess.READ_WRITE) { apiClient.setPowerState(getThingHaId(), OnOffType.ON.equals(command) ? STATE_POWER_ON : stateNotOn); } } diff --git a/bundles/org.openhab.binding.homeconnect/src/main/java/org/openhab/binding/homeconnect/internal/handler/HomeConnectWasherDryerHandler.java b/bundles/org.openhab.binding.homeconnect/src/main/java/org/openhab/binding/homeconnect/internal/handler/HomeConnectWasherDryerHandler.java index 226c02cacf8..7e46709aa98 100644 --- a/bundles/org.openhab.binding.homeconnect/src/main/java/org/openhab/binding/homeconnect/internal/handler/HomeConnectWasherDryerHandler.java +++ b/bundles/org.openhab.binding.homeconnect/src/main/java/org/openhab/binding/homeconnect/internal/handler/HomeConnectWasherDryerHandler.java @@ -53,6 +53,7 @@ public class HomeConnectWasherDryerHandler extends AbstractHomeConnectThingHandl @Override protected void configureChannelUpdateHandlers(Map handlers) { // register default update handlers + handlers.put(CHANNEL_POWER_STATE, defaultPowerStateChannelUpdateHandler()); handlers.put(CHANNEL_DOOR_STATE, defaultDoorStateChannelUpdateHandler()); handlers.put(CHANNEL_OPERATION_STATE, defaultOperationStateChannelUpdateHandler()); handlers.put(CHANNEL_REMOTE_CONTROL_ACTIVE_STATE, defaultRemoteControlActiveStateChannelUpdateHandler()); @@ -93,6 +94,7 @@ public class HomeConnectWasherDryerHandler extends AbstractHomeConnectThingHandl @Override protected void configureEventHandlers(Map handlers) { // register default event handlers + handlers.put(EVENT_POWER_STATE, defaultPowerStateEventHandler()); handlers.put(EVENT_DOOR_STATE, defaultDoorStateEventHandler()); handlers.put(EVENT_REMOTE_CONTROL_ACTIVE, updateRemoteControlActiveAndProgramOptionsStateEventHandler()); handlers.put(EVENT_REMOTE_CONTROL_START_ALLOWED, @@ -137,6 +139,9 @@ public class HomeConnectWasherDryerHandler extends AbstractHomeConnectThingHandl final HomeConnectApiClient apiClient) throws CommunicationException, AuthorizationException, ApplianceOfflineException { super.handleCommand(channelUID, command, apiClient); + + handlePowerCommand(channelUID, command, apiClient, STATE_POWER_OFF); + String operationState = getOperationState(); // only handle these commands if operation state allows it diff --git a/bundles/org.openhab.binding.homeconnect/src/main/java/org/openhab/binding/homeconnect/internal/handler/HomeConnectWasherHandler.java b/bundles/org.openhab.binding.homeconnect/src/main/java/org/openhab/binding/homeconnect/internal/handler/HomeConnectWasherHandler.java index 06fc272a766..b929f890f3f 100644 --- a/bundles/org.openhab.binding.homeconnect/src/main/java/org/openhab/binding/homeconnect/internal/handler/HomeConnectWasherHandler.java +++ b/bundles/org.openhab.binding.homeconnect/src/main/java/org/openhab/binding/homeconnect/internal/handler/HomeConnectWasherHandler.java @@ -54,6 +54,7 @@ public class HomeConnectWasherHandler extends AbstractHomeConnectThingHandler { @Override protected void configureChannelUpdateHandlers(Map handlers) { // register default update handlers + handlers.put(CHANNEL_POWER_STATE, defaultPowerStateChannelUpdateHandler()); handlers.put(CHANNEL_DOOR_STATE, defaultDoorStateChannelUpdateHandler()); handlers.put(CHANNEL_OPERATION_STATE, defaultOperationStateChannelUpdateHandler()); handlers.put(CHANNEL_REMOTE_CONTROL_ACTIVE_STATE, defaultRemoteControlActiveStateChannelUpdateHandler()); @@ -99,6 +100,7 @@ public class HomeConnectWasherHandler extends AbstractHomeConnectThingHandler { @Override protected void configureEventHandlers(Map handlers) { // register default event handlers + handlers.put(EVENT_POWER_STATE, defaultPowerStateEventHandler()); handlers.put(EVENT_DOOR_STATE, defaultDoorStateEventHandler()); handlers.put(EVENT_REMOTE_CONTROL_ACTIVE, updateRemoteControlActiveAndProgramOptionsStateEventHandler()); handlers.put(EVENT_REMOTE_CONTROL_START_ALLOWED, @@ -186,6 +188,9 @@ public class HomeConnectWasherHandler extends AbstractHomeConnectThingHandler { final HomeConnectApiClient apiClient) throws CommunicationException, AuthorizationException, ApplianceOfflineException { super.handleCommand(channelUID, command, apiClient); + + handlePowerCommand(channelUID, command, apiClient, STATE_POWER_OFF); + String operationState = getOperationState(); // only handle these commands if operation state allows it diff --git a/bundles/org.openhab.binding.homeconnect/src/main/java/org/openhab/binding/homeconnect/internal/type/HomeConnectDynamicStateDescriptionProvider.java b/bundles/org.openhab.binding.homeconnect/src/main/java/org/openhab/binding/homeconnect/internal/type/HomeConnectDynamicStateDescriptionProvider.java index d60111a2180..2c723dd4c0e 100644 --- a/bundles/org.openhab.binding.homeconnect/src/main/java/org/openhab/binding/homeconnect/internal/type/HomeConnectDynamicStateDescriptionProvider.java +++ b/bundles/org.openhab.binding.homeconnect/src/main/java/org/openhab/binding/homeconnect/internal/type/HomeConnectDynamicStateDescriptionProvider.java @@ -12,12 +12,26 @@ */ package org.openhab.binding.homeconnect.internal.type; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; import org.openhab.core.events.EventPublisher; +import org.openhab.core.thing.Channel; +import org.openhab.core.thing.ChannelUID; import org.openhab.core.thing.binding.BaseDynamicStateDescriptionProvider; +import org.openhab.core.thing.events.ThingEventFactory; import org.openhab.core.thing.i18n.ChannelTypeI18nLocalizationService; import org.openhab.core.thing.link.ItemChannelLinkRegistry; +import org.openhab.core.thing.type.ChannelType; import org.openhab.core.thing.type.DynamicStateDescriptionProvider; +import org.openhab.core.types.StateDescription; +import org.openhab.core.types.StateDescriptionFragmentBuilder; +import org.openhab.core.types.StateOption; import org.osgi.service.component.annotations.Activate; import org.osgi.service.component.annotations.Component; import org.osgi.service.component.annotations.Reference; @@ -31,6 +45,8 @@ import org.osgi.service.component.annotations.Reference; @NonNullByDefault public class HomeConnectDynamicStateDescriptionProvider extends BaseDynamicStateDescriptionProvider { + protected final Map channelReadOnlyMap = new ConcurrentHashMap<>(); + @Activate public HomeConnectDynamicStateDescriptionProvider(final @Reference EventPublisher eventPublisher, // final @Reference ItemChannelLinkRegistry itemChannelLinkRegistry, // @@ -39,4 +55,54 @@ public class HomeConnectDynamicStateDescriptionProvider extends BaseDynamicState this.itemChannelLinkRegistry = itemChannelLinkRegistry; this.channelTypeI18nLocalizationService = channelTypeI18nLocalizationService; } + + /** + * For a given {@link ChannelUID}, set a readyOnly flag that should be used for the channel, instead of the one + * defined statically in the {@link ChannelType}. + * + * @param channelUID the {@link ChannelUID} of the channel + * @param readOnly readOnly flag + */ + public void withReadOnly(ChannelUID channelUID, boolean readOnly) { + Boolean oldReadOnly = channelReadOnlyMap.get(channelUID); + if (oldReadOnly == null || oldReadOnly != readOnly) { + channelReadOnlyMap.put(channelUID, readOnly); + postEvent(ThingEventFactory.createChannelDescriptionChangedEvent(channelUID, + itemChannelLinkRegistry != null ? itemChannelLinkRegistry.getLinkedItemNames(channelUID) : Set.of(), + StateDescriptionFragmentBuilder.create().withReadOnly(readOnly).build(), null)); + } + } + + @Override + public @Nullable StateDescription getStateDescription(Channel channel, @Nullable StateDescription original, + @Nullable Locale locale) { + // can be overridden by subclasses + ChannelUID channelUID = channel.getUID(); + String pattern = channelPatternMap.get(channelUID); + List options = channelOptionsMap.get(channelUID); + Boolean readOnly = channelReadOnlyMap.get(channelUID); + if (pattern == null && options == null && readOnly == null) { + return null; + } + + StateDescriptionFragmentBuilder builder = (original == null) ? StateDescriptionFragmentBuilder.create() + : StateDescriptionFragmentBuilder.create(original); + + if (pattern != null) { + String localizedPattern = localizeStatePattern(pattern, channel, locale); + if (localizedPattern != null) { + builder.withPattern(localizedPattern); + } + } + + if (options != null) { + builder.withOptions(localizedStateOptions(options, channel, locale)); + } + + if (readOnly != null) { + builder.withReadOnly(readOnly); + } + + return builder.build().toStateDescription(); + } } diff --git a/bundles/org.openhab.binding.homeconnect/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.homeconnect/src/main/resources/OH-INF/thing/thing-types.xml index 2801429ba80..85ed96f49ed 100644 --- a/bundles/org.openhab.binding.homeconnect/src/main/resources/OH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.homeconnect/src/main/resources/OH-INF/thing/thing-types.xml @@ -103,6 +103,7 @@ Home Connect connected washing machine (e.g. Bosch or Siemens). WashingMachine + @@ -129,6 +130,9 @@ + + 1 + haId @@ -147,6 +151,7 @@ Home Connect connected combined washer dryer appliance. WashingMachine + @@ -170,6 +175,9 @@ + + 1 + haId diff --git a/bundles/org.openhab.binding.homeconnect/src/main/resources/OH-INF/update/update.xml b/bundles/org.openhab.binding.homeconnect/src/main/resources/OH-INF/update/update.xml new file mode 100644 index 00000000000..ee037777836 --- /dev/null +++ b/bundles/org.openhab.binding.homeconnect/src/main/resources/OH-INF/update/update.xml @@ -0,0 +1,22 @@ + + + + + + + system:power + + + + + + + + system:power + + + + +