Skip to content

Commit

Permalink
[homekit] bugfix 7491 / add support for merging several updates to on…
Browse files Browse the repository at this point in the history
…e command (openhab#7825)

* add support for merging several updates to one command
* incorporate J-N-K feedback, adapt the logic for dimmer
* add yfre to CODEOWNERS
* incorporate feedback from @cpmeister
* remove some blank lines

Signed-off-by: Eugen Freiter <[email protected]>
  • Loading branch information
yfre authored and andrewfg committed Aug 31, 2020
1 parent e0aaed1 commit 89ae161
Show file tree
Hide file tree
Showing 11 changed files with 563 additions and 194 deletions.
2 changes: 1 addition & 1 deletion CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@
/bundles/org.openhab.binding.zway/ @pathec
/bundles/org.openhab.extensionservice.marketplace/ @kaikreuzer
/bundles/org.openhab.extensionservice.marketplace.automation/ @kaikreuzer
/bundles/org.openhab.io.homekit/ @beowulfe
/bundles/org.openhab.io.homekit/ @beowulfe @yfre
/bundles/org.openhab.io.hueemulation/ @davidgraeff @digitaldan
/bundles/org.openhab.io.imperihome/ @pdegeus
/bundles/org.openhab.io.javasound/ @kaikreuzer
Expand Down
32 changes: 31 additions & 1 deletion bundles/org.openhab.io.homekit/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@ A full list of supported accessory types can be found in the table *below*.
| | | Name | String | Name of the light |
| | | Hue | Dimmer, Color | Hue |
| | | Saturation | Dimmer, Color | Saturation in % (1-100) |
| | | Brightness | Dimmer, Color | Brightness in % (1-100) |
| | | Brightness | Dimmer, Color | Brightness in % (1-100). See "Usage of dimmer modes" for configuration details. |
| | | ColorTemperature | Number | Color temperature which is represented in reciprocal megaKelvin, values - 50 to 400. should not be used in combination with hue, saturation and brightness |
| Fan | | | | Fan |
| | ActiveStatus | | Switch | accessory current working status. A value of "ON"/"OPEN" indicate that the accessory is active and is functioning without any errors. |
Expand Down Expand Up @@ -380,6 +380,36 @@ String security_current_state "Security Current State"
String security_target_state "Security Target State" (gSecuritySystem) {homekit="SecuritySystem.TargetSecuritySystemState"}
```

## Usage of dimmer modes

The way HomeKit handles dimmer devices can be different to the actual dimmers' way of working.
HomeKit home app sends following commands/update:

- On brightness change home app sends "ON" event along with target brightness, e.g. "Brightness = 50%" + "State = ON".
- On "ON" event home app sends "ON" along with brightness 100%, i.e. "Brightness = 100%" + "State = ON"
- On "OFF" event home app sends "OFF" without brightness information.

However, some dimmer devices for example do not expect brightness on "ON" event, some others do not expect "ON" upon brightness change.
In order to support different devices HomeKit binding can filter some events. Which events should be filtered is defined via dimmerMode configuration.

```xtend
Dimmer dimmer_light "Dimmer Light" {homekit="Lighting, Lighting.Brightness" [dimmerMode="<mode>"]}
```

Following modes are supported:

- "normal" - no filtering. The commands will be sent to device as received from HomeKit. This is default mode.
- "filterOn" - ON events are filtered out. only OFF events and brightness information are sent
- "filterBrightness100" - only Brightness=100% is filtered out. everything else sent unchanged. This allows custom logic for soft launch in devices.
- "filterOnExceptBrightness100" - ON events are filtered out in all cases except of brightness = 100%.

Examples:

```xtend
Dimmer dimmer_light_1 "Dimmer Light 1" {homekit="Lighting, Lighting.Brightness" [dimmerMode="filterOn"]}
Dimmer dimmer_light_2 "Dimmer Light 2" {homekit="Lighting, Lighting.Brightness" [dimmerMode="filterBrightness100"]}
Dimmer dimmer_light_3 "Dimmer Light 3" {homekit="Lighting, Lighting.Brightness" [dimmerMode="filterOnExceptBrightness100"]}
```

## Usage of valve timer

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,8 @@ private void createRootAccessories(Item item) {
if (!accessoryTypes.isEmpty() && groups.isEmpty()) { // it has homekit accessory type and is not part of bigger
// homekit group item
logger.trace("Item {} is a HomeKit accessory of types {}", item.getName(), accessoryTypes);
accessoryTypes.stream().forEach(rootAccessory -> createRootAccessory(new HomekitTaggedItem(item,
final HomekitOHItemProxy itemProxy = new HomekitOHItemProxy(item);
accessoryTypes.stream().forEach(rootAccessory -> createRootAccessory(new HomekitTaggedItem(itemProxy,
rootAccessory.getKey(), HomekitAccessoryFactory.getItemConfiguration(item, metadataRegistry))));
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/**
* Copyright (c) 2010-2020 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.io.homekit.internal;

import org.eclipse.jdt.annotation.NonNullByDefault;

/**
*
* Different command types supported by HomekitOHItemProxy.
*
* @author Eugen Freiter - Initial contribution
*/

@NonNullByDefault
public enum HomekitCommandType {
HUE_COMMAND,
SATURATION_COMMAND,
BRIGHTNESS_COMMAND,
ON_COMMAND;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/**
* Copyright (c) 2010-2020 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.io.homekit.internal;

import java.util.Arrays;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;

import org.eclipse.jdt.annotation.NonNullByDefault;

/**
* Dimmer commands are handled differently by different devices.
* Some devices expect only the brightness updates, some other expect brightness as well as "On/Off" commands.
* This enum describes different modes of dimmer handling in the context of HomeKit binding.
*
* Following modes are supported:
* DIMMER_MODE_NORMAL - no filtering. The commands will be sent to device as received from HomeKit.
* DIMMER_MODE_FILTER_ON - ON events are filtered out. only OFF and brightness information are sent
* DIMMER_MODE_FILTER_BRIGHTNESS_100 - only Brightness=100% is filtered out. everything else unchanged. This allows
* custom logic for soft launch in devices.
* DIMMER_MODE_FILTER_ON_EXCEPT_BRIGHTNESS_100 - ON events are filtered out in all cases except of Brightness = 100%.
*
* @author Eugen Freiter - Initial contribution
*/

@NonNullByDefault
public enum HomekitDimmerMode {
DIMMER_MODE_NORMAL("normal"),
DIMMER_MODE_FILTER_ON("filterOn"),
DIMMER_MODE_FILTER_BRIGHTNESS_100("filterBrightness100"),
DIMMER_MODE_FILTER_ON_EXCEPT_BRIGHTNESS_100("filterOnExceptBrightness100");

private static final Map<String, HomekitDimmerMode> TAG_MAP = Arrays.stream(HomekitDimmerMode.values())
.collect(Collectors.toMap(type -> type.tag.toUpperCase(), type -> type));

private final String tag;

private HomekitDimmerMode(String tag) {
this.tag = tag;
}

public String getTag() {
return tag;
}

public static Optional<HomekitDimmerMode> valueOfTag(String tag) {
return Optional.ofNullable(TAG_MAP.get(tag.toUpperCase()));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
/**
* Copyright (c) 2010-2020 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.io.homekit.internal;

import static org.openhab.io.homekit.internal.HomekitCommandType.*;
import static org.openhab.io.homekit.internal.HomekitDimmerMode.*;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.smarthome.core.common.ThreadPoolManager;
import org.eclipse.smarthome.core.items.Item;
import org.eclipse.smarthome.core.library.items.ColorItem;
import org.eclipse.smarthome.core.library.items.DimmerItem;
import org.eclipse.smarthome.core.library.types.DecimalType;
import org.eclipse.smarthome.core.library.types.HSBType;
import org.eclipse.smarthome.core.library.types.OnOffType;
import org.eclipse.smarthome.core.library.types.PercentType;
import org.eclipse.smarthome.core.types.State;
import org.eclipse.smarthome.core.types.UnDefType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
*
* Proxy class that can collect multiple commands for the same openHAB item and merge them to one command.
* e.g. Hue and Saturation update for Color Item
*
* @author Eugen Freiter - Initial contribution
*
*/
@NonNullByDefault
public class HomekitOHItemProxy {
private final Logger logger = LoggerFactory.getLogger(HomekitOHItemProxy.class);
private static final int DEFAULT_DELAY = 50; // in ms
private final Item item;
private final Map<HomekitCommandType, State> commandCache = new ConcurrentHashMap<>();
private final ScheduledExecutorService scheduler = ThreadPoolManager
.getScheduledPool(ThreadPoolManager.THREAD_POOL_NAME_COMMON);
private @Nullable ScheduledFuture<?> future;
private HomekitDimmerMode dimmerMode = DIMMER_MODE_NORMAL;
// delay, how long wait for further commands. in ms.
private int delay = DEFAULT_DELAY;

public HomekitOHItemProxy(final Item item) {
this.item = item;
}

public Item getItem() {
return item;
}

public void setDimmerMode(HomekitDimmerMode mode) {
dimmerMode = mode;
}

public void setDelay(int delay) {
this.delay = delay;
}

@SuppressWarnings("null")
private void sendCommand() {
if (!(item instanceof DimmerItem)) {
// currently supports only DimmerItem and ColorItem (which extends DimmerItem)
logger.debug("unexpected item type {}. Only DimmerItem and ColorItem are supported.", item);
return;
}
final OnOffType on = (OnOffType) commandCache.remove(ON_COMMAND);
final PercentType brightness = (PercentType) commandCache.remove(BRIGHTNESS_COMMAND);
final DecimalType hue = (DecimalType) commandCache.remove(HUE_COMMAND);
final PercentType saturation = (PercentType) commandCache.remove(SATURATION_COMMAND);
if (on != null) {
// always sends OFF.
// sends ON only if
// - DIMMER_MODE_NONE is enabled OR
// - DIMMER_MODE_FILTER_BRIGHTNESS_100 is enabled OR
// - DIMMER_MODE_FILTER_ON_EXCEPT100 is not enabled and brightness is null or below 100
if ((on == OnOffType.OFF) || (dimmerMode == DIMMER_MODE_NORMAL)
|| (dimmerMode == DIMMER_MODE_FILTER_BRIGHTNESS_100)
|| ((dimmerMode == DIMMER_MODE_FILTER_ON_EXCEPT_BRIGHTNESS_100)
&& ((brightness == null) || (brightness.intValue() == 100)))) {
logger.trace("send OnOff command for item {} with value {}", item, on);
((DimmerItem) item).send(on);
}
}

// if hue or saturation present, send an HSBType state update. no filter applied for HUE & Saturation
if ((hue != null) || (saturation != null)) {
if (item instanceof ColorItem) {
// logic for ColorItem = combine hue, saturation and brightness update to one command
final HSBType currentState = item.getState() instanceof UnDefType ? HSBType.BLACK
: (HSBType) item.getState();
((ColorItem) item).send(new HSBType(hue != null ? hue : currentState.getHue(),
saturation != null ? saturation : currentState.getSaturation(),
brightness != null ? brightness : currentState.getBrightness()));
logger.trace("send HSB command for item {} with following values hue={} saturation={} brightness={}",
item, hue, saturation, brightness);
}
} else if ((brightness != null) && (item instanceof DimmerItem)) {
// sends brightness:
// - DIMMER_MODE_NONE
// - DIMMER_MODE_FILTER_ON
// - other modes (DIMMER_MODE_FILTER_BRIGHTNESS_100 or DIMMER_MODE_FILTER_ON_EXCEPT_BRIGHTNESS_100) and
// <100%.
if ((dimmerMode == DIMMER_MODE_NORMAL) || (dimmerMode == DIMMER_MODE_FILTER_ON)
|| (brightness.intValue() < 100)) {
logger.trace("send Brightness command for item {} with value {}", item, brightness);
((DimmerItem) item).send(brightness);
}
}
commandCache.clear();
}

public synchronized void sendCommandProxy(final HomekitCommandType commandType, final State state) {
commandCache.put(commandType, state);
logger.trace("add command to command cache: item {}, command type {}, command state {}. cache state after: {}",
this, commandType, state, commandCache);
// if cache has already HUE+SATURATION or BRIGHTNESS+ON then we don't expect any further relevant command
if (((item instanceof ColorItem) && commandCache.containsKey(HUE_COMMAND)
&& commandCache.containsKey(SATURATION_COMMAND))
|| (commandCache.containsKey(BRIGHTNESS_COMMAND) && commandCache.containsKey(ON_COMMAND))) {
if (future != null) {
future.cancel(false);
}
sendCommand();
return;
}
// if timer is not already set, create a new one to ensure that the command command is send even if no follow up
// commands are received.
if (future == null || future.isDone()) {
future = scheduler.schedule(() -> {
logger.trace("timer of {} ms is over, sending the command", delay);
sendCommand();
}, delay, TimeUnit.MILLISECONDS);
}
}
}
Loading

0 comments on commit 89ae161

Please sign in to comment.