]> git.basschouten.com Git - openhab-addons.git/blob
9fc4942309355c6efbca2889aed76833a455b3a3
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.hyperion.internal.handler;
14
15 import static org.openhab.binding.hyperion.internal.HyperionBindingConstants.*;
16
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;
26
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;
64
65 import com.google.gson.Gson;
66 import com.google.gson.JsonParseException;
67
68 /**
69  * The {@link HyperionNgHandler} is responsible for handling commands, which are
70  * sent to one of the channels.
71  *
72  * @author Daniel Walters - Initial contribution
73  */
74 public class HyperionNgHandler extends BaseThingHandler {
75
76     private final Logger logger = LoggerFactory.getLogger(HyperionNgHandler.class);
77
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";
82
83     private JsonTcpConnection connection;
84     private ScheduledFuture<?> refreshFuture;
85     private ScheduledFuture<?> connectFuture;
86     private Gson gson = new Gson();
87
88     private static final ServerInfoCommand SERVER_INFO_COMMAND = new ServerInfoCommand();
89
90     private String address;
91     private int port;
92     private int refreshInterval;
93     private int priority;
94     private String origin;
95     private HyperionStateDescriptionProvider stateDescriptionProvider;
96
97     private Runnable refreshJob = new Runnable() {
98         @Override
99         public void run() {
100             try {
101                 NgResponse response = sendCommand(SERVER_INFO_COMMAND);
102                 if (response.isSuccess()) {
103                     handleServerInfoResponse(response);
104                 }
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());
112             }
113         }
114     };
115
116     private Runnable connectionJob = new Runnable() {
117         @Override
118         public void run() {
119             try {
120                 if (!connection.isConnected()) {
121                     connection.connect();
122                     updateOnlineStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE, null);
123                 }
124             } catch (IOException e) {
125                 updateOnlineStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
126             }
127         }
128     };
129
130     public HyperionNgHandler(Thing thing, HyperionStateDescriptionProvider stateDescriptionProvider) {
131         super(thing);
132         this.stateDescriptionProvider = stateDescriptionProvider;
133     }
134
135     @Override
136     public void initialize() {
137         logger.debug("Initializing Hyperion.ng thing handler.");
138         try {
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);
145
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());
152         }
153     }
154
155     @Override
156     public void dispose() {
157         logger.debug("Disposing of Hyperion.ng thing handler.");
158         if (refreshFuture != null) {
159             refreshFuture.cancel(true);
160         }
161         if (connectFuture != null) {
162             connectFuture.cancel(true);
163         }
164         if (connection != null && connection.isConnected()) {
165             try {
166                 connection.close();
167             } catch (IOException e) {
168                 // do nothing
169             }
170         }
171     }
172
173     protected void handleServerInfoResponse(NgResponse response) {
174         NgInfo info = response.getInfo();
175         if (info != null) {
176             // update Hyperion, older API compatibility
177             Hyperion hyperion = info.getHyperion();
178             if (hyperion != null) {
179                 updateHyperion(hyperion);
180             }
181
182             // populate the effect states
183             List<Effect> effects = info.getEffects();
184             populateEffects(effects);
185
186             // update adjustments
187             List<Adjustment> adjustments = info.getAdjustment();
188             updateAdjustments(adjustments);
189
190             // update components
191             List<Component> components = info.getComponents();
192             updateComponents(components);
193
194             // update colors/effects
195             List<Priority> priorities = info.getPriorities();
196             updatePriorities(priorities);
197         }
198     }
199
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()));
204         }
205         stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_EFFECT), options);
206     }
207
208     private void updatePriorities(List<Priority> priorities) {
209         populateClearPriorities(priorities);
210
211         String regex = origin + ".*";
212
213         // update color
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))
218                 .findFirst();
219
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);
229         } else {
230             updateState(CHANNEL_COLOR, UnDefType.NULL);
231         }
232
233         // update effect
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))
238                 .findFirst();
239
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);
245         } else {
246             updateState(CHANNEL_EFFECT, UnDefType.NULL);
247         }
248     }
249
250     private void populateClearPriorities(List<Priority> priorities) {
251         List<StateOption> options = new ArrayList<>();
252         options.add(new StateOption("ALL", "ALL"));
253         priorities.stream()
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()));
257                 });
258         stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_CLEAR), options);
259     }
260
261     private void updateHyperion(Hyperion hyperion) {
262         boolean isOff = hyperion.isOff();
263         OnOffType hyperionState = isOff ? OnOffType.OFF : OnOffType.ON;
264         updateState(CHANNEL_HYPERION_ENABLED, hyperionState);
265     }
266
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 = componentIsEnabled ? OnOffType.ON : OnOffType.OFF;
272             switch (componentName) {
273                 case COMPONENT_BLACKBORDER:
274                     updateState(CHANNEL_BLACKBORDER, componentState);
275                     break;
276                 case COMPONENT_SMOOTHING:
277                     updateState(CHANNEL_SMOOTHING, componentState);
278                     break;
279                 case COMPONENT_KODICHECKER:
280                     updateState(CHANNEL_KODICHECKER, componentState);
281                     break;
282                 case COMPONENT_FORWARDER:
283                     updateState(CHANNEL_FORWARDER, componentState);
284                     break;
285                 case COMPONENT_UDPLISTENER:
286                     updateState(CHANNEL_UDPLISTENER, componentState);
287                     break;
288                 case COMPONENT_BOBLIGHTSERVER:
289                     updateState(CHANNEL_BOBLIGHTSERVER, componentState);
290                     break;
291                 case COMPONENT_GRABBER:
292                     updateState(CHANNEL_GRABBER, componentState);
293                     break;
294                 case COMPONENT_V4L:
295                     updateState(CHANNEL_V4L, componentState);
296                     break;
297                 case COMPONENT_LEDDEVICE:
298                     updateState(CHANNEL_LEDDEVICE, componentState);
299                     break;
300                 case COMPONENT_ALL:
301                     updateState(CHANNEL_HYPERION_ENABLED, componentState);
302                     break;
303                 default:
304                     logger.debug("Unknown component: {}", componentName);
305             }
306         }
307     }
308
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();
312
313         if (defaultAdjustment.isPresent()) {
314             int brightness = defaultAdjustment.get().getBrightness();
315             PercentType brightnessState = new PercentType(brightness);
316             updateState(CHANNEL_BRIGHTNESS, brightnessState);
317         } else {
318             updateState(CHANNEL_BRIGHTNESS, UnDefType.NULL);
319         }
320     }
321
322     @Override
323     public void handleCommand(ChannelUID channelUID, Command command) {
324         try {
325             if (command instanceof RefreshType) {
326                 if (refreshFuture.isDone()) {
327                     refreshFuture = scheduler.scheduleWithFixedDelay(refreshJob, 0, refreshInterval, TimeUnit.SECONDS);
328                 } else {
329                     logger.debug("Previous refresh not yet completed");
330                 }
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);
359             }
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());
364         }
365     }
366
367     private void handleComponentEnabled(String channel, Command command)
368             throws IOException, CommandUnsuccessfulException {
369         if (command instanceof OnOffType) {
370             ComponentState componentState = new ComponentState();
371             switch (channel) {
372                 case CHANNEL_BLACKBORDER:
373                     componentState.setComponent(COMPONENT_BLACKBORDER);
374                     break;
375                 case CHANNEL_SMOOTHING:
376                     componentState.setComponent(COMPONENT_SMOOTHING);
377                     break;
378                 case CHANNEL_KODICHECKER:
379                     componentState.setComponent(COMPONENT_KODICHECKER);
380                     break;
381                 case CHANNEL_FORWARDER:
382                     componentState.setComponent(COMPONENT_FORWARDER);
383                     break;
384                 case CHANNEL_UDPLISTENER:
385                     componentState.setComponent(COMPONENT_UDPLISTENER);
386                     break;
387                 case CHANNEL_BOBLIGHTSERVER:
388                     componentState.setComponent(COMPONENT_BOBLIGHTSERVER);
389                     break;
390                 case CHANNEL_GRABBER:
391                     componentState.setComponent(COMPONENT_GRABBER);
392                     break;
393                 case CHANNEL_V4L:
394                     componentState.setComponent(COMPONENT_V4L);
395                     break;
396                 case CHANNEL_LEDDEVICE:
397                     componentState.setComponent(COMPONENT_LEDDEVICE);
398                     break;
399             }
400
401             boolean state = command == OnOffType.ON ? true : false;
402             componentState.setState(state);
403             ComponentStateCommand stateCommand = new ComponentStateCommand(componentState);
404             sendCommand(stateCommand);
405         } else {
406             logger.debug("Channel {} unable to process command {}", channel, command);
407         }
408     }
409
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);
418         } else {
419             logger.debug("Channel {} unable to process command {}", CHANNEL_HYPERION_ENABLED, command);
420         }
421     }
422
423     private void handleBrightness(Command command) throws IOException, CommandUnsuccessfulException {
424         if (command instanceof PercentType percentCommand) {
425             int brightnessValue = percentCommand.intValue();
426
427             Adjustment adjustment = new Adjustment();
428             adjustment.setBrightness(brightnessValue);
429
430             AdjustmentCommand brightnessCommand = new AdjustmentCommand(adjustment);
431             sendCommand(brightnessCommand);
432         } else {
433             logger.debug("Channel {} unable to process command {}", CHANNEL_BRIGHTNESS, command);
434         }
435     }
436
437     private void handleColor(Command command) throws IOException, CommandUnsuccessfulException {
438         if (command instanceof HSBType hsbCommand) {
439             Color c = new Color(hsbCommand.getRGB());
440             int r = c.getRed();
441             int g = c.getGreen();
442             int b = c.getBlue();
443
444             ColorCommand colorCommand = new ColorCommand(r, g, b, priority);
445             colorCommand.setOrigin(origin);
446             sendCommand(colorCommand);
447         } else {
448             logger.debug("Channel {} unable to process command {}", CHANNEL_COLOR, command);
449         }
450     }
451
452     private void handleEffect(Command command) throws IOException, CommandUnsuccessfulException {
453         if (command instanceof StringType) {
454             String effectName = command.toString();
455
456             Effect effect = new Effect(effectName);
457             EffectCommand effectCommand = new EffectCommand(effect, priority);
458
459             effectCommand.setOrigin(origin);
460
461             sendCommand(effectCommand);
462         } else {
463             logger.debug("Channel {} unable to process command {}", CHANNEL_EFFECT, command);
464         }
465     }
466
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);
473             } else {
474                 int priority = Integer.parseInt(cmd);
475                 ClearCommand clearCommand = new ClearCommand(priority);
476                 sendCommand(clearCommand);
477             }
478         }
479     }
480
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);
487         }
488     }
489
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());
496         }
497         return response;
498     }
499 }