]> git.basschouten.com Git - openhab-addons.git/blob
8112b1feabcff416c4805b82236847e225b96bed
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.io.homekit.internal;
14
15 import static org.openhab.io.homekit.internal.HomekitCommandType.*;
16 import static org.openhab.io.homekit.internal.HomekitDimmerMode.*;
17
18 import java.util.Map;
19 import java.util.concurrent.ConcurrentHashMap;
20 import java.util.concurrent.ScheduledExecutorService;
21 import java.util.concurrent.ScheduledFuture;
22 import java.util.concurrent.TimeUnit;
23
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;
39
40 /**
41  *
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
44  * 
45  * @author Eugen Freiter - Initial contribution
46  *
47  */
48 @NonNullByDefault
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;
61
62     public static Item getBaseItem(Item item) {
63         if (item instanceof GroupItem) {
64             final GroupItem groupItem = (GroupItem) item;
65             final Item baseItem = groupItem.getBaseItem();
66             if (baseItem != null) {
67                 return baseItem;
68             }
69         }
70         return item;
71     }
72
73     public HomekitOHItemProxy(Item item) {
74         this.item = item;
75         this.baseItem = getBaseItem(item);
76     }
77
78     public Item getItem() {
79         return item;
80     }
81
82     public void setDimmerMode(HomekitDimmerMode mode) {
83         dimmerMode = mode;
84     }
85
86     public void setDelay(int delay) {
87         this.delay = delay;
88     }
89
90     @SuppressWarnings("null")
91     private void sendCommand() {
92         if (!(baseItem instanceof DimmerItem)) {
93             // currently supports only DimmerItem and ColorItem (which extends DimmerItem)
94             logger.debug("unexpected item type {}. Only DimmerItem and ColorItem are supported.", baseItem);
95             return;
96         }
97         final OnOffType on = (OnOffType) commandCache.remove(ON_COMMAND);
98         final PercentType brightness = (PercentType) commandCache.remove(BRIGHTNESS_COMMAND);
99         final DecimalType hue = (DecimalType) commandCache.remove(HUE_COMMAND);
100         final PercentType saturation = (PercentType) commandCache.remove(SATURATION_COMMAND);
101         final @Nullable OnOffType currentOnState = ((DimmerItem) item).getStateAs(OnOffType.class);
102         if (on != null) {
103             // always sends OFF.
104             // sends ON only if
105             // - DIMMER_MODE_NONE is enabled OR
106             // - DIMMER_MODE_FILTER_BRIGHTNESS_100 is enabled OR
107             // - DIMMER_MODE_FILTER_ON_EXCEPT100 is not enabled and brightness is null or below 100
108             if ((on == OnOffType.OFF) || (dimmerMode == DIMMER_MODE_NORMAL)
109                     || (dimmerMode == DIMMER_MODE_FILTER_BRIGHTNESS_100)
110                     || ((dimmerMode == DIMMER_MODE_FILTER_ON_EXCEPT_BRIGHTNESS_100) && (currentOnState != OnOffType.ON)
111                             && ((brightness == null) || (brightness.intValue() == 100)))) {
112                 logger.trace("send OnOff command for item {} with value {}", item, on);
113                 if (item instanceof GroupItem) {
114                     ((GroupItem) item).send(on);
115                 } else {
116                     ((DimmerItem) item).send(on);
117                 }
118             }
119         }
120
121         // if hue or saturation present, send an HSBType state update. no filter applied for HUE & Saturation
122         if ((hue != null) || (saturation != null)) {
123             if (baseItem instanceof ColorItem) {
124                 sendHSBCommand((ColorItem) item, hue, saturation, brightness);
125             }
126         } else if ((brightness != null) && (baseItem instanceof DimmerItem)) {
127             // sends brightness:
128             // - DIMMER_MODE_NORMAL
129             // - DIMMER_MODE_FILTER_ON
130             // - other modes (DIMMER_MODE_FILTER_BRIGHTNESS_100 or DIMMER_MODE_FILTER_ON_EXCEPT_BRIGHTNESS_100) and
131             // <100%.
132             if ((dimmerMode == DIMMER_MODE_NORMAL) || (dimmerMode == DIMMER_MODE_FILTER_ON)
133                     || (brightness.intValue() < 100) || (currentOnState == OnOffType.ON)) {
134                 logger.trace("send Brightness command for item {} with value {}", item, brightness);
135                 if (item instanceof ColorItem) {
136                     sendHSBCommand((ColorItem) item, hue, saturation, brightness);
137                 } else if (item instanceof GroupItem) {
138                     ((GroupItem) item).send(brightness);
139                 } else {
140                     ((DimmerItem) item).send(brightness);
141                 }
142             }
143         }
144         commandCache.clear();
145     }
146
147     private void sendHSBCommand(Item item, @Nullable DecimalType hue, @Nullable PercentType saturation,
148             @Nullable PercentType brightness) {
149         final HSBType currentState = item.getState() instanceof UnDefType ? HSBType.BLACK : (HSBType) item.getState();
150         // logic for ColorItem = combine hue, saturation and brightness update to one command
151         final DecimalType targetHue = hue != null ? hue : currentState.getHue();
152         final PercentType targetSaturation = saturation != null ? saturation : currentState.getSaturation();
153         final PercentType targetBrightness = brightness != null ? brightness : currentState.getBrightness();
154         final HSBType command = new HSBType(targetHue, targetSaturation, targetBrightness);
155         if (item instanceof GroupItem) {
156             ((GroupItem) item).send(command);
157         } else {
158             ((ColorItem) item).send(command);
159         }
160         logger.trace("send HSB command for item {} with following values hue={} saturation={} brightness={}", item,
161                 targetHue, targetSaturation, targetBrightness);
162     }
163
164     public synchronized void sendCommandProxy(HomekitCommandType commandType, State state) {
165         commandCache.put(commandType, state);
166         logger.trace("add command to command cache: item {}, command type {}, command state {}. cache state after: {}",
167                 this, commandType, state, commandCache);
168         // if cache has already HUE+SATURATION or BRIGHTNESS+ON then we don't expect any further relevant command
169         if (((baseItem instanceof ColorItem) && commandCache.containsKey(HUE_COMMAND)
170                 && commandCache.containsKey(SATURATION_COMMAND))
171                 || (commandCache.containsKey(BRIGHTNESS_COMMAND) && commandCache.containsKey(ON_COMMAND))) {
172             if (future != null) {
173                 future.cancel(false);
174             }
175             sendCommand();
176             return;
177         }
178         // if timer is not already set, create a new one to ensure that the command command is send even if no follow up
179         // commands are received.
180         if (future == null || future.isDone()) {
181             future = scheduler.schedule(() -> {
182                 logger.trace("timer of {} ms is over, sending the command", delay);
183                 sendCommand();
184             }, delay, TimeUnit.MILLISECONDS);
185         }
186     }
187 }