2 * Copyright (c) 2010-2020 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.binding.lutron.internal.grxprg;
15 import java.util.concurrent.ScheduledFuture;
16 import java.util.concurrent.TimeUnit;
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;
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.
43 * @author Tim Roberts - Initial contribution
45 public class GrafikEyeHandler extends BaseThingHandler {
48 * Logger used by this class
50 private Logger logger = LoggerFactory.getLogger(GrafikEyeHandler.class);
53 * Cached instance of the {@link GrafikEyeConfig}. Will be null if disconnected.
55 private GrafikEyeConfig config = null;
58 * The current fade for the grafik eye (only used when setting zone intensity). Will initially be set from
64 * The polling job to poll the actual state of the grafik eye
66 private ScheduledFuture<?> pollingJob;
69 * Constructs the handler from the {@link org.openhab.core.thing.Thing}
71 * @param thing a non-null {@link org.openhab.core.thing.Thing} the handler is for
73 public GrafikEyeHandler(Thing thing) {
77 throw new IllegalArgumentException("thing cannot be null");
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
91 public void handleCommand(ChannelUID channelUID, Command command) {
92 if (command instanceof RefreshType) {
93 handleRefresh(channelUID.getId());
97 if (getThing().getStatus() != ThingStatus.ONLINE) {
98 // Ignore any command if not online
102 String id = channelUID.getId();
105 logger.warn("Called with a null channel id - ignoring");
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);
114 logger.error("Received a SCENE command with a non DecimalType: {}", command);
117 } else if (id.equals(PrgConstants.CHANNEL_SCENELOCK)) {
118 if (command instanceof OnOffType) {
119 getProtocolHandler().setSceneLock(config.getControlUnit(), command == OnOffType.ON);
121 logger.error("Received a SCENELOCK command with a non OnOffType: {}", command);
124 } else if (id.equals(PrgConstants.CHANNEL_SCENESEQ)) {
125 if (command instanceof OnOffType) {
126 getProtocolHandler().setSceneSequence(config.getControlUnit(), command == OnOffType.ON);
128 logger.error("Received a SCENESEQ command with a non OnOffType: {}", command);
131 } else if (id.equals(PrgConstants.CHANNEL_ZONELOCK)) {
132 if (command instanceof OnOffType) {
133 getProtocolHandler().setZoneLock(config.getControlUnit(), command == OnOffType.ON);
135 logger.error("Received a ZONELOCK command with a non OnOffType: {}", command);
138 } else if (id.startsWith(PrgConstants.CHANNEL_ZONELOWER)) {
139 final Integer zone = getTrailingNbr(id, PrgConstants.CHANNEL_ZONELOWER);
142 getProtocolHandler().setZoneLower(config.getControlUnit(), zone);
145 } else if (id.startsWith(PrgConstants.CHANNEL_ZONERAISE)) {
146 final Integer zone = getTrailingNbr(id, PrgConstants.CHANNEL_ZONERAISE);
149 getProtocolHandler().setZoneRaise(config.getControlUnit(), zone);
152 } else if (id.equals(PrgConstants.CHANNEL_ZONEFADE)) {
153 if (command instanceof DecimalType) {
154 setFade(((DecimalType) command).intValue());
156 logger.error("Received a ZONEFADE command with a non DecimalType: {}", command);
159 } else if (id.startsWith(PrgConstants.CHANNEL_ZONEINTENSITY)) {
160 final Integer zone = getTrailingNbr(id, PrgConstants.CHANNEL_ZONEINTENSITY);
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);
173 logger.error("Received a ZONEINTENSITY command with a non DecimalType: {}", command);
177 } else if (id.startsWith(PrgConstants.CHANNEL_ZONESHADE)) {
178 final Integer zone = getTrailingNbr(id, PrgConstants.CHANNEL_ZONESHADE);
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);
191 logger.error("Received a ZONEINTENSITY command with a non DecimalType: {}", command);
196 logger.error("Unknown/Unsupported Channel id: {}", id);
201 * Method that handles the {@link RefreshType} command specifically. Calls the {@link PrgProtocolHandler} to
202 * handle the actual refresh based on the channel id.
204 * @param id a non-null, possibly empty channel id to refresh
206 private void handleRefresh(String id) {
207 if (getThing().getStatus() != ThingStatus.ONLINE) {
211 if (id.equals(PrgConstants.CHANNEL_SCENE)) {
212 getProtocolHandler().refreshScene();
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));
222 * Gets the trailing number from the channel id (which usually represents the zone number).
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
228 private Integer getTrailingNbr(String id, String channelConstant) {
230 return Integer.parseInt(id.substring(channelConstant.length()));
231 } catch (NumberFormatException e) {
232 logger.warn("Unknown channel port #: {}", id);
238 * Initializes the thing - basically calls {@link #internalInitialize()} to do the work
241 public void initialize() {
243 internalInitialize();
247 * Set's the unit to offline and attempts to reinitialize via {@link #internalInitialize()}
250 public void thingUpdated(Thing thing) {
252 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.HANDLER_CONFIGURATION_PENDING);
254 internalInitialize();
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()}
262 public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
264 if (bridgeStatusInfo.getStatus() == ThingStatus.ONLINE) {
265 internalInitialize();
267 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
272 * Initializes the grafik eye. Essentially validates the {@link GrafikEyeConfig}, updates the status to online and
273 * starts a status refresh job
275 private void internalInitialize() {
276 config = getThing().getConfiguration().as(GrafikEyeConfig.class);
278 if (config == null) {
279 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Configuration file missing");
283 final String configErr = config.validate();
284 if (configErr != null) {
285 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, configErr);
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");
296 final ThingHandler handler = bridge.getHandler();
297 if (handler.getThing().getStatus() != ThingStatus.ONLINE) {
298 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
302 updateStatus(ThingStatus.ONLINE);
303 setFade(config.getFade());
306 pollingJob = this.scheduler.scheduleWithFixedDelay(new Runnable() {
309 final ThingStatus status = getThing().getStatus();
310 if (status == ThingStatus.ONLINE && config != null) {
311 getProtocolHandler().refreshState(config.getControlUnit());
314 }, 1, config.getPolling(), TimeUnit.SECONDS);
318 * Helper method to cancel our polling if we are currently polling
320 private void cancelPolling() {
321 if (pollingJob != null) {
322 pollingJob.cancel(true);
328 * Returns the {@link PrgProtocolHandler} to use
330 * @return a non-null {@link PrgProtocolHandler} to use
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");
338 final PrgProtocolHandler handler = ((PrgBridgeHandler) bridge.getHandler()).getProtocolHandler();
339 if (handler == null) {
340 throw new NullArgumentException("No protocol handler set in the PrgBridgeHandler!");
346 * Returns the control unit for this handler
348 * @return the control unit
350 int getControlUnit() {
351 return config.getControlUnit();
355 * Helper method to determine if the given zone is a shade. Off loads it's work to
356 * {@link GrafikEyeConfig#isShadeZone(int)}
358 * @param zone a zone to check
359 * @return true if a shade zone, false otherwise
361 boolean isShade(int zone) {
362 return config == null ? false : config.isShadeZone(zone);
366 * Helper method to expose the ability to change state outside of the class
368 * @param channelId the channel id
369 * @param state the new state
371 void stateChanged(String channelId, State state) {
372 updateState(channelId, state);
376 * Helper method to set the fade level. Will store the fade and update its state.
378 * @param fade the new fade
380 private void setFade(int fade) {
381 if (fade < 0 || fade > 3600) {
382 throw new IllegalArgumentException("fade must be between 1-3600");
386 updateState(PrgConstants.CHANNEL_ZONEFADE, new DecimalType(this.fade));