Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[homekit] bugfix 7491 / add support for merging several updates to one command #7825

Merged
merged 8 commits into from
Jun 7, 2020
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,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
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,156 @@
/**
* 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.concurrent.ConcurrentHashMap;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;

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
*
*/

public class HomekitOHItemProxy {
yfre marked this conversation as resolved.
Show resolved Hide resolved
private static final Logger logger = LoggerFactory.getLogger(HomekitOHItemProxy.class);
yfre marked this conversation as resolved.
Show resolved Hide resolved
public static final String HUE_COMMAND = "hue";
public static final String SATURATION_COMMAND = "saturation";
public static final String BRIGHTNESS_COMMAND = "brightness";
public static final String ON_COMMAND = "on";

public static final int DIMMER_MODE_NONE = 0;
yfre marked this conversation as resolved.
Show resolved Hide resolved
public static final int DIMMER_MODE_FILTER_BRIGHTNESS_100 = 1;
public static final int DIMMER_MODE_FILTER_ON = 2;
public static final int DIMMER_MODE_FILTER_ON_EXCEPT_BRIGHTNESS_100 = 3;

private static final int DEFAULT_DELAY = 50;
yfre marked this conversation as resolved.
Show resolved Hide resolved

private final ScheduledExecutorService scheduler = ThreadPoolManager
.getScheduledPool(ThreadPoolManager.THREAD_POOL_NAME_COMMON);
private ScheduledFuture<?> future;

private final Item item;
yfre marked this conversation as resolved.
Show resolved Hide resolved

private int dimmerMode = DIMMER_MODE_NONE;

// delay, how long wait for further commands. in ms.
private int delay = DEFAULT_DELAY;
private ConcurrentHashMap<String, State> commandCache = new ConcurrentHashMap<>();
yfre marked this conversation as resolved.
Show resolved Hide resolved

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

public Item getItem() {
return item;
}

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

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

private void sendCommand() {
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_NONE)
|| (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);
yfre marked this conversation as resolved.
Show resolved Hide resolved
}
}

// 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_NONE) || (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 String 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 ((commandCache.containsKey(HUE_COMMAND) && commandCache.containsKey(SATURATION_COMMAND))
|| (commandCache.containsKey(BRIGHTNESS_COMMAND) && commandCache.containsKey(ON_COMMAND))) {
yfre marked this conversation as resolved.
Show resolved Hide resolved
if (future != null)
yfre marked this conversation as resolved.
Show resolved Hide resolved
future.cancel(true);
yfre marked this conversation as resolved.
Show resolved Hide resolved
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 is over, sending the command");
sendCommand();
}, delay, TimeUnit.MILLISECONDS);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,15 @@

import static org.openhab.io.homekit.internal.HomekitAccessoryType.DUMMY;

import java.math.BigDecimal;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.smarthome.core.items.GroupItem;
import org.eclipse.smarthome.core.items.Item;
import org.eclipse.smarthome.core.types.State;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

