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