]> git.basschouten.com Git - openhab-addons.git/blob
f160eb079d21ab9568bbf5ae427a9c72366cf0fa
[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 sceneCommand) {
110                 final int scene = sceneCommand.intValue();
111                 getProtocolHandler().selectScene(config.getControlUnit(), scene);
112             } else {
113                 logger.error("Received a SCENE command with a non DecimalType: {}", command);
114             }
115         } else if (id.equals(PrgConstants.CHANNEL_SCENELOCK)) {
116             if (command instanceof OnOffType) {
117                 getProtocolHandler().setSceneLock(config.getControlUnit(), command == OnOffType.ON);
118             } else {
119                 logger.error("Received a SCENELOCK command with a non OnOffType: {}", command);
120             }
121         } else if (id.equals(PrgConstants.CHANNEL_SCENESEQ)) {
122             if (command instanceof OnOffType) {
123                 getProtocolHandler().setSceneSequence(config.getControlUnit(), command == OnOffType.ON);
124             } else {
125                 logger.error("Received a SCENESEQ command with a non OnOffType: {}", command);
126             }
127         } else if (id.equals(PrgConstants.CHANNEL_ZONELOCK)) {
128             if (command instanceof OnOffType) {
129                 getProtocolHandler().setZoneLock(config.getControlUnit(), command == OnOffType.ON);
130             } else {
131                 logger.error("Received a ZONELOCK command with a non OnOffType: {}", command);
132             }
133         } else if (id.startsWith(PrgConstants.CHANNEL_ZONELOWER)) {
134             final Integer zone = getTrailingNbr(id, PrgConstants.CHANNEL_ZONELOWER);
135
136             if (zone != null) {
137                 getProtocolHandler().setZoneLower(config.getControlUnit(), zone);
138             }
139         } else if (id.startsWith(PrgConstants.CHANNEL_ZONERAISE)) {
140             final Integer zone = getTrailingNbr(id, PrgConstants.CHANNEL_ZONERAISE);
141
142             if (zone != null) {
143                 getProtocolHandler().setZoneRaise(config.getControlUnit(), zone);
144             }
145         } else if (id.equals(PrgConstants.CHANNEL_ZONEFADE)) {
146             if (command instanceof DecimalType zoneFade) {
147                 setFade(zoneFade.intValue());
148             } else {
149                 logger.error("Received a ZONEFADE command with a non DecimalType: {}", command);
150             }
151         } else if (id.startsWith(PrgConstants.CHANNEL_ZONEINTENSITY)) {
152             final Integer zone = getTrailingNbr(id, PrgConstants.CHANNEL_ZONEINTENSITY);
153
154             if (zone != null) {
155                 if (command instanceof PercentType intensityPercent) {
156                     final int intensity = intensityPercent.intValue();
157                     getProtocolHandler().setZoneIntensity(config.getControlUnit(), zone, fade, intensity);
158                 } else if (command instanceof OnOffType) {
159                     getProtocolHandler().setZoneIntensity(config.getControlUnit(), zone, fade,
160                             command == OnOffType.ON ? 100 : 0);
161                 } else if (command instanceof IncreaseDecreaseType) {
162                     getProtocolHandler().setZoneIntensity(config.getControlUnit(), zone, fade,
163                             command == IncreaseDecreaseType.INCREASE);
164                 } else {
165                     logger.error("Received a ZONEINTENSITY command with a non DecimalType: {}", command);
166                 }
167             }
168         } else if (id.startsWith(PrgConstants.CHANNEL_ZONESHADE)) {
169             final Integer zone = getTrailingNbr(id, PrgConstants.CHANNEL_ZONESHADE);
170
171             if (zone != null) {
172                 if (command instanceof PercentType) {
173                     logger.info("PercentType is not suppored by QED shades");
174                 } else if (command == StopMoveType.MOVE) {
175                     logger.info("StopMoveType.Move is not suppored by QED shades");
176                 } else if (command == StopMoveType.STOP) {
177                     getProtocolHandler().setZoneIntensity(config.getControlUnit(), zone, fade, 0);
178                 } else if (command instanceof UpDownType) {
179                     getProtocolHandler().setZoneIntensity(config.getControlUnit(), zone, fade,
180                             command == UpDownType.UP ? 1 : 2);
181                 } else {
182                     logger.error("Received a ZONEINTENSITY command with a non DecimalType: {}", command);
183                 }
184             }
185         } else {
186             logger.error("Unknown/Unsupported Channel id: {}", id);
187         }
188     }
189
190     /**
191      * Method that handles the {@link RefreshType} command specifically. Calls the {@link PrgProtocolHandler} to
192      * handle the actual refresh based on the channel id.
193      *
194      * @param id a non-null, possibly empty channel id to refresh
195      */
196     private void handleRefresh(String id) {
197         if (getThing().getStatus() != ThingStatus.ONLINE) {
198             return;
199         }
200
201         if (id.equals(PrgConstants.CHANNEL_SCENE)) {
202             getProtocolHandler().refreshScene();
203         } else if (id.equals(PrgConstants.CHANNEL_ZONEINTENSITY)) {
204             getProtocolHandler().refreshZoneIntensity(config.getControlUnit());
205         } else if (id.equals(PrgConstants.CHANNEL_ZONEFADE)) {
206             updateState(PrgConstants.CHANNEL_ZONEFADE, new DecimalType(fade));
207         }
208     }
209
210     /**
211      * Gets the trailing number from the channel id (which usually represents the zone number).
212      *
213      * @param id a non-null, possibly empty channel id
214      * @param channelConstant a non-null, non-empty channel id constant to use in the parse.
215      * @return the trailing number or null if a parse exception occurs
216      */
217     private Integer getTrailingNbr(String id, String channelConstant) {
218         try {
219             return Integer.parseInt(id.substring(channelConstant.length()));
220         } catch (NumberFormatException e) {
221             logger.warn("Unknown channel port #: {}", id);
222             return null;
223         }
224     }
225
226     /**
227      * Initializes the thing - basically calls {@link #internalInitialize()} to do the work
228      */
229     @Override
230     public void initialize() {
231         cancelPolling();
232         internalInitialize();
233     }
234
235     /**
236      * Set's the unit to offline and attempts to reinitialize via {@link #internalInitialize()}
237      */
238     @Override
239     public void thingUpdated(Thing thing) {
240         cancelPolling();
241         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.HANDLER_CONFIGURATION_PENDING);
242         this.thing = thing;
243         internalInitialize();
244     }
245
246     /**
247      * If the bridge goes offline, cancels the polling and goes offline. If the bridge goes online, will attempt to
248      * re-initialize via {@link #internalInitialize()}
249      */
250     @Override
251     public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
252         cancelPolling();
253         if (bridgeStatusInfo.getStatus() == ThingStatus.ONLINE) {
254             internalInitialize();
255         } else {
256             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
257         }
258     }
259
260     /**
261      * Initializes the grafik eye. Essentially validates the {@link GrafikEyeConfig}, updates the status to online and
262      * starts a status refresh job
263      */
264     private void internalInitialize() {
265         config = getThing().getConfiguration().as(GrafikEyeConfig.class);
266
267         if (config == null) {
268             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Configuration file missing");
269             return;
270         }
271
272         final String configErr = config.validate();
273         if (configErr != null) {
274             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, configErr);
275             return;
276         }
277
278         final Bridge bridge = getBridge();
279         if (bridge == null || !(bridge.getHandler() instanceof PrgBridgeHandler)) {
280             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
281                     "GrafikEye must have a parent PRG Bridge");
282             return;
283         }
284
285         final ThingHandler handler = bridge.getHandler();
286         if (handler.getThing().getStatus() != ThingStatus.ONLINE) {
287             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
288             return;
289         }
290
291         updateStatus(ThingStatus.ONLINE);
292         setFade(config.getFade());
293
294         cancelPolling();
295         pollingJob = this.scheduler.scheduleWithFixedDelay(new Runnable() {
296             @Override
297             public void run() {
298                 final ThingStatus status = getThing().getStatus();
299                 if (status == ThingStatus.ONLINE && config != null) {
300                     getProtocolHandler().refreshState(config.getControlUnit());
301                 }
302             }
303         }, 1, config.getPolling(), TimeUnit.SECONDS);
304     }
305
306     /**
307      * Helper method to cancel our polling if we are currently polling
308      */
309     private void cancelPolling() {
310         if (pollingJob != null) {
311             pollingJob.cancel(true);
312             pollingJob = null;
313         }
314     }
315
316     /**
317      * Returns the {@link PrgProtocolHandler} to use
318      *
319      * @return a non-null {@link PrgProtocolHandler} to use
320      */
321     private PrgProtocolHandler getProtocolHandler() {
322         final Bridge bridge = getBridge();
323         if (bridge == null || !(bridge.getHandler() instanceof PrgBridgeHandler)) {
324             throw new IllegalArgumentException("Cannot have a Grafix Eye thing outside of the PRG bridge");
325         }
326
327         final PrgProtocolHandler handler = ((PrgBridgeHandler) bridge.getHandler()).getProtocolHandler();
328         if (handler == null) {
329             throw new IllegalArgumentException("No protocol handler set in the PrgBridgeHandler!");
330         }
331         return handler;
332     }
333
334     /**
335      * Returns the control unit for this handler
336      *
337      * @return the control unit
338      */
339     int getControlUnit() {
340         return config.getControlUnit();
341     }
342
343     /**
344      * Helper method to determine if the given zone is a shade. Off loads it's work to
345      * {@link GrafikEyeConfig#isShadeZone(int)}
346      *
347      * @param zone a zone to check
348      * @return true if a shade zone, false otherwise
349      */
350     boolean isShade(int zone) {
351         return config == null ? false : config.isShadeZone(zone);
352     }
353
354     /**
355      * Helper method to expose the ability to change state outside of the class
356      *
357      * @param channelId the channel id
358      * @param state the new state
359      */
360     void stateChanged(String channelId, State state) {
361         updateState(channelId, state);
362     }
363
364     /**
365      * Helper method to set the fade level. Will store the fade and update its state.
366      *
367      * @param fade the new fade
368      */
369     private void setFade(int fade) {
370         if (fade < 0 || fade > 3600) {
371             throw new IllegalArgumentException("fade must be between 1-3600");
372         }
373
374         this.fade = fade;
375         updateState(PrgConstants.CHANNEL_ZONEFADE, new DecimalType(this.fade));
376     }
377 }