]> git.basschouten.com Git - openhab-addons.git/blob
3501f01d527023041926df0f5fbc93ff4e6ce8b1
[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 stringCommand) {
175                         logger.trace("Received input command {}", command);
176                         Input input = new Input(stringCommand.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                                     OnOffType.from(muteStatus.isAudioMuted()));
192                             updateState(PJLinkDeviceBindingConstants.CHANNEL_VIDEO_MUTE,
193                                     OnOffType.from(muteStatus.isVideoMuted()));
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(), OnOffType.from(lampState.isActive()));
230                                     }
231                                     if (isLampHoursChannel) {
232                                         updateState(lampChannel.getUID(), new DecimalType(lampState.getLampHours()));
233                                     }
234                                 } catch (IndexOutOfBoundsException e) {
235                                     logger.debug("Status information for lamp {} is not available", lampNumber);
236                                     throw new ConfigurationException(
237                                             "Status information for lamp " + lampNumber + " is not available");
238                                 }
239                             }
240                         }
241                     } else {
242                         logger.debug("Received unknown lamp state command {}", command);
243                     }
244                     break;
245                 default:
246                     logger.debug("unknown channel {}", channelUID);
247                     break;
248             }
249
250             logger.trace("Successfully handled command {} on channel {}", command, channelUID.getId());
251             handleCommunicationEstablished();
252         } catch (IOException | ResponseException e) {
253             handleCommunicationException(e);
254         } catch (ConfigurationException e) {
255             handleConfigurationException(e);
256         } catch (AuthenticationException e) {
257             handleAuthenticationException(e);
258         }
259     }
260
261     private @Nullable String getChannelTypeId(ChannelUID channelUID) {
262         Channel channel = thing.getChannel(channelUID);
263         if (channel == null) {
264             logger.debug("channel is null");
265             return null;
266         }
267         ChannelTypeUID channelTypeUID = channel.getChannelTypeUID();
268         if (channelTypeUID == null) {
269             logger.debug("channelTypeUID for channel {} is null", channel);
270             return null;
271         }
272         String channelTypeId = channelTypeUID.getId();
273         if (channelTypeId == null) {
274             logger.debug("channelTypeId for channelTypeUID {} is null", channelTypeUID);
275             return null;
276         }
277         return channelTypeId;
278     }
279
280     private void handleCommunicationEstablished() {
281         updateStatus(ThingStatus.ONLINE);
282     }
283
284     @Override
285     public void initialize() {
286         this.setup(0);
287     }
288
289     public void setup(int delay) {
290         this.clearSetupJob();
291         this.setupJob = scheduler.schedule(() -> {
292             try {
293                 setupDevice();
294                 handleCommunicationEstablished();
295
296                 setupRefreshInterval();
297
298                 logger.trace("device {} setup up successfully", this.getThing().getUID());
299             } catch (ResponseException | IOException e) {
300                 handleCommunicationException(e);
301             } catch (ConfigurationException e) {
302                 handleConfigurationException(e);
303             } catch (AuthenticationException e) {
304                 handleAuthenticationException(e);
305             }
306         }, delay, TimeUnit.SECONDS);
307     }
308
309     protected PJLinkDeviceConfiguration getConfiguration() throws ConfigurationException {
310         PJLinkDeviceConfiguration config = this.config;
311         if (config != null) {
312             return config;
313         }
314
315         Map<String, String> validationMessages = new HashMap<>();
316         try {
317             validateConfigurationParameters(getThing().getConfiguration().getProperties());
318         } catch (ConfigValidationException e) {
319             validationMessages.putAll(e.getValidationMessages());
320         }
321
322         this.config = config = getConfigAs(PJLinkDeviceConfiguration.class);
323
324         int autoReconnectInterval = config.autoReconnectInterval;
325         if (autoReconnectInterval != 0 && autoReconnectInterval < 30) {
326             validationMessages.put("autoReconnectInterval", "allowed values are 0 (never) or >30");
327         }
328
329         if (!validationMessages.isEmpty()) {
330             String message = validationMessages.entrySet().stream()
331                     .map((Map.Entry<String, String> a) -> (a.getKey() + ": " + a.getValue()))
332                     .collect(Collectors.joining("; "));
333             throw new ConfigurationException(message);
334         }
335
336         return config;
337     }
338
339     private void clearSetupJob() {
340         final ScheduledFuture<?> setupJob = this.setupJob;
341         if (setupJob != null) {
342             setupJob.cancel(true);
343             this.setupJob = null;
344         }
345     }
346
347     private void clearRefreshInterval() {
348         final ScheduledFuture<?> refreshJob = this.refreshJob;
349         if (refreshJob != null) {
350             refreshJob.cancel(true);
351             this.refreshJob = null;
352         }
353     }
354
355     private void handleAuthenticationException(AuthenticationException e) {
356         this.clearRefreshInterval();
357         updateProperty(PJLinkDeviceBindingConstants.PROPERTY_AUTHENTICATION_REQUIRED, Boolean.TRUE.toString());
358         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
359     }
360
361     private void handleCommunicationException(Exception e) {
362         this.clearRefreshInterval();
363         PJLinkDeviceConfiguration config = this.config;
364         if (config != null && config.autoReconnectInterval > 0) {
365             this.setup(config.autoReconnectInterval);
366         }
367         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
368     }
369
370     private void handleConfigurationException(ConfigurationException e) {
371         this.clearRefreshInterval();
372         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
373     }
374
375     private void setupDevice() throws ConfigurationException, IOException, AuthenticationException, ResponseException {
376         PJLinkDevice device = getDevice();
377         device.checkAvailability();
378
379         updateDeviceProperties(device);
380         updateInputChannelStates(device);
381     }
382
383     private void setupRefreshInterval() throws ConfigurationException {
384         clearRefreshInterval();
385         PJLinkDeviceConfiguration config = PJLinkDeviceHandler.this.getConfiguration();
386         boolean atLeastOneChannelToBeRefreshed = config.refreshPower || config.refreshMute || config.refreshInputChannel
387                 || config.refreshLampState;
388         if (config.refreshInterval > 0 && atLeastOneChannelToBeRefreshed) {
389             refreshJob = scheduler.scheduleWithFixedDelay(() -> refresh(config), 0, config.refreshInterval,
390                     TimeUnit.SECONDS);
391         }
392     }
393
394     private void updateDeviceProperties(PJLinkDevice device) throws IOException, AuthenticationException {
395         Map<String, String> properties = editProperties();
396
397         properties.put(PJLinkDeviceBindingConstants.PROPERTY_AUTHENTICATION_REQUIRED,
398                 device.getAuthenticationRequired().toString());
399
400         try {
401             properties.put(PJLinkDeviceBindingConstants.PROPERTY_NAME, device.getName());
402         } catch (ResponseException e) {
403             logger.debug("Error retrieving property {}", PJLinkDeviceBindingConstants.PROPERTY_NAME, e);
404         }
405         try {
406             properties.put(Thing.PROPERTY_VENDOR, device.getManufacturer());
407         } catch (ResponseException e) {
408             logger.debug("Error retrieving property {}", Thing.PROPERTY_VENDOR, e);
409         }
410         try {
411             properties.put(Thing.PROPERTY_MODEL_ID, device.getModel());
412         } catch (ResponseException e) {
413             logger.debug("Error retrieving property {}", Thing.PROPERTY_MODEL_ID, e);
414         }
415         try {
416             properties.put(PJLinkDeviceBindingConstants.PROPERTY_CLASS, device.getPJLinkClass());
417         } catch (ResponseException e) {
418             logger.debug("Error retrieving property {}", PJLinkDeviceBindingConstants.PROPERTY_CLASS, e);
419         }
420         try {
421             device.getErrorStatus().forEach((k, v) -> properties
422                     .put(PJLinkDeviceBindingConstants.PROPERTY_ERROR_STATUS + k.getCamelCaseText(), v.getText()));
423         } catch (ResponseException e) {
424             logger.debug("Error retrieving property {}", PJLinkDeviceBindingConstants.PROPERTY_ERROR_STATUS, e);
425         }
426         try {
427             properties.put(PJLinkDeviceBindingConstants.PROPERTY_OTHER_INFORMATION, device.getOtherInformation());
428         } catch (ResponseException e) {
429             logger.debug("Error retrieving property {}", PJLinkDeviceBindingConstants.PROPERTY_OTHER_INFORMATION, e);
430         }
431
432         updateProperties(properties);
433     }
434
435     private void updateInputChannelStates(PJLinkDevice device)
436             throws ResponseException, IOException, AuthenticationException {
437         Set<Input> inputs = device.getAvailableInputs();
438         List<StateOption> states = new LinkedList<>();
439         for (Input input : inputs) {
440             states.add(new StateOption(input.getPJLinkRepresentation(), input.getText()));
441         }
442
443         ChannelUID channelUid = new ChannelUID(getThing().getUID(), PJLinkDeviceBindingConstants.CHANNEL_INPUT);
444         this.stateDescriptionProvider.setStateOptions(channelUid, states);
445     }
446 }