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 sceneCommand) {
110 final int scene = sceneCommand.intValue();
111 getProtocolHandler().selectScene(config.getControlUnit(), scene);
113 logger.error("Received a SCENE command with a non DecimalType: {}", command);
115 } else if (id.equals(PrgConstants.CHANNEL_SCENELOCK)) {
116 if (command instanceof OnOffType) {
117 getProtocolHandler().setSceneLock(config.getControlUnit(), command == OnOffType.ON);
119 logger.error("Received a SCENELOCK command with a non OnOffType: {}", command);
121 } else if (id.equals(PrgConstants.CHANNEL_SCENESEQ)) {
122 if (command instanceof OnOffType) {
123 getProtocolHandler().setSceneSequence(config.getControlUnit(), command == OnOffType.ON);
125 logger.error("Received a SCENESEQ command with a non OnOffType: {}", command);
127 } else if (id.equals(PrgConstants.CHANNEL_ZONELOCK)) {
128 if (command instanceof OnOffType) {
129 getProtocolHandler().setZoneLock(config.getControlUnit(), command == OnOffType.ON);
131 logger.error("Received a ZONELOCK command with a non OnOffType: {}", command);
133 } else if (id.startsWith(PrgConstants.CHANNEL_ZONELOWER)) {
134 final Integer zone = getTrailingNbr(id, PrgConstants.CHANNEL_ZONELOWER);
137 getProtocolHandler().setZoneLower(config.getControlUnit(), zone);
139 } else if (id.startsWith(PrgConstants.CHANNEL_ZONERAISE)) {
140 final Integer zone = getTrailingNbr(id, PrgConstants.CHANNEL_ZONERAISE);
143 getProtocolHandler().setZoneRaise(config.getControlUnit(), zone);
145 } else if (id.equals(PrgConstants.CHANNEL_ZONEFADE)) {
146 if (command instanceof DecimalType zoneFade) {
147 setFade(zoneFade.intValue());
149 logger.error("Received a ZONEFADE command with a non DecimalType: {}", command);
151 } else if (id.startsWith(PrgConstants.CHANNEL_ZONEINTENSITY)) {
152 final Integer zone = getTrailingNbr(id, PrgConstants.CHANNEL_ZONEINTENSITY);
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);
165 logger.error("Received a ZONEINTENSITY command with a non DecimalType: {}", command);
168 } else if (id.startsWith(PrgConstants.CHANNEL_ZONESHADE)) {
169 final Integer zone = getTrailingNbr(id, PrgConstants.CHANNEL_ZONESHADE);
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);
182 logger.error("Received a ZONEINTENSITY command with a non DecimalType: {}", command);
186 logger.error("Unknown/Unsupported Channel id: {}", id);
191 * Method that handles the {@link RefreshType} command specifically. Calls the {@link PrgProtocolHandler} to
192 * handle the actual refresh based on the channel id.
194 * @param id a non-null, possibly empty channel id to refresh
196 private void handleRefresh(String id) {
197 if (getThing().getStatus() != ThingStatus.ONLINE) {
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));
211 * Gets the trailing number from the channel id (which usually represents the zone number).
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
217 private Integer getTrailingNbr(String id, String channelConstant) {
219 return Integer.parseInt(id.substring(channelConstant.length()));
220 } catch (NumberFormatException e) {
221 logger.warn("Unknown channel port #: {}", id);
227 * Initializes the thing - basically calls {@link #internalInitialize()} to do the work
230 public void initialize() {
232 internalInitialize();
236 * Set's the unit to offline and attempts to reinitialize via {@link #internalInitialize()}
239 public void thingUpdated(Thing thing) {
241 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.HANDLER_CONFIGURATION_PENDING);
243 internalInitialize();
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()}
251 public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
253 if (bridgeStatusInfo.getStatus() == ThingStatus.ONLINE) {
254 internalInitialize();
256 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
261 * Initializes the grafik eye. Essentially validates the {@link GrafikEyeConfig}, updates the status to online and
262 * starts a status refresh job
264 private void internalInitialize() {
265 config = getThing().getConfiguration().as(GrafikEyeConfig.class);
267 if (config == null) {
268 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Configuration file missing");
272 final String configErr = config.validate();
273 if (configErr != null) {
274 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, configErr);
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");
285 final ThingHandler handler = bridge.getHandler();
286 if (handler.getThing().getStatus() != ThingStatus.ONLINE) {
287 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
291 updateStatus(ThingStatus.ONLINE);
292 setFade(config.getFade());
295 pollingJob = this.scheduler.scheduleWithFixedDelay(new Runnable() {
298 final ThingStatus status = getThing().getStatus();
299 if (status == ThingStatus.ONLINE && config != null) {
300 getProtocolHandler().refreshState(config.getControlUnit());
303 }, 1, config.getPolling(), TimeUnit.SECONDS);
307 * Helper method to cancel our polling if we are currently polling
309 private void cancelPolling() {
310 if (pollingJob != null) {
311 pollingJob.cancel(true);
317 * Returns the {@link PrgProtocolHandler} to use
319 * @return a non-null {@link PrgProtocolHandler} to use
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");
327 final PrgProtocolHandler handler = ((PrgBridgeHandler) bridge.getHandler()).getProtocolHandler();
328 if (handler == null) {
329 throw new IllegalArgumentException("No protocol handler set in the PrgBridgeHandler!");
335 * Returns the control unit for this handler
337 * @return the control unit
339 int getControlUnit() {
340 return config.getControlUnit();
344 * Helper method to determine if the given zone is a shade. Off loads it's work to
345 * {@link GrafikEyeConfig#isShadeZone(int)}
347 * @param zone a zone to check
348 * @return true if a shade zone, false otherwise
350 boolean isShade(int zone) {
351 return config == null ? false : config.isShadeZone(zone);
355 * Helper method to expose the ability to change state outside of the class
357 * @param channelId the channel id
358 * @param state the new state
360 void stateChanged(String channelId, State state) {
361 updateState(channelId, state);
365 * Helper method to set the fade level. Will store the fade and update its state.
367 * @param fade the new fade
369 private void setFade(int fade) {
370 if (fade < 0 || fade > 3600) {
371 throw new IllegalArgumentException("fade must be between 1-3600");
375 updateState(PrgConstants.CHANNEL_ZONEFADE, new DecimalType(this.fade));