Expand All @@ -37,49 +39,60 @@ public class HomekitTaggedItem {
public final static String MIN_VALUE = "minValue";
public final static String MAX_VALUE = "maxValue";
public final static String STEP = "step";
public final static String DIMMER_MODE = "dimmerMode";
public final static String DELAY = "commandDelay";

private static final Map<Integer, String> CREATED_ACCESSORY_IDS = new ConcurrentHashMap<>();
/**
* The type of HomekitDevice we've decided this was. If the item is question is the member of a group which is a
* HomekitDevice, then this is null.
*/
private final Item item;

// proxy item used to group commands for complex item types like Color or Dimmer
private final HomekitOHItemProxy proxyItem;

// type of HomeKit accessory/service, e.g. TemperatureSensor
private final HomekitAccessoryType homekitAccessoryType;

// type of HomeKit characteristic, e.g. CurrentTemperature
private @Nullable HomekitCharacteristicType homekitCharacteristicType;

// configuration attached to the openHAB Item, e.g. minValue, maxValue, valveType
private @Nullable Map<String, Object> configuration;

// link to the groupItem if item is part of a group
private @Nullable GroupItem parentGroupItem;

// HomeKit accessory id (aid) which is generated from item name
private final int id;

public HomekitTaggedItem(Item item, HomekitAccessoryType homekitAccessoryType,
public HomekitTaggedItem(HomekitOHItemProxy item, HomekitAccessoryType homekitAccessoryType,
@Nullable Map<String, Object> configuration) {
this.item = item;
this.proxyItem = item;
this.parentGroupItem = null;
this.configuration = configuration;
this.homekitAccessoryType = homekitAccessoryType;
this.homekitCharacteristicType = HomekitCharacteristicType.EMPTY;
if (homekitAccessoryType != DUMMY) {
this.id = calculateId(item);
this.id = calculateId(item.getItem());
} else {
this.id = 0;
}
parseConfiguration();
}

public HomekitTaggedItem(Item item, HomekitAccessoryType homekitAccessoryType,
public HomekitTaggedItem(HomekitOHItemProxy item, HomekitAccessoryType homekitAccessoryType,
@Nullable HomekitCharacteristicType homekitCharacteristicType,
@Nullable Map<String, Object> configuration) {
this(item, homekitAccessoryType, configuration);
this.homekitCharacteristicType = homekitCharacteristicType;
}

public HomekitTaggedItem(Item item, HomekitAccessoryType homekitAccessoryType,
public HomekitTaggedItem(HomekitOHItemProxy item, HomekitAccessoryType homekitAccessoryType,
@Nullable HomekitCharacteristicType homekitCharacteristicType, @Nullable GroupItem parentGroup,
@Nullable Map<String, Object> configuration) {
this(item, homekitAccessoryType, homekitCharacteristicType, configuration);
this.parentGroupItem = parentGroup;
}

public boolean isGroup() {
return (isAccessory() && (this.item instanceof GroupItem));
return (isAccessory() && (proxyItem.getItem() instanceof GroupItem));
}

public HomekitAccessoryType getAccessoryType() {
Expand Down Expand Up @@ -112,16 +125,42 @@ public boolean isCharacteristic() {
return homekitCharacteristicType != null && homekitCharacteristicType != HomekitCharacteristicType.EMPTY;
}

/**
* return openHAB item responsible for the HomeKit item
*
* @return openHAB item
*/
public Item getItem() {
return item;
return proxyItem.getItem();
}

/**
* return proxy item which is used to group commands.
*
* @return proxy item
*/
public HomekitOHItemProxy getProxyItem() {
return proxyItem;
}

/**
* send openHAB item command via proxy item, which allows to group commands.
* e.g. sendCommandProxy(hue), sendCommandProxy(brightness) would lead to one openHAB command that updates hue and
* brightness at once
*
* @param commandType type of the command, e.g. OHItemProxy.HUE_COMMAND
* @param command command/state
*/
public void sendCommandProxy(String commandType, State command) {
proxyItem.sendCommandProxy(commandType, command);
}

public int getId() {
return id;
}

public String getName() {
return item.getName();
return proxyItem.getItem().getName();
}

/**
Expand All @@ -141,6 +180,28 @@ public boolean isMemberOfAccessoryGroup() {
return parentGroupItem != null;
}

private void parseConfiguration() {
if (configuration != null) {
Object dimmerModeConfig = configuration.get(DIMMER_MODE);
if (dimmerModeConfig instanceof String) {
yfre marked this conversation as resolved.
Show resolved Hide resolved
final String dimmerModeConfigStr = (String) dimmerModeConfig;
if (dimmerModeConfigStr.equalsIgnoreCase("none")) {
proxyItem.setDimmerMode(HomekitOHItemProxy.DIMMER_MODE_NONE);
} else if (dimmerModeConfigStr.equalsIgnoreCase("filterOn")) {
proxyItem.setDimmerMode(HomekitOHItemProxy.DIMMER_MODE_FILTER_ON);
} else if (dimmerModeConfigStr.equalsIgnoreCase("filterBrightness100")) {
proxyItem.setDimmerMode(HomekitOHItemProxy.DIMMER_MODE_FILTER_BRIGHTNESS_100);
} else if (dimmerModeConfigStr.equalsIgnoreCase("filterOnExceptBrightness100")) {
proxyItem.setDimmerMode(HomekitOHItemProxy.DIMMER_MODE_FILTER_ON_EXCEPT_BRIGHTNESS_100);
}
}
Object delayConfig = configuration.get(DELAY);
if (delayConfig instanceof BigDecimal) {
proxyItem.setDelay(((BigDecimal) delayConfig).intValue());
}
yfre marked this conversation as resolved.
Show resolved Hide resolved
}
}

private int calculateId(Item item) {
// magic number 629 is the legacy from apache HashCodeBuilder (17*37)
int id = 629 + item.getName().hashCode();
Expand All @@ -164,7 +225,7 @@ private int calculateId(Item item) {
}

public String toString() {
return "Item:" + item + " HomeKit type:" + homekitAccessoryType + " HomeKit characteristic:"
return "Item:" + proxyItem + " HomeKit type:" + homekitAccessoryType + " HomeKit characteristic:"
+ homekitCharacteristicType;
}
}
Loading