[homeconnect] Add power state support for the washing machines (#18634)

* Add power state support for the washing machines.(#18633)

Signed-off-by: Philipp Schneider <philipp.schneider@nixo-soft.de>
pull/18742/head
Philipp S. 2025-06-01 20:59:54 +02:00 committed by GitHub
parent 9409800d4f
commit aa60331a8e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 220 additions and 13 deletions

View File

@ -18,7 +18,7 @@ Supported devices: dishwasher, washer, washer / dryer combination, dryer, oven,
#### experimental support
| Home appliance | Thing Type ID |
| --------------- | ------------ |
| -------------------------- | ------------- |
| Dishwasher | dishwasher |
| Washer | washer |
| Washer / Dryer combination | washerdryer |
@ -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 |

View File

@ -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.
*

View File

@ -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;
}
}
}

View File

@ -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<HomeConnectApiClient> apiClient = getApiClient();
if (apiClient.isPresent()) {
// set read-only state description, if device has read-only power state option
Optional<Channel> 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);
}
}

View File

@ -53,6 +53,7 @@ public class HomeConnectWasherDryerHandler extends AbstractHomeConnectThingHandl
@Override
protected void configureChannelUpdateHandlers(Map<String, ChannelUpdateHandler> 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<String, EventHandler> 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

View File

@ -54,6 +54,7 @@ public class HomeConnectWasherHandler extends AbstractHomeConnectThingHandler {
@Override
protected void configureChannelUpdateHandlers(Map<String, ChannelUpdateHandler> 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<String, EventHandler> 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

View File

@ -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<ChannelUID, Boolean> 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<StateOption> 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();
}
}

View File

@ -103,6 +103,7 @@
<description>Home Connect connected washing machine (e.g. Bosch or Siemens).</description>
<semantic-equipment-tag>WashingMachine</semantic-equipment-tag>
<channels>
<channel id="power_state" typeId="system.power"/>
<channel id="door_state" typeId="door_state"/>
<channel id="operation_state" typeId="operation_state"/>
<channel id="remote_start_allowance_state" typeId="remote_start_allowance_state"/>
@ -129,6 +130,9 @@
<channel id="remaining_program_time_state" typeId="remaining_program_time_state"/>
<channel id="program_progress_state" typeId="program_progress_state"/>
</channels>
<properties>
<property name="thingTypeVersion">1</property>
</properties>
<representation-property>haId</representation-property>
<config-description>
<parameter name="haId" type="text" required="true">
@ -147,6 +151,7 @@
<description>Home Connect connected combined washer dryer appliance.</description>
<semantic-equipment-tag>WashingMachine</semantic-equipment-tag>
<channels>
<channel id="power_state" typeId="system.power"/>
<channel id="door_state" typeId="door_state"/>
<channel id="operation_state" typeId="operation_state"/>
<channel id="remote_start_allowance_state" typeId="remote_start_allowance_state"/>
@ -170,6 +175,9 @@
<channel id="remaining_program_time_state" typeId="remaining_program_time_state"/>
<channel id="program_progress_state" typeId="program_progress_state"/>
</channels>
<properties>
<property name="thingTypeVersion">1</property>
</properties>
<representation-property>haId</representation-property>
<config-description>
<parameter name="haId" type="text" required="true">

View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
<update:update-descriptions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:update="https://openhab.org/schemas/update-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/update-description/v1.0.0 https://openhab.org/schemas/update-description-1.0.0.xsd">
<thing-type uid="homeconnect:washer">
<instruction-set targetVersion="1">
<add-channel id="power_state">
<type>system:power</type>
</add-channel>
</instruction-set>
</thing-type>
<thing-type uid="homeconnect:washerdryer">
<instruction-set targetVersion="1">
<add-channel id="power_state">
<type>system:power</type>
</add-channel>
</instruction-set>
</thing-type>
</update:update-descriptions>