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.hyperion.internal.handler;
15 import static org.openhab.binding.hyperion.internal.HyperionBindingConstants.*;
17 import java.awt.Color;
18 import java.io.IOException;
19 import java.math.BigDecimal;
20 import java.net.UnknownHostException;
21 import java.util.ArrayList;
22 import java.util.List;
23 import java.util.Optional;
24 import java.util.concurrent.ScheduledFuture;
25 import java.util.concurrent.TimeUnit;
27 import org.openhab.binding.hyperion.internal.HyperionStateDescriptionProvider;
28 import org.openhab.binding.hyperion.internal.connection.JsonTcpConnection;
29 import org.openhab.binding.hyperion.internal.protocol.ColorCommand;
30 import org.openhab.binding.hyperion.internal.protocol.CommandUnsuccessfulException;
31 import org.openhab.binding.hyperion.internal.protocol.EffectCommand;
32 import org.openhab.binding.hyperion.internal.protocol.HyperionCommand;
33 import org.openhab.binding.hyperion.internal.protocol.ServerInfoCommand;
34 import org.openhab.binding.hyperion.internal.protocol.ng.Adjustment;
35 import org.openhab.binding.hyperion.internal.protocol.ng.AdjustmentCommand;
36 import org.openhab.binding.hyperion.internal.protocol.ng.Component;
37 import org.openhab.binding.hyperion.internal.protocol.ng.ComponentState;
38 import org.openhab.binding.hyperion.internal.protocol.ng.ComponentStateCommand;
39 import org.openhab.binding.hyperion.internal.protocol.ng.Hyperion;
40 import org.openhab.binding.hyperion.internal.protocol.ng.NgInfo;
41 import org.openhab.binding.hyperion.internal.protocol.ng.NgResponse;
42 import org.openhab.binding.hyperion.internal.protocol.ng.Priority;
43 import org.openhab.binding.hyperion.internal.protocol.ng.Value;
44 import org.openhab.binding.hyperion.internal.protocol.v1.ClearAllCommand;
45 import org.openhab.binding.hyperion.internal.protocol.v1.ClearCommand;
46 import org.openhab.binding.hyperion.internal.protocol.v1.Effect;
47 import org.openhab.core.config.core.Configuration;
48 import org.openhab.core.library.types.HSBType;
49 import org.openhab.core.library.types.OnOffType;
50 import org.openhab.core.library.types.PercentType;
51 import org.openhab.core.library.types.StringType;
52 import org.openhab.core.thing.ChannelUID;
53 import org.openhab.core.thing.Thing;
54 import org.openhab.core.thing.ThingStatus;
55 import org.openhab.core.thing.ThingStatusDetail;
56 import org.openhab.core.thing.ThingStatusInfo;
57 import org.openhab.core.thing.binding.BaseThingHandler;
58 import org.openhab.core.types.Command;
59 import org.openhab.core.types.RefreshType;
60 import org.openhab.core.types.StateOption;
61 import org.openhab.core.types.UnDefType;
62 import org.slf4j.Logger;
63 import org.slf4j.LoggerFactory;
65 import com.google.gson.Gson;
66 import com.google.gson.JsonParseException;
69 * The {@link HyperionNgHandler} is responsible for handling commands, which are
70 * sent to one of the channels.
72 * @author Daniel Walters - Initial contribution
74 public class HyperionNgHandler extends BaseThingHandler {
76 private final Logger logger = LoggerFactory.getLogger(HyperionNgHandler.class);
78 private static final String COLOR_PRIORITY = "COLOR";
79 private static final String EFFECT_PRIORITY = "EFFECT";
80 private static final String DEFAULT_ADJUSTMENT = "default";
81 private static final String COMPONENTS_ALL = "ALL";
83 private JsonTcpConnection connection;
84 private ScheduledFuture<?> refreshFuture;
85 private ScheduledFuture<?> connectFuture;
86 private Gson gson = new Gson();
88 private static final ServerInfoCommand SERVER_INFO_COMMAND = new ServerInfoCommand();
90 private String address;
92 private int refreshInterval;
94 private String origin;
95 private HyperionStateDescriptionProvider stateDescriptionProvider;
97 private Runnable refreshJob = new Runnable() {
101 NgResponse response = sendCommand(SERVER_INFO_COMMAND);
102 if (response.isSuccess()) {
103 handleServerInfoResponse(response);
105 updateOnlineStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE, null);
106 } catch (IOException e) {
107 updateOnlineStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
108 } catch (JsonParseException e) {
109 logger.debug("{}", e.getMessage(), e);
110 } catch (CommandUnsuccessfulException e) {
111 logger.debug("Server rejected the command: {}", e.getMessage());
116 private Runnable connectionJob = new Runnable() {
120 if (!connection.isConnected()) {
121 connection.connect();
122 updateOnlineStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE, null);
124 } catch (IOException e) {
125 updateOnlineStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
130 public HyperionNgHandler(Thing thing, HyperionStateDescriptionProvider stateDescriptionProvider) {
132 this.stateDescriptionProvider = stateDescriptionProvider;
136 public void initialize() {
137 logger.debug("Initializing Hyperion.ng thing handler.");
139 Configuration config = thing.getConfiguration();
140 address = (String) config.get(PROP_HOST);
141 port = ((BigDecimal) config.get(PROP_PORT)).intValue();
142 refreshInterval = ((BigDecimal) config.get(PROP_POLL_FREQUENCY)).intValue();
143 priority = ((BigDecimal) config.get(PROP_PRIORITY)).intValue();
144 origin = (String) config.get(PROP_ORIGIN);
146 connection = new JsonTcpConnection(address, port);
147 connectFuture = scheduler.scheduleWithFixedDelay(connectionJob, 0, refreshInterval, TimeUnit.SECONDS);
148 refreshFuture = scheduler.scheduleWithFixedDelay(refreshJob, 0, refreshInterval, TimeUnit.SECONDS);
149 } catch (UnknownHostException e) {
150 logger.debug("Could not resolve host: {}", e.getMessage());
151 updateOnlineStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
156 public void dispose() {
157 logger.debug("Disposing of Hyperion.ng thing handler.");
158 if (refreshFuture != null) {
159 refreshFuture.cancel(true);
161 if (connectFuture != null) {
162 connectFuture.cancel(true);
164 if (connection != null && connection.isConnected()) {
167 } catch (IOException e) {
173 protected void handleServerInfoResponse(NgResponse response) {
174 NgInfo info = response.getInfo();
176 // update Hyperion, older API compatibility
177 Hyperion hyperion = info.getHyperion();
178 if (hyperion != null) {
179 updateHyperion(hyperion);
182 // populate the effect states
183 List<Effect> effects = info.getEffects();
184 populateEffects(effects);
186 // update adjustments
187 List<Adjustment> adjustments = info.getAdjustment();
188 updateAdjustments(adjustments);
191 List<Component> components = info.getComponents();
192 updateComponents(components);
194 // update colors/effects
195 List<Priority> priorities = info.getPriorities();
196 updatePriorities(priorities);
200 private void populateEffects(List<Effect> effects) {
201 List<StateOption> options = new ArrayList<>();
202 for (Effect effect : effects) {
203 options.add(new StateOption(effect.getName(), effect.getName()));
205 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_EFFECT), options);
208 private void updatePriorities(List<Priority> priorities) {
209 populateClearPriorities(priorities);
211 String regex = origin + ".*";
214 // find the color priority that has the same origin specified in the Thing configuration
215 Optional<Priority> colorPriority = priorities.stream() // convert list to stream
216 .filter(priority -> COLOR_PRIORITY.equals(priority.getComponentId())
217 && priority.getOrigin().matches(regex))
220 // if there is no color priority for the openHAB origin then set channel to NULL
221 if (colorPriority.isPresent()) {
222 Value value = colorPriority.get().getValue();
223 List<Integer> rgbVals = value.getRGB();
224 int r = rgbVals.get(0);
225 int g = rgbVals.get(1);
226 int b = rgbVals.get(2);
227 HSBType hsbType = HSBType.fromRGB(r, g, b);
228 updateState(CHANNEL_COLOR, hsbType);
230 updateState(CHANNEL_COLOR, UnDefType.NULL);
234 // find the color priority that has the same origin specified in the Thing configuration
235 Optional<Priority> effectPriority = priorities.stream() // convert list to stream
236 .filter(priority -> EFFECT_PRIORITY.equals(priority.getComponentId())
237 && priority.getOrigin().matches(regex))
240 // if there is no effect priority for the openHAB origin then set channel to NULL
241 if (effectPriority.isPresent()) {
242 String effectString = effectPriority.get().getOwner();
243 StringType effect = new StringType(effectString);
244 updateState(CHANNEL_EFFECT, effect);
246 updateState(CHANNEL_EFFECT, UnDefType.NULL);
250 private void populateClearPriorities(List<Priority> priorities) {
251 List<StateOption> options = new ArrayList<>();
252 options.add(new StateOption("ALL", "ALL"));
254 .filter(priority -> priority.getPriority() >= 1 && priority.getPriority() <= 253 && priority.isActive())
255 .forEach(priority -> {
256 options.add(new StateOption(priority.getPriority().toString(), priority.getPriority().toString()));
258 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_CLEAR), options);
261 private void updateHyperion(Hyperion hyperion) {
262 boolean isOff = hyperion.isOff();
263 OnOffType hyperionState = OnOffType.from(!isOff);
264 updateState(CHANNEL_HYPERION_ENABLED, hyperionState);
267 private void updateComponents(List<Component> components) {
268 for (Component component : components) {
269 String componentName = component.getName();
270 boolean componentIsEnabled = component.isEnabled();
271 OnOffType componentState = OnOffType.from(componentIsEnabled);
272 switch (componentName) {
273 case COMPONENT_BLACKBORDER:
274 updateState(CHANNEL_BLACKBORDER, componentState);
276 case COMPONENT_SMOOTHING:
277 updateState(CHANNEL_SMOOTHING, componentState);
279 case COMPONENT_KODICHECKER:
280 updateState(CHANNEL_KODICHECKER, componentState);
282 case COMPONENT_FORWARDER:
283 updateState(CHANNEL_FORWARDER, componentState);
285 case COMPONENT_UDPLISTENER:
286 updateState(CHANNEL_UDPLISTENER, componentState);
288 case COMPONENT_BOBLIGHTSERVER:
289 updateState(CHANNEL_BOBLIGHTSERVER, componentState);
291 case COMPONENT_GRABBER:
292 updateState(CHANNEL_GRABBER, componentState);
295 updateState(CHANNEL_V4L, componentState);
297 case COMPONENT_LEDDEVICE:
298 updateState(CHANNEL_LEDDEVICE, componentState);
301 updateState(CHANNEL_HYPERION_ENABLED, componentState);
304 logger.debug("Unknown component: {}", componentName);
309 private void updateAdjustments(List<Adjustment> adjustments) {
310 Optional<Adjustment> defaultAdjustment = adjustments.stream() // convert list to stream
311 .filter(adjustment -> DEFAULT_ADJUSTMENT.equals(adjustment.getId())).findFirst();
313 if (defaultAdjustment.isPresent()) {
314 int brightness = defaultAdjustment.get().getBrightness();
315 PercentType brightnessState = new PercentType(brightness);
316 updateState(CHANNEL_BRIGHTNESS, brightnessState);
318 updateState(CHANNEL_BRIGHTNESS, UnDefType.NULL);
323 public void handleCommand(ChannelUID channelUID, Command command) {
325 if (command instanceof RefreshType) {
326 if (refreshFuture.isDone()) {
327 refreshFuture = scheduler.scheduleWithFixedDelay(refreshJob, 0, refreshInterval, TimeUnit.SECONDS);
329 logger.debug("Previous refresh not yet completed");
331 } else if (CHANNEL_BRIGHTNESS.equals(channelUID.getId())) {
332 handleBrightness(command);
333 } else if (CHANNEL_COLOR.equals(channelUID.getId())) {
334 handleColor(command);
335 } else if (CHANNEL_HYPERION_ENABLED.equals(channelUID.getId())) {
336 handleHyperionEnabled(command);
337 } else if (CHANNEL_EFFECT.equals(channelUID.getId())) {
338 handleEffect(command);
339 } else if (CHANNEL_CLEAR.equals(channelUID.getId())) {
340 handleClear(command);
341 } else if (CHANNEL_BLACKBORDER.equals(channelUID.getId())) {
342 handleComponentEnabled(channelUID.getId(), command);
343 } else if (CHANNEL_SMOOTHING.equals(channelUID.getId())) {
344 handleComponentEnabled(channelUID.getId(), command);
345 } else if (CHANNEL_KODICHECKER.equals(channelUID.getId())) {
346 handleComponentEnabled(channelUID.getId(), command);
347 } else if (CHANNEL_FORWARDER.equals(channelUID.getId())) {
348 handleComponentEnabled(channelUID.getId(), command);
349 } else if (CHANNEL_UDPLISTENER.equals(channelUID.getId())) {
350 handleComponentEnabled(channelUID.getId(), command);
351 } else if (CHANNEL_GRABBER.equals(channelUID.getId())) {
352 handleComponentEnabled(channelUID.getId(), command);
353 } else if (CHANNEL_BOBLIGHTSERVER.equals(channelUID.getId())) {
354 handleComponentEnabled(channelUID.getId(), command);
355 } else if (CHANNEL_V4L.equals(channelUID.getId())) {
356 handleComponentEnabled(channelUID.getId(), command);
357 } else if (CHANNEL_LEDDEVICE.equals(channelUID.getId())) {
358 handleComponentEnabled(channelUID.getId(), command);
360 } catch (IOException e) {
361 updateOnlineStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
362 } catch (CommandUnsuccessfulException e) {
363 logger.debug("Server rejected the command: {}", e.getMessage());
367 private void handleComponentEnabled(String channel, Command command)
368 throws IOException, CommandUnsuccessfulException {
369 if (command instanceof OnOffType) {
370 ComponentState componentState = new ComponentState();
372 case CHANNEL_BLACKBORDER:
373 componentState.setComponent(COMPONENT_BLACKBORDER);
375 case CHANNEL_SMOOTHING:
376 componentState.setComponent(COMPONENT_SMOOTHING);
378 case CHANNEL_KODICHECKER:
379 componentState.setComponent(COMPONENT_KODICHECKER);
381 case CHANNEL_FORWARDER:
382 componentState.setComponent(COMPONENT_FORWARDER);
384 case CHANNEL_UDPLISTENER:
385 componentState.setComponent(COMPONENT_UDPLISTENER);
387 case CHANNEL_BOBLIGHTSERVER:
388 componentState.setComponent(COMPONENT_BOBLIGHTSERVER);
390 case CHANNEL_GRABBER:
391 componentState.setComponent(COMPONENT_GRABBER);
394 componentState.setComponent(COMPONENT_V4L);
396 case CHANNEL_LEDDEVICE:
397 componentState.setComponent(COMPONENT_LEDDEVICE);
401 boolean state = command == OnOffType.ON ? true : false;
402 componentState.setState(state);
403 ComponentStateCommand stateCommand = new ComponentStateCommand(componentState);
404 sendCommand(stateCommand);
406 logger.debug("Channel {} unable to process command {}", channel, command);
410 private void handleHyperionEnabled(Command command) throws IOException, CommandUnsuccessfulException {
411 if (command instanceof OnOffType) {
412 ComponentState componentState = new ComponentState();
413 componentState.setComponent(COMPONENTS_ALL);
414 boolean state = command == OnOffType.ON ? true : false;
415 componentState.setState(state);
416 ComponentStateCommand stateCommand = new ComponentStateCommand(componentState);
417 sendCommand(stateCommand);
419 logger.debug("Channel {} unable to process command {}", CHANNEL_HYPERION_ENABLED, command);
423 private void handleBrightness(Command command) throws IOException, CommandUnsuccessfulException {
424 if (command instanceof PercentType percentCommand) {
425 int brightnessValue = percentCommand.intValue();
427 Adjustment adjustment = new Adjustment();
428 adjustment.setBrightness(brightnessValue);
430 AdjustmentCommand brightnessCommand = new AdjustmentCommand(adjustment);
431 sendCommand(brightnessCommand);
433 logger.debug("Channel {} unable to process command {}", CHANNEL_BRIGHTNESS, command);
437 private void handleColor(Command command) throws IOException, CommandUnsuccessfulException {
438 if (command instanceof HSBType hsbCommand) {
439 Color c = new Color(hsbCommand.getRGB());
441 int g = c.getGreen();
444 ColorCommand colorCommand = new ColorCommand(r, g, b, priority);
445 colorCommand.setOrigin(origin);
446 sendCommand(colorCommand);
448 logger.debug("Channel {} unable to process command {}", CHANNEL_COLOR, command);
452 private void handleEffect(Command command) throws IOException, CommandUnsuccessfulException {
453 if (command instanceof StringType) {
454 String effectName = command.toString();
456 Effect effect = new Effect(effectName);
457 EffectCommand effectCommand = new EffectCommand(effect, priority);
459 effectCommand.setOrigin(origin);
461 sendCommand(effectCommand);
463 logger.debug("Channel {} unable to process command {}", CHANNEL_EFFECT, command);
467 private void handleClear(Command command) throws IOException, CommandUnsuccessfulException {
468 if (command instanceof StringType) {
469 String cmd = command.toString();
470 if ("ALL".equals(cmd)) {
471 ClearAllCommand clearCommand = new ClearAllCommand();
472 sendCommand(clearCommand);
474 int priority = Integer.parseInt(cmd);
475 ClearCommand clearCommand = new ClearCommand(priority);
476 sendCommand(clearCommand);
481 private void updateOnlineStatus(ThingStatus status, ThingStatusDetail detail, String message) {
482 ThingStatusInfo currentStatusInfo = thing.getStatusInfo();
483 ThingStatus currentStatus = currentStatusInfo.getStatus();
484 ThingStatusDetail currentDetail = currentStatusInfo.getStatusDetail();
485 if (!currentStatus.equals(status) || !currentDetail.equals(detail)) {
486 updateStatus(status, detail, message);
490 public NgResponse sendCommand(HyperionCommand command) throws IOException, CommandUnsuccessfulException {
491 String commandJson = gson.toJson(command);
492 String jsonResponse = connection.send(commandJson);
493 NgResponse response = gson.fromJson(jsonResponse, NgResponse.class);
494 if (!response.isSuccess()) {
495 throw new CommandUnsuccessfulException(gson.toJson(command) + " - Reason: " + response.getError());