2 * Copyright (c) 2010-2024 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.io.homekit.internal;
15 import static org.openhab.io.homekit.internal.HomekitCommandType.*;
16 import static org.openhab.io.homekit.internal.HomekitDimmerMode.*;
19 import java.util.concurrent.ConcurrentHashMap;
20 import java.util.concurrent.ScheduledExecutorService;
21 import java.util.concurrent.ScheduledFuture;
22 import java.util.concurrent.TimeUnit;
24 import org.eclipse.jdt.annotation.NonNullByDefault;
25 import org.eclipse.jdt.annotation.Nullable;
26 import org.openhab.core.common.ThreadPoolManager;
27 import org.openhab.core.items.GroupItem;
28 import org.openhab.core.items.Item;
29 import org.openhab.core.library.items.ColorItem;
30 import org.openhab.core.library.items.DimmerItem;
31 import org.openhab.core.library.types.DecimalType;
32 import org.openhab.core.library.types.HSBType;
33 import org.openhab.core.library.types.OnOffType;
34 import org.openhab.core.library.types.PercentType;
35 import org.openhab.core.types.State;
36 import org.openhab.core.types.UnDefType;
37 import org.slf4j.Logger;
38 import org.slf4j.LoggerFactory;
42 * Proxy class that can collect multiple commands for the same openHAB item and merge them to one command.
43 * e.g. Hue and Saturation update for Color Item
45 * @author Eugen Freiter - Initial contribution
49 public class HomekitOHItemProxy {
50 private final Logger logger = LoggerFactory.getLogger(HomekitOHItemProxy.class);
51 private static final int DEFAULT_DELAY = 50; // in ms
52 private final Item item;
53 private final Item baseItem;
54 private final Map<HomekitCommandType, State> commandCache = new ConcurrentHashMap<>();
55 private final ScheduledExecutorService scheduler = ThreadPoolManager
56 .getScheduledPool(ThreadPoolManager.THREAD_POOL_NAME_COMMON);
57 private @NonNullByDefault({}) ScheduledFuture<?> future;
58 private HomekitDimmerMode dimmerMode = DIMMER_MODE_NORMAL;
59 // delay, how long wait for further commands. in ms.
60 private int delay = DEFAULT_DELAY;
62 public static Item getBaseItem(Item item) {
63 if (item instanceof GroupItem groupItem) {
64 final Item baseItem = groupItem.getBaseItem();
65 if (baseItem != null) {
72 public HomekitOHItemProxy(Item item) {
74 this.baseItem = getBaseItem(item);
77 public Item getItem() {
81 public void setDimmerMode(HomekitDimmerMode mode) {
85 public void setDelay(int delay) {
89 @SuppressWarnings("null")
90 private void sendCommand() {
91 if (!(baseItem instanceof DimmerItem)) {
92 // currently supports only DimmerItem and ColorItem (which extends DimmerItem)
93 logger.debug("unexpected item type {}. Only DimmerItem and ColorItem are supported.", baseItem);
96 final OnOffType on = (OnOffType) commandCache.remove(ON_COMMAND);
97 final PercentType brightness = (PercentType) commandCache.remove(BRIGHTNESS_COMMAND);
98 final DecimalType hue = (DecimalType) commandCache.remove(HUE_COMMAND);
99 final PercentType saturation = (PercentType) commandCache.remove(SATURATION_COMMAND);
100 final @Nullable OnOffType currentOnState = ((DimmerItem) item).getStateAs(OnOffType.class);
104 // - DIMMER_MODE_NONE is enabled OR
105 // - DIMMER_MODE_FILTER_BRIGHTNESS_100 is enabled OR
106 // - DIMMER_MODE_FILTER_ON_EXCEPT100 is not enabled and brightness is null or below 100
107 if ((on == OnOffType.OFF) || (dimmerMode == DIMMER_MODE_NORMAL)
108 || (dimmerMode == DIMMER_MODE_FILTER_BRIGHTNESS_100)
109 || ((dimmerMode == DIMMER_MODE_FILTER_ON_EXCEPT_BRIGHTNESS_100) && (currentOnState != OnOffType.ON)
110 && ((brightness == null) || (brightness.intValue() == 100)))) {
111 logger.trace("send OnOff command for item {} with value {}", item, on);
112 if (item instanceof GroupItem groupItem) {
115 ((DimmerItem) item).send(on);
120 // if hue or saturation present, send an HSBType state update. no filter applied for HUE & Saturation
121 if ((hue != null) || (saturation != null)) {
122 if (baseItem instanceof ColorItem colorItem) {
123 sendHSBCommand(colorItem, hue, saturation, brightness);
125 } else if ((brightness != null) && (baseItem instanceof DimmerItem)) {
127 // - DIMMER_MODE_NORMAL
128 // - DIMMER_MODE_FILTER_ON
129 // - other modes (DIMMER_MODE_FILTER_BRIGHTNESS_100 or DIMMER_MODE_FILTER_ON_EXCEPT_BRIGHTNESS_100) and
131 if ((dimmerMode == DIMMER_MODE_NORMAL) || (dimmerMode == DIMMER_MODE_FILTER_ON)
132 || (brightness.intValue() < 100) || (currentOnState == OnOffType.ON)) {
133 logger.trace("send Brightness command for item {} with value {}", item, brightness);
134 if (item instanceof ColorItem colorItem) {
135 sendHSBCommand(colorItem, hue, saturation, brightness);
136 } else if (item instanceof GroupItem groupItem) {
137 groupItem.send(brightness);
139 ((DimmerItem) item).send(brightness);
143 commandCache.clear();
146 private void sendHSBCommand(Item item, @Nullable DecimalType hue, @Nullable PercentType saturation,
147 @Nullable PercentType brightness) {
148 final HSBType currentState = item.getState() instanceof UnDefType ? HSBType.BLACK : (HSBType) item.getState();
149 // logic for ColorItem = combine hue, saturation and brightness update to one command
150 final DecimalType targetHue = hue != null ? hue : currentState.getHue();
151 final PercentType targetSaturation = saturation != null ? saturation : currentState.getSaturation();
152 final PercentType targetBrightness = brightness != null ? brightness : currentState.getBrightness();
153 final HSBType command = new HSBType(targetHue, targetSaturation, targetBrightness);
154 if (item instanceof GroupItem groupItem) {
155 groupItem.send(command);
157 ((ColorItem) item).send(command);
159 logger.trace("send HSB command for item {} with following values hue={} saturation={} brightness={}", item,
160 targetHue, targetSaturation, targetBrightness);
163 public synchronized void sendCommandProxy(HomekitCommandType commandType, State state) {
164 commandCache.put(commandType, state);
165 logger.trace("add command to command cache: item {}, command type {}, command state {}. cache state after: {}",
166 this, commandType, state, commandCache);
167 // if cache has already HUE+SATURATION or BRIGHTNESS+ON then we don't expect any further relevant command
168 if (((baseItem instanceof ColorItem) && commandCache.containsKey(HUE_COMMAND)
169 && commandCache.containsKey(SATURATION_COMMAND))
170 || (commandCache.containsKey(BRIGHTNESS_COMMAND) && commandCache.containsKey(ON_COMMAND))) {
171 if (future != null) {
172 future.cancel(false);
177 // if timer is not already set, create a new one to ensure that the command command is send even if no follow up
178 // commands are received.
179 if (future == null || future.isDone()) {
180 future = scheduler.schedule(() -> {
181 logger.trace("timer of {} ms is over, sending the command", delay);
183 }, delay, TimeUnit.MILLISECONDS);