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.pjlinkdevice.internal;
15 import static org.openhab.binding.pjlinkdevice.internal.PJLinkDeviceBindingConstants.*;
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;
27 import java.util.concurrent.ScheduledFuture;
28 import java.util.concurrent.TimeUnit;
29 import java.util.stream.Collectors;
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;
59 * The {@link PJLinkDeviceHandler} is responsible for handling commands, which are
60 * sent to one of the channels.
62 * @author Nils Schnabel - Initial contribution
65 public class PJLinkDeviceHandler extends BaseThingHandler {
67 private @Nullable PJLinkDeviceConfiguration config;
69 private @Nullable PJLinkDevice device;
71 private InputChannelStateDescriptionProvider stateDescriptionProvider;
73 private final Logger logger = LoggerFactory.getLogger(PJLinkDeviceHandler.class);
75 private @Nullable ScheduledFuture<?> refreshJob;
77 private @Nullable ScheduledFuture<?> setupJob;
79 public PJLinkDeviceHandler(Thing thing, InputChannelStateDescriptionProvider stateDescriptionProvider) {
81 this.stateDescriptionProvider = stateDescriptionProvider;
85 public void dispose() {
88 clearRefreshInterval();
90 final PJLinkDevice device = this.device;
99 public void refresh(PJLinkDeviceConfiguration config) {
100 PJLinkDeviceHandler.this.logger.debug("Polling device status...");
102 // build list of channels to be refreshed
103 List<String> channelNames = new ArrayList<>();
104 if (config.refreshPower) {
105 channelNames.add(CHANNEL_POWER);
107 if (config.refreshMute) {
108 // this updates both CHANNEL_AUDIO_MUTE and CHANNEL_VIDEO_MUTE
109 channelNames.add(CHANNEL_AUDIO_MUTE);
111 if (config.refreshInputChannel) {
112 channelNames.add(CHANNEL_INPUT);
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);
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();
129 PJLinkDeviceHandler.this.handleCommand(new ChannelUID(getThing().getUID(), channelName),
130 RefreshType.REFRESH);
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);
145 public void handleCommand(ChannelUID channelUID, Command command) {
146 logger.trace("Received command {} on channel {}", command, channelUID.getId());
148 PJLinkDevice device = getDevice();
149 String channelTypeId = getChannelTypeId(channelUID);
150 if (channelTypeId == null) {
151 logger.debug("unknown channel {}", channelUID);
154 switch (channelTypeId) {
155 case CHANNEL_TYPE_POWER:
156 logger.trace("Received power command {}", command);
157 if (command == OnOffType.ON) {
159 } else if (command == OnOffType.OFF) {
161 } else if (command == RefreshType.REFRESH) {
162 updateState(PJLinkDeviceBindingConstants.CHANNEL_POWER,
163 PowerQueryResponseValue.POWER_ON.equals(device.getPowerStatus().getResult())
167 logger.debug("Received unknown power command {}", command);
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);
179 logger.debug("Received unknown channel command {}", command);
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()));
196 logger.trace("Received audio mute command {}", command);
197 boolean muteOn = command == OnOffType.ON;
198 device.setMute(MuteInstructionChannel.AUDIO, muteOn);
201 logger.trace("Received video mute command {}", command);
202 boolean muteOn = command == OnOffType.ON;
203 device.setMute(MuteInstructionChannel.VIDEO, muteOn);
207 logger.debug("Received unknown audio/video mute command {}", command);
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) {
220 boolean isLampActiveChannel = CHANNEL_TYPE_LAMP_ACTIVE.equals(lampChannelTypeId);
221 boolean isLampHoursChannel = CHANNEL_TYPE_LAMP_HOURS.equals(lampChannelTypeId);
223 if (isLampActiveChannel || isLampHoursChannel) {
224 int lampNumber = ((BigDecimal) lampChannel.getConfiguration()
225 .get(CHANNEL_PARAMETER_LAMP_NUMBER)).intValue();
227 LampState lampState = lampStates.get(lampNumber - 1);
228 if (isLampActiveChannel) {
229 updateState(lampChannel.getUID(), OnOffType.from(lampState.isActive()));
231 if (isLampHoursChannel) {
232 updateState(lampChannel.getUID(), new DecimalType(lampState.getLampHours()));
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");
242 logger.debug("Received unknown lamp state command {}", command);
246 logger.debug("unknown channel {}", channelUID);
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);
261 private @Nullable String getChannelTypeId(ChannelUID channelUID) {
262 Channel channel = thing.getChannel(channelUID);
263 if (channel == null) {
264 logger.debug("channel is null");
267 ChannelTypeUID channelTypeUID = channel.getChannelTypeUID();
268 if (channelTypeUID == null) {
269 logger.debug("channelTypeUID for channel {} is null", channel);
272 String channelTypeId = channelTypeUID.getId();
273 if (channelTypeId == null) {
274 logger.debug("channelTypeId for channelTypeUID {} is null", channelTypeUID);
277 return channelTypeId;
280 private void handleCommunicationEstablished() {
281 updateStatus(ThingStatus.ONLINE);
285 public void initialize() {
289 public void setup(int delay) {
290 this.clearSetupJob();
291 this.setupJob = scheduler.schedule(() -> {
294 handleCommunicationEstablished();
296 setupRefreshInterval();
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);
306 }, delay, TimeUnit.SECONDS);
309 protected PJLinkDeviceConfiguration getConfiguration() throws ConfigurationException {
310 PJLinkDeviceConfiguration config = this.config;
311 if (config != null) {
315 Map<String, String> validationMessages = new HashMap<>();
317 validateConfigurationParameters(getThing().getConfiguration().getProperties());
318 } catch (ConfigValidationException e) {
319 validationMessages.putAll(e.getValidationMessages());
322 this.config = config = getConfigAs(PJLinkDeviceConfiguration.class);
324 int autoReconnectInterval = config.autoReconnectInterval;
325 if (autoReconnectInterval != 0 && autoReconnectInterval < 30) {
326 validationMessages.put("autoReconnectInterval", "allowed values are 0 (never) or >30");
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);
339 private void clearSetupJob() {
340 final ScheduledFuture<?> setupJob = this.setupJob;
341 if (setupJob != null) {
342 setupJob.cancel(true);
343 this.setupJob = null;
347 private void clearRefreshInterval() {
348 final ScheduledFuture<?> refreshJob = this.refreshJob;
349 if (refreshJob != null) {
350 refreshJob.cancel(true);
351 this.refreshJob = null;
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());
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);
367 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
370 private void handleConfigurationException(ConfigurationException e) {
371 this.clearRefreshInterval();
372 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
375 private void setupDevice() throws ConfigurationException, IOException, AuthenticationException, ResponseException {
376 PJLinkDevice device = getDevice();
377 device.checkAvailability();
379 updateDeviceProperties(device);
380 updateInputChannelStates(device);
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,
394 private void updateDeviceProperties(PJLinkDevice device) throws IOException, AuthenticationException {
395 Map<String, String> properties = editProperties();
397 properties.put(PJLinkDeviceBindingConstants.PROPERTY_AUTHENTICATION_REQUIRED,
398 device.getAuthenticationRequired().toString());
401 properties.put(PJLinkDeviceBindingConstants.PROPERTY_NAME, device.getName());
402 } catch (ResponseException e) {
403 logger.debug("Error retrieving property {}", PJLinkDeviceBindingConstants.PROPERTY_NAME, e);
406 properties.put(Thing.PROPERTY_VENDOR, device.getManufacturer());
407 } catch (ResponseException e) {
408 logger.debug("Error retrieving property {}", Thing.PROPERTY_VENDOR, e);
411 properties.put(Thing.PROPERTY_MODEL_ID, device.getModel());
412 } catch (ResponseException e) {
413 logger.debug("Error retrieving property {}", Thing.PROPERTY_MODEL_ID, e);
416 properties.put(PJLinkDeviceBindingConstants.PROPERTY_CLASS, device.getPJLinkClass());
417 } catch (ResponseException e) {
418 logger.debug("Error retrieving property {}", PJLinkDeviceBindingConstants.PROPERTY_CLASS, e);
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);
427 properties.put(PJLinkDeviceBindingConstants.PROPERTY_OTHER_INFORMATION, device.getOtherInformation());
428 } catch (ResponseException e) {
429 logger.debug("Error retrieving property {}", PJLinkDeviceBindingConstants.PROPERTY_OTHER_INFORMATION, e);
432 updateProperties(properties);
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()));
443 ChannelUID channelUid = new ChannelUID(getThing().getUID(), PJLinkDeviceBindingConstants.CHANNEL_INPUT);
444 this.stateDescriptionProvider.setStateOptions(channelUid, states);