]> git.basschouten.com Git - openhab-addons.git/blob
9c718542de1f13c400f28f7b2ed665fd6ae8ac42
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2020 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.binding.lutron.internal.grxprg;
14
15 import java.util.concurrent.ScheduledFuture;
16 import java.util.concurrent.TimeUnit;
17
18 import org.apache.commons.lang.NullArgumentException;
19 import org.openhab.core.library.types.DecimalType;
20 import org.openhab.core.library.types.IncreaseDecreaseType;
21 import org.openhab.core.library.types.OnOffType;
22 import org.openhab.core.library.types.PercentType;
23 import org.openhab.core.library.types.StopMoveType;
24 import org.openhab.core.library.types.UpDownType;
25 import org.openhab.core.thing.Bridge;
26 import org.openhab.core.thing.ChannelUID;
27 import org.openhab.core.thing.Thing;
28 import org.openhab.core.thing.ThingStatus;
29 import org.openhab.core.thing.ThingStatusDetail;
30 import org.openhab.core.thing.ThingStatusInfo;
31 import org.openhab.core.thing.binding.BaseThingHandler;
32 import org.openhab.core.thing.binding.ThingHandler;
33 import org.openhab.core.types.Command;
34 import org.openhab.core.types.RefreshType;
35 import org.openhab.core.types.State;
36 import org.slf4j.Logger;
37 import org.slf4j.LoggerFactory;
38
39 /**
40  * The {@link BaseThingHandler} is responsible for handling a specific grafik eye unit (identified by it's control
41  * number). This handler is responsible for handling the commands and management for a single grafik eye unit.
42  *
43  * @author Tim Roberts - Initial contribution
44  */
45 public class GrafikEyeHandler extends BaseThingHandler {
46
47     /**
48      * Logger used by this class
49      */
50     private Logger logger = LoggerFactory.getLogger(GrafikEyeHandler.class);
51
52     /**
53      * Cached instance of the {@link GrafikEyeConfig}. Will be null if disconnected.
54      */
55     private GrafikEyeConfig config = null;
56
57     /**
58      * The current fade for the grafik eye (only used when setting zone intensity). Will initially be set from
59      * configuration.
60      */
61     private int fade = 0;
62
63     /**
64      * The polling job to poll the actual state of the grafik eye
65      */
66     private ScheduledFuture<?> pollingJob;
67
68     /**
69      * Constructs the handler from the {@link org.openhab.core.thing.Thing}
70      *
71      * @param thing a non-null {@link org.openhab.core.thing.Thing} the handler is for
72      */
73     public GrafikEyeHandler(Thing thing) {
74         super(thing);
75
76         if (thing == null) {
77             throw new IllegalArgumentException("thing cannot be null");
78         }
79     }
80
81     /**
82      * {@inheritDoc}
83      *
84      * Handles commands to specific channels. This implementation will offload much of its work to the
85      * {@link PrgProtocolHandler}. Basically we validate the type of command for the channel then call the
86      * {@link PrgProtocolHandler} to handle the actual protocol. Special use case is the {@link RefreshType}
87      * where we call {{@link #handleRefresh(String)} to handle a refresh of the specific channel (which in turn calls
88      * {@link PrgProtocolHandler} to handle the actual refresh
89      */
90     @Override
91     public void handleCommand(ChannelUID channelUID, Command command) {
92         if (command instanceof RefreshType) {
93             handleRefresh(channelUID.getId());
94             return;
95         }
96
97         if (getThing().getStatus() != ThingStatus.ONLINE) {
98             // Ignore any command if not online
99             return;
100         }
101
102         String id = channelUID.getId();
103
104         if (id == null) {
105             logger.warn("Called with a null channel id - ignoring");
106             return;
107         }
108
109         if (id.equals(PrgConstants.CHANNEL_SCENE)) {
110             if (command instanceof DecimalType) {
111                 final int scene = ((DecimalType) command).intValue();
112                 getProtocolHandler().selectScene(config.getControlUnit(), scene);
113             } else {
114                 logger.error("Received a SCENE command with a non DecimalType: {}", command);
115             }
116
117         } else if (id.equals(PrgConstants.CHANNEL_SCENELOCK)) {
118             if (command instanceof OnOffType) {
119                 getProtocolHandler().setSceneLock(config.getControlUnit(), command == OnOffType.ON);
120             } else {
121                 logger.error("Received a SCENELOCK command with a non OnOffType: {}", command);
122             }
123
124         } else if (id.equals(PrgConstants.CHANNEL_SCENESEQ)) {
125             if (command instanceof OnOffType) {
126                 getProtocolHandler().setSceneSequence(config.getControlUnit(), command == OnOffType.ON);
127             } else {
128                 logger.error("Received a SCENESEQ command with a non OnOffType: {}", command);
129             }
130
131         } else if (id.equals(PrgConstants.CHANNEL_ZONELOCK)) {
132             if (command instanceof OnOffType) {
133                 getProtocolHandler().setZoneLock(config.getControlUnit(), command == OnOffType.ON);
134             } else {
135                 logger.error("Received a ZONELOCK command with a non OnOffType: {}", command);
136             }
137
138         } else if (id.startsWith(PrgConstants.CHANNEL_ZONELOWER)) {
139             final Integer zone = getTrailingNbr(id, PrgConstants.CHANNEL_ZONELOWER);
140
141             if (zone != null) {
142                 getProtocolHandler().setZoneLower(config.getControlUnit(), zone);
143             }
144
145         } else if (id.startsWith(PrgConstants.CHANNEL_ZONERAISE)) {
146             final Integer zone = getTrailingNbr(id, PrgConstants.CHANNEL_ZONERAISE);
147
148             if (zone != null) {
149                 getProtocolHandler().setZoneRaise(config.getControlUnit(), zone);
150             }
151
152         } else if (id.equals(PrgConstants.CHANNEL_ZONEFADE)) {
153             if (command instanceof DecimalType) {
154                 setFade(((DecimalType) command).intValue());
155             } else {
156                 logger.error("Received a ZONEFADE command with a non DecimalType: {}", command);
157             }
158
159         } else if (id.startsWith(PrgConstants.CHANNEL_ZONEINTENSITY)) {
160             final Integer zone = getTrailingNbr(id, PrgConstants.CHANNEL_ZONEINTENSITY);
161
162             if (zone != null) {
163                 if (command instanceof PercentType) {
164                     final int intensity = ((PercentType) command).intValue();
165                     getProtocolHandler().setZoneIntensity(config.getControlUnit(), zone, fade, intensity);
166                 } else if (command instanceof OnOffType) {
167                     getProtocolHandler().setZoneIntensity(config.getControlUnit(), zone, fade,
168                             command == OnOffType.ON ? 100 : 0);
169                 } else if (command instanceof IncreaseDecreaseType) {
170                     getProtocolHandler().setZoneIntensity(config.getControlUnit(), zone, fade,
171                             command == IncreaseDecreaseType.INCREASE);
172                 } else {
173                     logger.error("Received a ZONEINTENSITY command with a non DecimalType: {}", command);
174                 }
175             }
176
177         } else if (id.startsWith(PrgConstants.CHANNEL_ZONESHADE)) {
178             final Integer zone = getTrailingNbr(id, PrgConstants.CHANNEL_ZONESHADE);
179
180             if (zone != null) {
181                 if (command instanceof PercentType) {
182                     logger.info("PercentType is not suppored by QED shades");
183                 } else if (command == StopMoveType.MOVE) {
184                     logger.info("StopMoveType.Move is not suppored by QED shades");
185                 } else if (command == StopMoveType.STOP) {
186                     getProtocolHandler().setZoneIntensity(config.getControlUnit(), zone, fade, 0);
187                 } else if (command instanceof UpDownType) {
188                     getProtocolHandler().setZoneIntensity(config.getControlUnit(), zone, fade,
189                             command == UpDownType.UP ? 1 : 2);
190                 } else {
191                     logger.error("Received a ZONEINTENSITY command with a non DecimalType: {}", command);
192                 }
193             }
194
195         } else {
196             logger.error("Unknown/Unsupported Channel id: {}", id);
197         }
198     }
199
200     /**
201      * Method that handles the {@link RefreshType} command specifically. Calls the {@link PrgProtocolHandler} to
202      * handle the actual refresh based on the channel id.
203      *
204      * @param id a non-null, possibly empty channel id to refresh
205      */
206     private void handleRefresh(String id) {
207         if (getThing().getStatus() != ThingStatus.ONLINE) {
208             return;
209         }
210
211         if (id.equals(PrgConstants.CHANNEL_SCENE)) {
212             getProtocolHandler().refreshScene();
213
214         } else if (id.equals(PrgConstants.CHANNEL_ZONEINTENSITY)) {
215             getProtocolHandler().refreshZoneIntensity(config.getControlUnit());
216         } else if (id.equals(PrgConstants.CHANNEL_ZONEFADE)) {
217             updateState(PrgConstants.CHANNEL_ZONEFADE, new DecimalType(fade));
218         }
219     }
220
221     /**
222      * Gets the trailing number from the channel id (which usually represents the zone number).
223      *
224      * @param id a non-null, possibly empty channel id
225      * @param channelConstant a non-null, non-empty channel id constant to use in the parse.
226      * @return the trailing number or null if a parse exception occurs
227      */
228     private Integer getTrailingNbr(String id, String channelConstant) {
229         try {
230             return Integer.parseInt(id.substring(channelConstant.length()));
231         } catch (NumberFormatException e) {
232             logger.warn("Unknown channel port #: {}", id);
233             return null;
234         }
235     }
236
237     /**
238      * Initializes the thing - basically calls {@link #internalInitialize()} to do the work
239      */
240     @Override
241     public void initialize() {
242         cancelPolling();
243         internalInitialize();
244     }
245
246     /**
247      * Set's the unit to offline and attempts to reinitialize via {@link #internalInitialize()}
248      */
249     @Override
250     public void thingUpdated(Thing thing) {
251         cancelPolling();
252         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.HANDLER_CONFIGURATION_PENDING);
253         this.thing = thing;
254         internalInitialize();
255     }
256
257     /**
258      * If the bridge goes offline, cancels the polling and goes offline. If the bridge goes online, will attempt to
259      * re-initialize via {@link #internalInitialize()}
260      */
261     @Override
262     public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
263         cancelPolling();
264         if (bridgeStatusInfo.getStatus() == ThingStatus.ONLINE) {
265             internalInitialize();
266         } else {
267             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
268         }
269     }
270
271     /**
272      * Initializes the grafik eye. Essentially validates the {@link GrafikEyeConfig}, updates the status to online and
273      * starts a status refresh job
274      */
275     private void internalInitialize() {
276         config = getThing().getConfiguration().as(GrafikEyeConfig.class);
277
278         if (config == null) {
279             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Configuration file missing");
280             return;
281         }
282
283         final String configErr = config.validate();
284         if (configErr != null) {
285             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, configErr);
286             return;
287         }
288
289         final Bridge bridge = getBridge();
290         if (bridge == null || !(bridge.getHandler() instanceof PrgBridgeHandler)) {
291             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
292                     "GrafikEye must have a parent PRG Bridge");
293             return;
294         }
295
296         final ThingHandler handler = bridge.getHandler();
297         if (handler.getThing().getStatus() != ThingStatus.ONLINE) {
298             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
299             return;
300         }
301
302         updateStatus(ThingStatus.ONLINE);
303         setFade(config.getFade());
304
305         cancelPolling();
306         pollingJob = this.scheduler.scheduleWithFixedDelay(new Runnable() {
307             @Override
308             public void run() {
309                 final ThingStatus status = getThing().getStatus();
310                 if (status == ThingStatus.ONLINE && config != null) {
311                     getProtocolHandler().refreshState(config.getControlUnit());
312                 }
313             }
314         }, 1, config.getPolling(), TimeUnit.SECONDS);
315     }
316
317     /**
318      * Helper method to cancel our polling if we are currently polling
319      */
320     private void cancelPolling() {
321         if (pollingJob != null) {
322             pollingJob.cancel(true);
323             pollingJob = null;
324         }
325     }
326
327     /**
328      * Returns the {@link PrgProtocolHandler} to use
329      *
330      * @return a non-null {@link PrgProtocolHandler} to use
331      */
332     private PrgProtocolHandler getProtocolHandler() {
333         final Bridge bridge = getBridge();
334         if (bridge == null || !(bridge.getHandler() instanceof PrgBridgeHandler)) {
335             throw new NullArgumentException("Cannot have a Grafix Eye thing outside of the PRG bridge");
336         }
337
338         final PrgProtocolHandler handler = ((PrgBridgeHandler) bridge.getHandler()).getProtocolHandler();
339         if (handler == null) {
340             throw new NullArgumentException("No protocol handler set in the PrgBridgeHandler!");
341         }
342         return handler;
343     }
344
345     /**
346      * Returns the control unit for this handler
347      *
348      * @return the control unit
349      */
350     int getControlUnit() {
351         return config.getControlUnit();
352     }
353
354     /**
355      * Helper method to determine if the given zone is a shade. Off loads it's work to
356      * {@link GrafikEyeConfig#isShadeZone(int)}
357      *
358      * @param zone a zone to check
359      * @return true if a shade zone, false otherwise
360      */
361     boolean isShade(int zone) {
362         return config == null ? false : config.isShadeZone(zone);
363     }
364
365     /**
366      * Helper method to expose the ability to change state outside of the class
367      *
368      * @param channelId the channel id
369      * @param state the new state
370      */
371     void stateChanged(String channelId, State state) {
372         updateState(channelId, state);
373     }
374
375     /**
376      * Helper method to set the fade level. Will store the fade and update its state.
377      *
378      * @param fade the new fade
379      */
380     private void setFade(int fade) {
381         if (fade < 0 || fade > 3600) {
382             throw new IllegalArgumentException("fade must be between 1-3600");
383         }
384
385         this.fade = fade;
386         updateState(PrgConstants.CHANNEL_ZONEFADE, new DecimalType(this.fade));
387     }
388 }