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) {
180 updateHyperion(hyperion);
183 // populate the effect states
184 List<Effect> effects = info.getEffects();
185 populateEffects(effects);
187 // update adjustments
188 List<Adjustment> adjustments = info.getAdjustment();
189 updateAdjustments(adjustments);
192 List<Component> components = info.getComponents();
193 updateComponents(components);
195 // update colors/effects
196 List<Priority> priorities = info.getPriorities();
197 updatePriorities(priorities);
201 private void populateEffects(List<Effect> effects) {
202 List<StateOption> options = new ArrayList<>();
203 for (Effect effect : effects) {
204 options.add(new StateOption(effect.getName(), effect.getName()));
206 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_EFFECT), options);
209 private void updatePriorities(List<Priority> priorities) {
210 populateClearPriorities(priorities);
212 String regex = origin + ".*";
215 // find the color priority that has the same origin specified in the Thing configuration
216 Optional<Priority> colorPriority = priorities.stream() // convert list to stream
217 .filter(priority -> COLOR_PRIORITY.equals(priority.getComponentId())
218 && priority.getOrigin().matches(regex))
221 // if there is no color priority for the openHAB origin then set channel to NULL
222 if (colorPriority.isPresent()) {
223 Value value = colorPriority.get().getValue();
224 List<Integer> rgbVals = value.getRGB();
225 int r = rgbVals.get(0);
226 int g = rgbVals.get(1);
227 int b = rgbVals.get(2);
228 HSBType hsbType = HSBType.fromRGB(r, g, b);
229 updateState(CHANNEL_COLOR, hsbType);
231 updateState(CHANNEL_COLOR, UnDefType.NULL);
235 // find the color priority that has the same origin specified in the Thing configuration
236 Optional<Priority> effectPriority = priorities.stream() // convert list to stream
237 .filter(priority -> EFFECT_PRIORITY.equals(priority.getComponentId())
238 && priority.getOrigin().matches(regex))
241 // if there is no effect priority for the openHAB origin then set channel to NULL
242 if (effectPriority.isPresent()) {
243 String effectString = effectPriority.get().getOwner();
244 StringType effect = new StringType(effectString);
245 updateState(CHANNEL_EFFECT, effect);
247 updateState(CHANNEL_EFFECT, UnDefType.NULL);
251 private void populateClearPriorities(List<Priority> priorities) {
252 List<StateOption> options = new ArrayList<>();
253 options.add(new StateOption("ALL", "ALL"));
255 .filter(priority -> priority.getPriority() >= 1 && priority.getPriority() <= 253 && priority.isActive())
256 .forEach(priority -> {
257 options.add(new StateOption(priority.getPriority().toString(), priority.getPriority().toString()));
259 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_CLEAR), options);
262 private void updateHyperion(Hyperion hyperion) {
263 boolean isOff = hyperion.isOff();
264 OnOffType hyperionState = isOff ? OnOffType.OFF : OnOffType.ON;
265 updateState(CHANNEL_HYPERION_ENABLED, hyperionState);
268 private void updateComponents(List<Component> components) {
269 for (Component component : components) {
270 String componentName = component.getName();
271 boolean componentIsEnabled = component.isEnabled();
272 OnOffType componentState = componentIsEnabled ? OnOffType.ON : OnOffType.OFF;
273 switch (componentName) {
274 case COMPONENT_BLACKBORDER:
275 updateState(CHANNEL_BLACKBORDER, componentState);
277 case COMPONENT_SMOOTHING:
278 updateState(CHANNEL_SMOOTHING, componentState);
280 case COMPONENT_KODICHECKER:
281 updateState(CHANNEL_KODICHECKER, componentState);
283 case COMPONENT_FORWARDER:
284 updateState(CHANNEL_FORWARDER, componentState);
286 case COMPONENT_UDPLISTENER:
287 updateState(CHANNEL_UDPLISTENER, componentState);
289 case COMPONENT_BOBLIGHTSERVER:
290 updateState(CHANNEL_BOBLIGHTSERVER, componentState);
292 case COMPONENT_GRABBER:
293 updateState(CHANNEL_GRABBER, componentState);
296 updateState(CHANNEL_V4L, componentState);
298 case COMPONENT_LEDDEVICE:
299 updateState(CHANNEL_LEDDEVICE, componentState);
302 updateState(CHANNEL_HYPERION_ENABLED, componentState);
305 logger.debug("Unknown component: {}", componentName);
310 private void updateAdjustments(List<Adjustment> adjustments) {
311 Optional<Adjustment> defaultAdjustment = adjustments.stream() // convert list to stream
312 .filter(adjustment -> DEFAULT_ADJUSTMENT.equals(adjustment.getId())).findFirst();
314 if (defaultAdjustment.isPresent()) {
315 int brightness = defaultAdjustment.get().getBrightness();
316 PercentType brightnessState = new PercentType(brightness);
317 updateState(CHANNEL_BRIGHTNESS, brightnessState);
319 updateState(CHANNEL_BRIGHTNESS, UnDefType.NULL);
324 public void handleCommand(ChannelUID channelUID, Command command) {
326 if (command instanceof RefreshType) {
327 if (refreshFuture.isDone()) {
328 refreshFuture = scheduler.scheduleWithFixedDelay(refreshJob, 0, refreshInterval, TimeUnit.SECONDS);
330 logger.debug("Previous refresh not yet completed");
332 } else if (CHANNEL_BRIGHTNESS.equals(channelUID.getId())) {
333 handleBrightness(command);
334 } else if (CHANNEL_COLOR.equals(channelUID.getId())) {
335 handleColor(command);
336 } else if (CHANNEL_HYPERION_ENABLED.equals(channelUID.getId())) {
337 handleHyperionEnabled(command);
338 } else if (CHANNEL_EFFECT.equals(channelUID.getId())) {
339 handleEffect(command);
340 } else if (CHANNEL_CLEAR.equals(channelUID.getId())) {
341 handleClear(command);
342 } else if (CHANNEL_BLACKBORDER.equals(channelUID.getId())) {
343 handleComponentEnabled(channelUID.getId(), command);
344 } else if (CHANNEL_SMOOTHING.equals(channelUID.getId())) {
345 handleComponentEnabled(channelUID.getId(), command);
346 } else if (CHANNEL_KODICHECKER.equals(channelUID.getId())) {
347 handleComponentEnabled(channelUID.getId(), command);
348 } else if (CHANNEL_FORWARDER.equals(channelUID.getId())) {
349 handleComponentEnabled(channelUID.getId(), command);
350 } else if (CHANNEL_UDPLISTENER.equals(channelUID.getId())) {
351 handleComponentEnabled(channelUID.getId(), command);
352 } else if (CHANNEL_GRABBER.equals(channelUID.getId())) {
353 handleComponentEnabled(channelUID.getId(), command);
354 } else if (CHANNEL_BOBLIGHTSERVER.equals(channelUID.getId())) {
355 handleComponentEnabled(channelUID.getId(), command);
356 } else if (CHANNEL_V4L.equals(channelUID.getId())) {
357 handleComponentEnabled(channelUID.getId(), command);
358 } else if (CHANNEL_LEDDEVICE.equals(channelUID.getId())) {
359 handleComponentEnabled(channelUID.getId(), command);
361 } catch (IOException e) {
362 updateOnlineStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
363 } catch (CommandUnsuccessfulException e) {
364 logger.debug("Server rejected the command: {}", e.getMessage());
368 private void handleComponentEnabled(String channel, Command command)
369 throws IOException, CommandUnsuccessfulException {
370 if (command instanceof OnOffType) {
371 ComponentState componentState = new ComponentState();
373 case CHANNEL_BLACKBORDER:
374 componentState.setComponent(COMPONENT_BLACKBORDER);
376 case CHANNEL_SMOOTHING:
377 componentState.setComponent(COMPONENT_SMOOTHING);
379 case CHANNEL_KODICHECKER:
380 componentState.setComponent(COMPONENT_KODICHECKER);
382 case CHANNEL_FORWARDER:
383 componentState.setComponent(COMPONENT_FORWARDER);
385 case CHANNEL_UDPLISTENER:
386 componentState.setComponent(COMPONENT_UDPLISTENER);
388 case CHANNEL_BOBLIGHTSERVER:
389 componentState.setComponent(COMPONENT_BOBLIGHTSERVER);
391 case CHANNEL_GRABBER:
392 componentState.setComponent(COMPONENT_GRABBER);
395 componentState.setComponent(COMPONENT_V4L);
397 case CHANNEL_LEDDEVICE:
398 componentState.setComponent(COMPONENT_LEDDEVICE);
402 boolean state = command == OnOffType.ON ? true : false;
403 componentState.setState(state);
404 ComponentStateCommand stateCommand = new ComponentStateCommand(componentState);
405 sendCommand(stateCommand);
407 logger.debug("Channel {} unable to process command {}", channel, command);
411 private void handleHyperionEnabled(Command command) throws IOException, CommandUnsuccessfulException {
412 if (command instanceof OnOffType) {
413 ComponentState componentState = new ComponentState();
414 componentState.setComponent(COMPONENTS_ALL);
415 boolean state = command == OnOffType.ON ? true : false;
416 componentState.setState(state);
417 ComponentStateCommand stateCommand = new ComponentStateCommand(componentState);
418 sendCommand(stateCommand);
420 logger.debug("Channel {} unable to process command {}", CHANNEL_HYPERION_ENABLED, command);
424 private void handleBrightness(Command command) throws IOException, CommandUnsuccessfulException {
425 if (command instanceof PercentType) {
426 PercentType percent = (PercentType) command;
427 int brightnessValue = percent.intValue();
429 Adjustment adjustment = new Adjustment();
430 adjustment.setBrightness(brightnessValue);
432 AdjustmentCommand brightnessCommand = new AdjustmentCommand(adjustment);
433 sendCommand(brightnessCommand);
435 logger.debug("Channel {} unable to process command {}", CHANNEL_BRIGHTNESS, command);
439 private void handleColor(Command command) throws IOException, CommandUnsuccessfulException {
440 if (command instanceof HSBType) {
441 HSBType color = (HSBType) command;
442 Color c = new Color(color.getRGB());
444 int g = c.getGreen();
447 ColorCommand colorCommand = new ColorCommand(r, g, b, priority);
448 colorCommand.setOrigin(origin);
449 sendCommand(colorCommand);
451 logger.debug("Channel {} unable to process command {}", CHANNEL_COLOR, command);
455 private void handleEffect(Command command) throws IOException, CommandUnsuccessfulException {
456 if (command instanceof StringType) {
457 String effectName = command.toString();
459 Effect effect = new Effect(effectName);
460 EffectCommand effectCommand = new EffectCommand(effect, priority);
462 effectCommand.setOrigin(origin);
464 sendCommand(effectCommand);
466 logger.debug("Channel {} unable to process command {}", CHANNEL_EFFECT, command);
470 private void handleClear(Command command) throws IOException, CommandUnsuccessfulException {
471 if (command instanceof StringType) {
472 String cmd = command.toString();
473 if ("ALL".equals(cmd)) {
474 ClearAllCommand clearCommand = new ClearAllCommand();
475 sendCommand(clearCommand);
477 int priority = Integer.parseInt(cmd);
478 ClearCommand clearCommand = new ClearCommand(priority);
479 sendCommand(clearCommand);
484 private void updateOnlineStatus(ThingStatus status, ThingStatusDetail detail, String message) {
485 ThingStatusInfo currentStatusInfo = thing.getStatusInfo();
486 ThingStatus currentStatus = currentStatusInfo.getStatus();
487 ThingStatusDetail currentDetail = currentStatusInfo.getStatusDetail();
488 if (!currentStatus.equals(status) || !currentDetail.equals(detail)) {
489 updateStatus(status, detail, message);
493 public NgResponse sendCommand(HyperionCommand command) throws IOException, CommandUnsuccessfulException {
494 String commandJson = gson.toJson(command);
495 String jsonResponse = connection.send(commandJson);
496 NgResponse response = gson.fromJson(jsonResponse, NgResponse.class);
497 if (!response.isSuccess()) {
498 throw new CommandUnsuccessfulException(gson.toJson(command) + " - Reason: " + response.getError());