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.hdpowerview.internal.handler;
15 import static org.openhab.binding.hdpowerview.internal.HDPowerViewBindingConstants.*;
17 import java.util.concurrent.ScheduledFuture;
18 import java.util.concurrent.TimeUnit;
20 import org.eclipse.jdt.annotation.NonNullByDefault;
21 import org.eclipse.jdt.annotation.Nullable;
22 import org.openhab.binding.hdpowerview.internal.HDPowerViewWebTargets;
23 import org.openhab.binding.hdpowerview.internal.config.HDPowerViewRepeaterConfiguration;
24 import org.openhab.binding.hdpowerview.internal.dto.Color;
25 import org.openhab.binding.hdpowerview.internal.dto.Firmware;
26 import org.openhab.binding.hdpowerview.internal.dto.responses.RepeaterData;
27 import org.openhab.binding.hdpowerview.internal.exceptions.HubException;
28 import org.openhab.binding.hdpowerview.internal.exceptions.HubInvalidResponseException;
29 import org.openhab.binding.hdpowerview.internal.exceptions.HubMaintenanceException;
30 import org.openhab.core.library.types.HSBType;
31 import org.openhab.core.library.types.IncreaseDecreaseType;
32 import org.openhab.core.library.types.OnOffType;
33 import org.openhab.core.library.types.PercentType;
34 import org.openhab.core.library.types.StringType;
35 import org.openhab.core.thing.Bridge;
36 import org.openhab.core.thing.ChannelUID;
37 import org.openhab.core.thing.Thing;
38 import org.openhab.core.thing.ThingStatus;
39 import org.openhab.core.thing.ThingStatusDetail;
40 import org.openhab.core.types.Command;
41 import org.openhab.core.types.RefreshType;
42 import org.openhab.core.types.UnDefType;
43 import org.openhab.core.util.ColorUtil;
44 import org.slf4j.Logger;
45 import org.slf4j.LoggerFactory;
48 * Handles commands for an HD PowerView Repeater
50 * @author Jacob Laursen - Initial contribution
53 public class HDPowerViewRepeaterHandler extends AbstractHubbedThingHandler {
55 private final Logger logger = LoggerFactory.getLogger(HDPowerViewRepeaterHandler.class);
57 private static final int REFRESH_INTERVAL_MINUTES = 5;
58 private static final int IDENTITY_PERIOD_SECONDS = 3;
59 private static final int BRIGHTNESS_STEP_PERCENT = 5;
60 private static final String COMMAND_IDENTIFY = "IDENTIFY";
62 private @Nullable ScheduledFuture<?> refreshStatusFuture = null;
63 private @Nullable ScheduledFuture<?> resetIdentifyStateFuture = null;
64 private int repeaterId;
66 public HDPowerViewRepeaterHandler(Thing thing) {
71 public void initialize() {
72 repeaterId = getConfigAs(HDPowerViewRepeaterConfiguration.class).id;
73 logger.debug("Initializing repeater handler for repeater {}", repeaterId);
74 Bridge bridge = getBridge();
76 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
77 "@text/offline.conf-error.invalid-bridge-handler");
81 updateStatus(ThingStatus.UNKNOWN);
87 public void dispose() {
88 logger.debug("Disposing repeater handler for repeater {}", repeaterId);
90 cancelResetIdentifyStateJob();
94 public void handleCommand(ChannelUID channelUID, Command command) {
95 HDPowerViewHubHandler bridge = getBridgeHandler();
97 logger.warn("Missing bridge handler");
100 if (command == RefreshType.REFRESH) {
101 scheduleRefreshJob();
104 HDPowerViewWebTargets webTargets = bridge.getWebTargets();
106 RepeaterData repeaterData;
108 switch (channelUID.getId()) {
109 case CHANNEL_REPEATER_COLOR:
110 handleColorCommand(command, webTargets);
112 case CHANNEL_REPEATER_IDENTIFY:
113 if (command instanceof StringType stringCommand) {
114 if (COMMAND_IDENTIFY.equals(stringCommand.toString())) {
115 repeaterData = webTargets.identifyRepeater(repeaterId);
116 scheduler.submit(() -> updatePropertyAndStates(repeaterData));
117 cancelResetIdentifyStateJob();
118 resetIdentifyStateFuture = scheduler.schedule(() -> {
119 updateState(CHANNEL_REPEATER_IDENTIFY, UnDefType.UNDEF);
120 }, IDENTITY_PERIOD_SECONDS, TimeUnit.SECONDS);
122 logger.warn("Unsupported command: {}. Supported commands are: " + COMMAND_IDENTIFY,
127 case CHANNEL_REPEATER_BLINKING_ENABLED:
128 repeaterData = webTargets.enableRepeaterBlinking(repeaterId, OnOffType.ON == command);
129 scheduler.submit(() -> updatePropertyAndStates(repeaterData));
132 } catch (HubInvalidResponseException e) {
133 Throwable cause = e.getCause();
135 logger.warn("Bridge returned a bad JSON response: {}", e.getMessage());
137 logger.warn("Bridge returned a bad JSON response: {} -> {}", e.getMessage(), cause.getMessage());
139 } catch (HubMaintenanceException e) {
140 // exceptions are logged in HDPowerViewWebTargets
141 } catch (HubException e) {
142 logger.warn("Unexpected error: {}", e.getMessage());
146 private void handleColorCommand(Command command, HDPowerViewWebTargets webTargets) throws HubException {
147 if (command instanceof HSBType hsbCommand) {
148 var color = new Color(hsbCommand.getBrightness().intValue(), ColorUtil.hsbTosRgb(hsbCommand));
149 RepeaterData repeaterData = webTargets.setRepeaterColor(repeaterId, color);
150 scheduler.submit(() -> updatePropertyAndStates(repeaterData));
153 Color currentColor = webTargets.getRepeater(repeaterId).color;
154 if (currentColor == null) {
158 if (command instanceof PercentType brightnessCommand) {
159 newColor = applyBrightnessToColor(currentColor, brightnessCommand.intValue());
160 } else if (command instanceof IncreaseDecreaseType increaseDecreaseCommand) {
161 int brightness = switch (increaseDecreaseCommand) {
162 case INCREASE -> currentColor.brightness + BRIGHTNESS_STEP_PERCENT;
163 case DECREASE -> currentColor.brightness - BRIGHTNESS_STEP_PERCENT;
165 brightness = brightness < 0 ? 0 : brightness > 100 ? 100 : brightness;
166 newColor = applyBrightnessToColor(currentColor, brightness);
167 } else if (command instanceof OnOffType) {
168 // Light is turned off either by RGB black or zero brightness.
170 if (command == OnOffType.ON) {
171 // Turn on with maximum brightness level per default,
172 // if no existing brightness level is available.
173 brightness = currentColor.brightness > 0 ? currentColor.brightness : 100;
175 // Turn off by zero brightness to preserve color.
178 newColor = applyBrightnessToColor(currentColor, brightness);
180 logger.warn("Unsupported command: {}", command);
183 RepeaterData repeaterData = webTargets.setRepeaterColor(repeaterId, newColor);
184 scheduler.submit(() -> updatePropertyAndStates(repeaterData));
187 private Color applyBrightnessToColor(Color currentColor, int brightness) {
188 // If light is off by RGB black, then reset to white since otherwise brightness
189 // would have no effect; otherwise preserve color.
190 return currentColor.isBlack() ? new Color(brightness, java.awt.Color.WHITE)
191 : new Color(brightness, currentColor);
194 private void cancelResetIdentifyStateJob() {
195 ScheduledFuture<?> scheduledJob = resetIdentifyStateFuture;
196 if (scheduledJob != null) {
197 scheduledJob.cancel(true);
199 resetIdentifyStateFuture = null;
202 private void scheduleRefreshJob() {
204 logger.debug("Scheduling poll for repeater {} now, then every {} minutes", repeaterId,
205 REFRESH_INTERVAL_MINUTES);
206 this.refreshStatusFuture = scheduler.scheduleWithFixedDelay(this::poll, 0, REFRESH_INTERVAL_MINUTES,
210 private void cancelRefreshJob() {
211 ScheduledFuture<?> future = this.refreshStatusFuture;
212 if (future != null) {
215 this.refreshStatusFuture = null;
218 private synchronized void poll() {
219 HDPowerViewHubHandler bridge = getBridgeHandler();
220 if (bridge == null) {
221 logger.warn("Missing bridge handler");
224 HDPowerViewWebTargets webTargets = bridge.getWebTargets();
226 logger.debug("Polling for status information");
228 RepeaterData repeaterData = webTargets.getRepeater(repeaterId);
229 updatePropertyAndStates(repeaterData);
231 } catch (HubInvalidResponseException e) {
232 Throwable cause = e.getCause();
234 logger.warn("Bridge returned a bad JSON response: {}", e.getMessage());
236 logger.warn("Bridge returned a bad JSON response: {} -> {}", e.getMessage(), cause.getMessage());
238 } catch (HubMaintenanceException e) {
239 // exceptions are logged in HDPowerViewWebTargets
240 } catch (HubException e) {
241 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE, e.getMessage());
245 private void updatePropertyAndStates(RepeaterData repeaterData) {
246 updateStatus(ThingStatus.ONLINE);
248 Firmware firmware = repeaterData.firmware;
249 if (firmware != null) {
250 logger.debug("Repeater firmware version received: {}", firmware.toString());
251 updateProperty(Thing.PROPERTY_FIRMWARE_VERSION, firmware.toString());
253 logger.warn("Repeater firmware version missing in response");
256 Color color = repeaterData.color;
258 logger.debug("Repeater color data received: {}", color.toString());
260 if (color.isBlack()) {
261 // Light is off when RGB black, so discard brightness as otherwise it would appear on.
264 hsb = HSBType.fromRGB(color.red, color.green, color.blue);
265 hsb = new HSBType(hsb.getHue(), hsb.getSaturation(), new PercentType(color.brightness));
267 updateState(CHANNEL_REPEATER_COLOR, hsb);
270 updateState(CHANNEL_REPEATER_BLINKING_ENABLED, OnOffType.from(repeaterData.blinkEnabled));