]> git.basschouten.com Git - openhab-addons.git/blob
fd28f1598e72902a3f5eac8823076269a71767a2
[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.pjlinkdevice.internal;
14
15 import static org.openhab.binding.pjlinkdevice.internal.PJLinkDeviceBindingConstants.*;
16
17 import java.io.IOException;
18 import java.math.BigDecimal;
19 import java.net.InetAddress;
20 import java.net.UnknownHostException;
21 import java.util.ArrayList;
22 import java.util.HashMap;
23 import java.util.LinkedList;
24 import java.util.List;
25 import java.util.Map;
26 import java.util.Set;
27 import java.util.concurrent.ScheduledFuture;
28 import java.util.concurrent.TimeUnit;
29 import java.util.stream.Collectors;
30
31 import org.eclipse.jdt.annotation.NonNullByDefault;
32 import org.eclipse.jdt.annotation.Nullable;
33 import org.openhab.binding.pjlinkdevice.internal.device.PJLinkDevice;
34 import org.openhab.binding.pjlinkdevice.internal.device.command.AuthenticationException;
35 import org.openhab.binding.pjlinkdevice.internal.device.command.ResponseException;
36 import org.openhab.binding.pjlinkdevice.internal.device.command.input.Input;
37 import org.openhab.binding.pjlinkdevice.internal.device.command.lampstatus.LampStatesResponse.LampState;
38 import org.openhab.binding.pjlinkdevice.internal.device.command.mute.MuteInstructionCommand.MuteInstructionChannel;
39 import org.openhab.binding.pjlinkdevice.internal.device.command.mute.MuteQueryResponse.MuteQueryResponseValue;
40 import org.openhab.binding.pjlinkdevice.internal.device.command.power.PowerQueryResponse.PowerQueryResponseValue;
41 import org.openhab.core.config.core.validation.ConfigValidationException;
42 import org.openhab.core.library.types.DecimalType;
43 import org.openhab.core.library.types.OnOffType;
44 import org.openhab.core.library.types.StringType;
45 import org.openhab.core.thing.Channel;
46 import org.openhab.core.thing.ChannelUID;
47 import org.openhab.core.thing.Thing;
48 import org.openhab.core.thing.ThingStatus;
49 import org.openhab.core.thing.ThingStatusDetail;
50 import org.openhab.core.thing.binding.BaseThingHandler;
51 import org.openhab.core.thing.type.ChannelTypeUID;
52 import org.openhab.core.types.Command;
53 import org.openhab.core.types.RefreshType;
54 import org.openhab.core.types.StateOption;
55 import org.slf4j.Logger;
56 import org.slf4j.LoggerFactory;
57
58 /**
59  * The {@link PJLinkDeviceHandler} is responsible for handling commands, which are
60  * sent to one of the channels.
61  *
62  * @author Nils Schnabel - Initial contribution
63  */
64 @NonNullByDefault
65 public class PJLinkDeviceHandler extends BaseThingHandler {
66
67     private @Nullable PJLinkDeviceConfiguration config;
68
69     private @Nullable PJLinkDevice device;
70
71     private InputChannelStateDescriptionProvider stateDescriptionProvider;
72
73     private final Logger logger = LoggerFactory.getLogger(PJLinkDeviceHandler.class);
74
75     private @Nullable ScheduledFuture<?> refreshJob;
76
77     private @Nullable ScheduledFuture<?> setupJob;
78
79     public PJLinkDeviceHandler(Thing thing, InputChannelStateDescriptionProvider stateDescriptionProvider) {
80         super(thing);
81         this.stateDescriptionProvider = stateDescriptionProvider;
82     }
83
84     @Override
85     public void dispose() {
86         clearSetupJob();
87
88         clearRefreshInterval();
89
90         final PJLinkDevice device = this.device;
91         if (device != null) {
92             device.dispose();
93         }
94
95         this.config = null;
96         this.device = null;
97     }
98
99     public void refresh(PJLinkDeviceConfiguration config) {
100         PJLinkDeviceHandler.this.logger.debug("Polling device status...");
101
102         // build list of channels to be refreshed
103         List<String> channelNames = new ArrayList<>();
104         if (config.refreshPower) {
105             channelNames.add(CHANNEL_POWER);
106         }
107         if (config.refreshMute) {
108             // this updates both CHANNEL_AUDIO_MUTE and CHANNEL_VIDEO_MUTE
109             channelNames.add(CHANNEL_AUDIO_MUTE);
110         }
111         if (config.refreshInputChannel) {
112             channelNames.add(CHANNEL_INPUT);
113         }
114         if (config.refreshLampState) {
115             // this updates both CHANNEL_LAMP_ACTIVE and CHANNEL_LAMP_HOURS for all lamps
116             channelNames.add(CHANNEL_LAMP_1_HOURS);
117         }
118
119         // refresh all channels enabled for refreshing
120         for (String channelName : channelNames) {
121             // Do not poll if device is offline
122             if (PJLinkDeviceHandler.this.getThing().getStatus() != ThingStatus.ONLINE) {
123                 PJLinkDeviceHandler.this.logger.debug("Not polling device status because device is offline");
124                 // setup() will schedule a new refresh interval after successful reconnection, cancel this one
125                 this.clearRefreshInterval();
126                 return;
127             }
128
129             PJLinkDeviceHandler.this.handleCommand(new ChannelUID(getThing().getUID(), channelName),
130                     RefreshType.REFRESH);
131         }
132     }
133
134     public PJLinkDevice getDevice() throws UnknownHostException, ConfigurationException {
135         PJLinkDevice device = this.device;
136         if (device == null) {
137             PJLinkDeviceConfiguration config = getConfiguration();
138             this.device = device = new PJLinkDevice(config.tcpPort, InetAddress.getByName(config.ipAddress),
139                     config.adminPassword);
140         }
141         return device;
142     }
143
144     @Override
145     public void handleCommand(ChannelUID channelUID, Command command) {
146         logger.trace("Received command {} on channel {}", command, channelUID.getId());
147         try {
148             PJLinkDevice device = getDevice();
149             String channelTypeId = getChannelTypeId(channelUID);
150             if (channelTypeId == null) {
151                 logger.debug("unknown channel {}", channelUID);
152                 return;
153             }
154             switch (channelTypeId) {
155                 case CHANNEL_TYPE_POWER:
156                     logger.trace("Received power command {}", command);
157                     if (command == OnOffType.ON) {
158                         device.powerOn();
159                     } else if (command == OnOffType.OFF) {
160                         device.powerOff();
161                     } else if (command == RefreshType.REFRESH) {
162                         updateState(PJLinkDeviceBindingConstants.CHANNEL_POWER,
163                                 PowerQueryResponseValue.POWER_ON.equals(device.getPowerStatus().getResult())
164                                         ? OnOffType.ON
165                                         : OnOffType.OFF);
166                     } else {
167                         logger.debug("Received unknown power command {}", command);
168                     }
169                     break;
170                 case CHANNEL_TYPE_INPUT:
171                     if (command == RefreshType.REFRESH) {
172                         StringType input = new StringType(device.getInputStatus().getResult().getValue());
173                         updateState(PJLinkDeviceBindingConstants.CHANNEL_INPUT, input);
174                     } else if (command instanceof StringType) {
175                         logger.trace("Received input command {}", command);
176                         Input input = new Input(((StringType) command).toString());
177                         device.setInput(input);
178                     } else {
179                         logger.debug("Received unknown channel command {}", command);
180                     }
181                     break;
182                 case CHANNEL_TYPE_AUDIO_MUTE:
183                 case CHANNEL_TYPE_VIDEO_MUTE:
184                     boolean isAudioMute = channelTypeId.equals(PJLinkDeviceBindingConstants.CHANNEL_TYPE_AUDIO_MUTE);
185                     boolean isVideoMute = channelTypeId.equals(PJLinkDeviceBindingConstants.CHANNEL_TYPE_VIDEO_MUTE);
186                     if (isVideoMute || isAudioMute) {
187                         if (command == RefreshType.REFRESH) {
188                             // refresh both video and audio mute, as it's one request
189                             MuteQueryResponseValue muteStatus = device.getMuteStatus();
190                             updateState(PJLinkDeviceBindingConstants.CHANNEL_AUDIO_MUTE,
191                                     muteStatus.isAudioMuted() ? OnOffType.ON : OnOffType.OFF);
192                             updateState(PJLinkDeviceBindingConstants.CHANNEL_VIDEO_MUTE,
193                                     muteStatus.isVideoMuted() ? OnOffType.ON : OnOffType.OFF);
194                         } else {
195                             if (isAudioMute) {
196                                 logger.trace("Received audio mute command {}", command);
197                                 boolean muteOn = command == OnOffType.ON;
198                                 device.setMute(MuteInstructionChannel.AUDIO, muteOn);
199                             }
200                             if (isVideoMute) {
201                                 logger.trace("Received video mute command {}", command);
202                                 boolean muteOn = command == OnOffType.ON;
203                                 device.setMute(MuteInstructionChannel.VIDEO, muteOn);
204                             }
205                         }
206                     } else {
207                         logger.debug("Received unknown audio/video mute command {}", command);
208                     }
209                     break;
210                 case CHANNEL_TYPE_LAMP_ACTIVE:
211                 case CHANNEL_TYPE_LAMP_HOURS:
212                     if (command == RefreshType.REFRESH) {
213                         List<LampState> lampStates = device.getLampStatesCached();
214                         // update all lamp related channels, as the response contains information about all of them
215                         for (Channel lampChannel : thing.getChannels()) {
216                             String lampChannelTypeId = getChannelTypeId(lampChannel.getUID());
217                             if (lampChannelTypeId == null) {
218                                 continue;
219                             }
220                             boolean isLampActiveChannel = CHANNEL_TYPE_LAMP_ACTIVE.equals(lampChannelTypeId);
221                             boolean isLampHoursChannel = CHANNEL_TYPE_LAMP_HOURS.equals(lampChannelTypeId);
222
223                             if (isLampActiveChannel || isLampHoursChannel) {
224                                 int lampNumber = ((BigDecimal) lampChannel.getConfiguration()
225                                         .get(CHANNEL_PARAMETER_LAMP_NUMBER)).intValue();
226                                 try {
227                                     LampState lampState = lampStates.get(lampNumber - 1);
228                                     if (isLampActiveChannel) {
229                                         updateState(lampChannel.getUID(),
230                                                 lampState.isActive() ? OnOffType.ON : OnOffType.OFF);
231                                     }
232                                     if (isLampHoursChannel) {
233                                         updateState(lampChannel.getUID(), new DecimalType(lampState.getLampHours()));
234                                     }
235                                 } catch (IndexOutOfBoundsException e) {
236                                     logger.debug("Status information for lamp {} is not available", lampNumber);
237                                     throw new ConfigurationException(
238                                             "Status information for lamp " + lampNumber + " is not available");
239                                 }
240                             }
241                         }
242                     } else {
243                         logger.debug("Received unknown lamp state command {}", command);
244                     }
245                     break;
246                 default:
247                     logger.debug("unknown channel {}", channelUID);
248                     break;
249             }
250
251             logger.trace("Successfully handled command {} on channel {}", command, channelUID.getId());
252             handleCommunicationEstablished();
253         } catch (IOException | ResponseException e) {
254             handleCommunicationException(e);
255         } catch (ConfigurationException e) {
256             handleConfigurationException(e);
257         } catch (AuthenticationException e) {
258             handleAuthenticationException(e);
259         }
260     }
261
262     private @Nullable String getChannelTypeId(ChannelUID channelUID) {
263         Channel channel = thing.getChannel(channelUID);
264         if (channel == null) {
265             logger.debug("channel is null");
266             return null;
267         }
268         ChannelTypeUID channelTypeUID = channel.getChannelTypeUID();
269         if (channelTypeUID == null) {
270             logger.debug("channelTypeUID for channel {} is null", channel);
271             return null;
272         }
273         String channelTypeId = channelTypeUID.getId();
274         if (channelTypeId == null) {
275             logger.debug("channelTypeId for channelTypeUID {} is null", channelTypeUID);
276             return null;
277         }
278         return channelTypeId;
279     }
280
281     private void handleCommunicationEstablished() {
282         updateStatus(ThingStatus.ONLINE);
283     }
284
285     @Override
286     public void initialize() {
287         this.setup(0);
288     }
289
290     public void setup(int delay) {
291         this.clearSetupJob();
292         this.setupJob = scheduler.schedule(() -> {
293             try {
294                 setupDevice();
295                 handleCommunicationEstablished();
296
297                 setupRefreshInterval();
298
299                 logger.trace("device {} setup up successfully", this.getThing().getUID());
300             } catch (ResponseException | IOException e) {
301                 handleCommunicationException(e);
302             } catch (ConfigurationException e) {
303                 handleConfigurationException(e);
304             } catch (AuthenticationException e) {
305                 handleAuthenticationException(e);
306             }
307         }, delay, TimeUnit.SECONDS);
308     }
309
310     protected PJLinkDeviceConfiguration getConfiguration() throws ConfigurationException {
311         PJLinkDeviceConfiguration config = this.config;
312         if (config != null) {
313             return config;
314         }
315
316         Map<String, String> validationMessages = new HashMap<>();
317         try {
318             validateConfigurationParameters(getThing().getConfiguration().getProperties());
319         } catch (ConfigValidationException e) {
320             validationMessages.putAll(e.getValidationMessages());
321         }
322
323         this.config = config = getConfigAs(PJLinkDeviceConfiguration.class);
324
325         int autoReconnectInterval = config.autoReconnectInterval;
326         if (autoReconnectInterval != 0 && autoReconnectInterval < 30) {
327             validationMessages.put("autoReconnectInterval", "allowed values are 0 (never) or >30");
328         }
329
330         if (!validationMessages.isEmpty()) {
331             String message = validationMessages.entrySet().stream()
332                     .map((Map.Entry<String, String> a) -> (a.getKey() + ": " + a.getValue()))
333                     .collect(Collectors.joining("; "));
334             throw new ConfigurationException(message);
335         }
336
337         return config;
338     }
339
340     private void clearSetupJob() {
341         final ScheduledFuture<?> setupJob = this.setupJob;
342         if (setupJob != null) {
343             setupJob.cancel(true);
344             this.setupJob = null;
345         }
346     }
347
348     private void clearRefreshInterval() {
349         final ScheduledFuture<?> refreshJob = this.refreshJob;
350         if (refreshJob != null) {
351             refreshJob.cancel(true);
352             this.refreshJob = null;
353         }
354     }
355
356     private void handleAuthenticationException(AuthenticationException e) {
357         this.clearRefreshInterval();
358         updateProperty(PJLinkDeviceBindingConstants.PROPERTY_AUTHENTICATION_REQUIRED, Boolean.TRUE.toString());
359         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
360     }
361
362     private void handleCommunicationException(Exception e) {
363         this.clearRefreshInterval();
364         PJLinkDeviceConfiguration config = this.config;
365         if (config != null && config.autoReconnectInterval > 0) {
366             this.setup(config.autoReconnectInterval);
367         }
368         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
369     }
370
371     private void handleConfigurationException(ConfigurationException e) {
372         this.clearRefreshInterval();
373         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
374     }
375
376     private void setupDevice() throws ConfigurationException, IOException, AuthenticationException, ResponseException {
377         PJLinkDevice device = getDevice();
378         device.checkAvailability();
379
380         updateDeviceProperties(device);
381         updateInputChannelStates(device);
382     }
383
384     private void setupRefreshInterval() throws ConfigurationException {
385         clearRefreshInterval();
386         PJLinkDeviceConfiguration config = PJLinkDeviceHandler.this.getConfiguration();
387         boolean atLeastOneChannelToBeRefreshed = config.refreshPower || config.refreshMute || config.refreshInputChannel
388                 || config.refreshLampState;
389         if (config.refreshInterval > 0 && atLeastOneChannelToBeRefreshed) {
390             refreshJob = scheduler.scheduleWithFixedDelay(() -> refresh(config), 0, config.refreshInterval,
391                     TimeUnit.SECONDS);
392         }
393     }
394
395     private void updateDeviceProperties(PJLinkDevice device) throws IOException, AuthenticationException {
396         Map<String, String> properties = editProperties();
397
398         properties.put(PJLinkDeviceBindingConstants.PROPERTY_AUTHENTICATION_REQUIRED,
399                 device.getAuthenticationRequired().toString());
400
401         try {
402             properties.put(PJLinkDeviceBindingConstants.PROPERTY_NAME, device.getName());
403         } catch (ResponseException e) {
404             logger.debug("Error retrieving property {}", PJLinkDeviceBindingConstants.PROPERTY_NAME, e);
405         }
406         try {
407             properties.put(Thing.PROPERTY_VENDOR, device.getManufacturer());
408         } catch (ResponseException e) {
409             logger.debug("Error retrieving property {}", Thing.PROPERTY_VENDOR, e);
410         }
411         try {
412             properties.put(Thing.PROPERTY_MODEL_ID, device.getModel());
413         } catch (ResponseException e) {
414             logger.debug("Error retrieving property {}", Thing.PROPERTY_MODEL_ID, e);
415         }
416         try {
417             properties.put(PJLinkDeviceBindingConstants.PROPERTY_CLASS, device.getPJLinkClass());
418         } catch (ResponseException e) {
419             logger.debug("Error retrieving property {}", PJLinkDeviceBindingConstants.PROPERTY_CLASS, e);
420         }
421         try {
422             device.getErrorStatus().forEach((k, v) -> properties
423                     .put(PJLinkDeviceBindingConstants.PROPERTY_ERROR_STATUS + k.getCamelCaseText(), v.getText()));
424         } catch (ResponseException e) {
425             logger.debug("Error retrieving property {}", PJLinkDeviceBindingConstants.PROPERTY_ERROR_STATUS, e);
426         }
427         try {
428             properties.put(PJLinkDeviceBindingConstants.PROPERTY_OTHER_INFORMATION, device.getOtherInformation());
429         } catch (ResponseException e) {
430             logger.debug("Error retrieving property {}", PJLinkDeviceBindingConstants.PROPERTY_OTHER_INFORMATION, e);
431         }
432
433         updateProperties(properties);
434     }
435
436     private void updateInputChannelStates(PJLinkDevice device)
437             throws ResponseException, IOException, AuthenticationException {
438         Set<Input> inputs = device.getAvailableInputs();
439         List<StateOption> states = new LinkedList<>();
440         for (Input input : inputs) {
441             states.add(new StateOption(input.getPJLinkRepresentation(), input.getText()));
442         }
443
444         ChannelUID channelUid = new ChannelUID(getThing().getUID(), PJLinkDeviceBindingConstants.CHANNEL_INPUT);
445         this.stateDescriptionProvider.setStateOptions(channelUid, states);
446     }
447 }