2 * Copyright (c) 2010-2023 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.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;
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.
42 * @author Tim Roberts - Initial contribution
44 public class GrafikEyeHandler extends BaseThingHandler {
47 * Logger used by this class
49 private Logger logger = LoggerFactory.getLogger(GrafikEyeHandler.class);
52 * Cached instance of the {@link GrafikEyeConfig}. Will be null if disconnected.
54 private GrafikEyeConfig config = null;
57 * The current fade for the grafik eye (only used when setting zone intensity). Will initially be set from
63 * The polling job to poll the actual state of the grafik eye
65 private ScheduledFuture<?> pollingJob;
68 * Constructs the handler from the {@link org.openhab.core.thing.Thing}
70 * @param thing a non-null {@link org.openhab.core.thing.Thing} the handler is for
72 public GrafikEyeHandler(Thing thing) {
76 throw new IllegalArgumentException("thing cannot be null");
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
90 public void handleCommand(ChannelUID channelUID, Command command) {
91 if (command instanceof RefreshType) {
92 handleRefresh(channelUID.getId());
96 if (getThing().getStatus() != ThingStatus.ONLINE) {
97 // Ignore any command if not online
101 String id = channelUID.getId();
104 logger.warn("Called with a null channel id - ignoring");
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);
113 logger.error("Received a SCENE command with a non DecimalType: {}", command);
116 } else if (id.equals(PrgConstants.CHANNEL_SCENELOCK)) {
117 if (command instanceof OnOffType) {
118 getProtocolHandler().setSceneLock(config.getControlUnit(), command == OnOffType.ON);
120 logger.error("Received a SCENELOCK command with a non OnOffType: {}", command);
123 } else if (id.equals(PrgConstants.CHANNEL_SCENESEQ)) {
124 if (command instanceof OnOffType) {
125 getProtocolHandler().setSceneSequence(config.getControlUnit(), command == OnOffType.ON);
127 logger.error("Received a SCENESEQ command with a non OnOffType: {}", command);
130 } else if (id.equals(PrgConstants.CHANNEL_ZONELOCK)) {
131 if (command instanceof OnOffType) {
132 getProtocolHandler().setZoneLock(config.getControlUnit(), command == OnOffType.ON);
134 logger.error("Received a ZONELOCK command with a non OnOffType: {}", command);
137 } else if (id.startsWith(PrgConstants.CHANNEL_ZONELOWER)) {
138 final Integer zone = getTrailingNbr(id, PrgConstants.CHANNEL_ZONELOWER);
141 getProtocolHandler().setZoneLower(config.getControlUnit(), zone);
144 } else if (id.startsWith(PrgConstants.CHANNEL_ZONERAISE)) {
145 final Integer zone = getTrailingNbr(id, PrgConstants.CHANNEL_ZONERAISE);
148 getProtocolHandler().setZoneRaise(config.getControlUnit(), zone);
151 } else if (id.equals(PrgConstants.CHANNEL_ZONEFADE)) {
152 if (command instanceof DecimalType) {
153 setFade(((DecimalType) command).intValue());
155 logger.error("Received a ZONEFADE command with a non DecimalType: {}", command);
158 } else if (id.startsWith(PrgConstants.CHANNEL_ZONEINTENSITY)) {
159 final Integer zone = getTrailingNbr(id, PrgConstants.CHANNEL_ZONEINTENSITY);
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);
172 logger.error("Received a ZONEINTENSITY command with a non DecimalType: {}", command);
176 } else if (id.startsWith(PrgConstants.CHANNEL_ZONESHADE)) {
177 final Integer zone = getTrailingNbr(id, PrgConstants.CHANNEL_ZONESHADE);
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);
190 logger.error("Received a ZONEINTENSITY command with a non DecimalType: {}", command);
195 logger.error("Unknown/Unsupported Channel id: {}", id);
200 * Method that handles the {@link RefreshType} command specifically. Calls the {@link PrgProtocolHandler} to
201 * handle the actual refresh based on the channel id.
203 * @param id a non-null, possibly empty channel id to refresh
205 private void handleRefresh(String id) {
206 if (getThing().getStatus() != ThingStatus.ONLINE) {
210 if (id.equals(PrgConstants.CHANNEL_SCENE)) {
211 getProtocolHandler().refreshScene();
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));
221 * Gets the trailing number from the channel id (which usually represents the zone number).
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
227 private Integer getTrailingNbr(String id, String channelConstant) {
229 return Integer.parseInt(id.substring(channelConstant.length()));
230 } catch (NumberFormatException e) {
231 logger.warn("Unknown channel port #: {}", id);
237 * Initializes the thing - basically calls {@link #internalInitialize()} to do the work
240 public void initialize() {
242 internalInitialize();
246 * Set's the unit to offline and attempts to reinitialize via {@link #internalInitialize()}
249 public void thingUpdated(Thing thing) {
251 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.HANDLER_CONFIGURATION_PENDING);
253 internalInitialize();
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()}
261 public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
263 if (bridgeStatusInfo.getStatus() == ThingStatus.ONLINE) {
264 internalInitialize();
266 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
271 * Initializes the grafik eye. Essentially validates the {@link GrafikEyeConfig}, updates the status to online and
272 * starts a status refresh job
274 private void internalInitialize() {
275 config = getThing().getConfiguration().as(GrafikEyeConfig.class);
277 if (config == null) {
278 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Configuration file missing");
282 final String configErr = config.validate();
283 if (configErr != null) {
284 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, configErr);
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");
295 final ThingHandler handler = bridge.getHandler();
296 if (handler.getThing().getStatus() != ThingStatus.ONLINE) {
297 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
301 updateStatus(ThingStatus.ONLINE);
302 setFade(config.getFade());
305 pollingJob = this.scheduler.scheduleWithFixedDelay(new Runnable() {
308 final ThingStatus status = getThing().getStatus();
309 if (status == ThingStatus.ONLINE && config != null) {
310 getProtocolHandler().refreshState(config.getControlUnit());
313 }, 1, config.getPolling(), TimeUnit.SECONDS);
317 * Helper method to cancel our polling if we are currently polling
319 private void cancelPolling() {
320 if (pollingJob != null) {
321 pollingJob.cancel(true);
327 * Returns the {@link PrgProtocolHandler} to use
329 * @return a non-null {@link PrgProtocolHandler} to use
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");
337 final PrgProtocolHandler handler = ((PrgBridgeHandler) bridge.getHandler()).getProtocolHandler();
338 if (handler == null) {
339 throw new IllegalArgumentException("No protocol handler set in the PrgBridgeHandler!");
345 * Returns the control unit for this handler
347 * @return the control unit
349 int getControlUnit() {
350 return config.getControlUnit();
354 * Helper method to determine if the given zone is a shade. Off loads it's work to
355 * {@link GrafikEyeConfig#isShadeZone(int)}
357 * @param zone a zone to check
358 * @return true if a shade zone, false otherwise
360 boolean isShade(int zone) {
361 return config == null ? false : config.isShadeZone(zone);
365 * Helper method to expose the ability to change state outside of the class
367 * @param channelId the channel id
368 * @param state the new state
370 void stateChanged(String channelId, State state) {
371 updateState(channelId, state);
375 * Helper method to set the fade level. Will store the fade and update its state.
377 * @param fade the new fade
379 private void setFade(int fade) {
380 if (fade < 0 || fade > 3600) {
381 throw new IllegalArgumentException("fade must be between 1-3600");
385 updateState(PrgConstants.CHANNEL_ZONEFADE, new DecimalType(this.fade));