]> git.basschouten.com Git - openhab-addons.git/blob
26aae96fdee3dc03f28bf20d16aad8ae4513f351
[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
180                 updateHyperion(hyperion);
181             }
182
183             // populate the effect states
184             List<Effect> effects = info.getEffects();
185             populateEffects(effects);
186
187             // update adjustments
188             List<Adjustment> adjustments = info.getAdjustment();
189             updateAdjustments(adjustments);
190
191             // update components
192             List<Component> components = info.getComponents();
193             updateComponents(components);
194
195             // update colors/effects
196             List<Priority> priorities = info.getPriorities();
197             updatePriorities(priorities);
198         }
199     }
200
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()));
205         }
206         stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_EFFECT), options);
207     }
208
209     private void updatePriorities(List<Priority> priorities) {
210         populateClearPriorities(priorities);
211
212         String regex = origin + ".*";
213
214         // update color
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))
219                 .findFirst();
220
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);
230         } else {
231             updateState(CHANNEL_COLOR, UnDefType.NULL);
232         }
233
234         // update effect
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))
239                 .findFirst();
240
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);
246         } else {
247             updateState(CHANNEL_EFFECT, UnDefType.NULL);
248         }
249     }
250
251     private void populateClearPriorities(List<Priority> priorities) {
252         List<StateOption> options = new ArrayList<>();
253         options.add(new StateOption("ALL", "ALL"));
254         priorities.stream()
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()));
258                 });
259         stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_CLEAR), options);
260     }
261
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);
266     }
267
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);
276                     break;
277                 case COMPONENT_SMOOTHING:
278                     updateState(CHANNEL_SMOOTHING, componentState);
279                     break;
280                 case COMPONENT_KODICHECKER:
281                     updateState(CHANNEL_KODICHECKER, componentState);
282                     break;
283                 case COMPONENT_FORWARDER:
284                     updateState(CHANNEL_FORWARDER, componentState);
285                     break;
286                 case COMPONENT_UDPLISTENER:
287                     updateState(CHANNEL_UDPLISTENER, componentState);
288                     break;
289                 case COMPONENT_BOBLIGHTSERVER:
290                     updateState(CHANNEL_BOBLIGHTSERVER, componentState);
291                     break;
292                 case COMPONENT_GRABBER:
293                     updateState(CHANNEL_GRABBER, componentState);
294                     break;
295                 case COMPONENT_V4L:
296                     updateState(CHANNEL_V4L, componentState);
297                     break;
298                 case COMPONENT_LEDDEVICE:
299                     updateState(CHANNEL_LEDDEVICE, componentState);
300                     break;
301                 case COMPONENT_ALL:
302                     updateState(CHANNEL_HYPERION_ENABLED, componentState);
303                     break;
304                 default:
305                     logger.debug("Unknown component: {}", componentName);
306             }
307         }
308     }
309
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();
313
314         if (defaultAdjustment.isPresent()) {
315             int brightness = defaultAdjustment.get().getBrightness();
316             PercentType brightnessState = new PercentType(brightness);
317             updateState(CHANNEL_BRIGHTNESS, brightnessState);
318         } else {
319             updateState(CHANNEL_BRIGHTNESS, UnDefType.NULL);
320         }
321     }
322
323     @Override
324     public void handleCommand(ChannelUID channelUID, Command command) {
325         try {
326             if (command instanceof RefreshType) {
327                 if (refreshFuture.isDone()) {
328                     refreshFuture = scheduler.scheduleWithFixedDelay(refreshJob, 0, refreshInterval, TimeUnit.SECONDS);
329                 } else {
330                     logger.debug("Previous refresh not yet completed");
331                 }
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);
360             }
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());
365         }
366     }
367
368     private void handleComponentEnabled(String channel, Command command)
369             throws IOException, CommandUnsuccessfulException {
370         if (command instanceof OnOffType) {
371             ComponentState componentState = new ComponentState();
372             switch (channel) {
373                 case CHANNEL_BLACKBORDER:
374                     componentState.setComponent(COMPONENT_BLACKBORDER);
375                     break;
376                 case CHANNEL_SMOOTHING:
377                     componentState.setComponent(COMPONENT_SMOOTHING);
378                     break;
379                 case CHANNEL_KODICHECKER:
380                     componentState.setComponent(COMPONENT_KODICHECKER);
381                     break;
382                 case CHANNEL_FORWARDER:
383                     componentState.setComponent(COMPONENT_FORWARDER);
384                     break;
385                 case CHANNEL_UDPLISTENER:
386                     componentState.setComponent(COMPONENT_UDPLISTENER);
387                     break;
388                 case CHANNEL_BOBLIGHTSERVER:
389                     componentState.setComponent(COMPONENT_BOBLIGHTSERVER);
390                     break;
391                 case CHANNEL_GRABBER:
392                     componentState.setComponent(COMPONENT_GRABBER);
393                     break;
394                 case CHANNEL_V4L:
395                     componentState.setComponent(COMPONENT_V4L);
396                     break;
397                 case CHANNEL_LEDDEVICE:
398                     componentState.setComponent(COMPONENT_LEDDEVICE);
399                     break;
400             }
401
402             boolean state = command == OnOffType.ON ? true : false;
403             componentState.setState(state);
404             ComponentStateCommand stateCommand = new ComponentStateCommand(componentState);
405             sendCommand(stateCommand);
406         } else {
407             logger.debug("Channel {} unable to process command {}", channel, command);
408         }
409     }
410
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);
419         } else {
420             logger.debug("Channel {} unable to process command {}", CHANNEL_HYPERION_ENABLED, command);
421         }
422     }
423
424     private void handleBrightness(Command command) throws IOException, CommandUnsuccessfulException {
425         if (command instanceof PercentType) {
426             PercentType percent = (PercentType) command;
427             int brightnessValue = percent.intValue();
428
429             Adjustment adjustment = new Adjustment();
430             adjustment.setBrightness(brightnessValue);
431
432             AdjustmentCommand brightnessCommand = new AdjustmentCommand(adjustment);
433             sendCommand(brightnessCommand);
434         } else {
435             logger.debug("Channel {} unable to process command {}", CHANNEL_BRIGHTNESS, command);
436         }
437     }
438
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());
443             int r = c.getRed();
444             int g = c.getGreen();
445             int b = c.getBlue();
446
447             ColorCommand colorCommand = new ColorCommand(r, g, b, priority);
448             colorCommand.setOrigin(origin);
449             sendCommand(colorCommand);
450         } else {
451             logger.debug("Channel {} unable to process command {}", CHANNEL_COLOR, command);
452         }
453     }
454
455     private void handleEffect(Command command) throws IOException, CommandUnsuccessfulException {
456         if (command instanceof StringType) {
457             String effectName = command.toString();
458
459             Effect effect = new Effect(effectName);
460             EffectCommand effectCommand = new EffectCommand(effect, priority);
461
462             effectCommand.setOrigin(origin);
463
464             sendCommand(effectCommand);
465         } else {
466             logger.debug("Channel {} unable to process command {}", CHANNEL_EFFECT, command);
467         }
468     }
469
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);
476             } else {
477                 int priority = Integer.parseInt(cmd);
478                 ClearCommand clearCommand = new ClearCommand(priority);
479                 sendCommand(clearCommand);
480             }
481         }
482     }
483
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);
490         }
491     }
492
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());
499         }
500         return response;
501     }
502 }