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) {
175 logger.trace("Received input command {}", command);
176 Input input = new Input(((StringType) command).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 muteStatus.isAudioMuted() ? OnOffType.ON : OnOffType.OFF);
192 updateState(PJLinkDeviceBindingConstants.CHANNEL_VIDEO_MUTE,
193 muteStatus.isVideoMuted() ? OnOffType.ON : OnOffType.OFF);
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(),
230 lampState.isActive() ? OnOffType.ON : OnOffType.OFF);
232 if (isLampHoursChannel) {
233 updateState(lampChannel.getUID(), new DecimalType(lampState.getLampHours()));
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");
243 logger.debug("Received unknown lamp state command {}", command);
247 logger.debug("unknown channel {}", channelUID);
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);
262 private @Nullable String getChannelTypeId(ChannelUID channelUID) {
263 Channel channel = thing.getChannel(channelUID);
264 if (channel == null) {
265 logger.debug("channel is null");
268 ChannelTypeUID channelTypeUID = channel.getChannelTypeUID();
269 if (channelTypeUID == null) {
270 logger.debug("channelTypeUID for channel {} is null", channel);
273 String channelTypeId = channelTypeUID.getId();
274 if (channelTypeId == null) {
275 logger.debug("channelTypeId for channelTypeUID {} is null", channelTypeUID);
278 return channelTypeId;
281 private void handleCommunicationEstablished() {
282 updateStatus(ThingStatus.ONLINE);
286 public void initialize() {
290 public void setup(int delay) {
291 this.clearSetupJob();
292 this.setupJob = scheduler.schedule(() -> {
295 handleCommunicationEstablished();
297 setupRefreshInterval();
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);
307 }, delay, TimeUnit.SECONDS);
310 protected PJLinkDeviceConfiguration getConfiguration() throws ConfigurationException {
311 PJLinkDeviceConfiguration config = this.config;
312 if (config != null) {
316 Map<String, String> validationMessages = new HashMap<>();
318 validateConfigurationParameters(getThing().getConfiguration().getProperties());
319 } catch (ConfigValidationException e) {
320 validationMessages.putAll(e.getValidationMessages());
323 this.config = config = getConfigAs(PJLinkDeviceConfiguration.class);
325 int autoReconnectInterval = config.autoReconnectInterval;
326 if (autoReconnectInterval != 0 && autoReconnectInterval < 30) {
327 validationMessages.put("autoReconnectInterval", "allowed values are 0 (never) or >30");
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);
340 private void clearSetupJob() {
341 final ScheduledFuture<?> setupJob = this.setupJob;
342 if (setupJob != null) {
343 setupJob.cancel(true);
344 this.setupJob = null;
348 private void clearRefreshInterval() {
349 final ScheduledFuture<?> refreshJob = this.refreshJob;
350 if (refreshJob != null) {
351 refreshJob.cancel(true);
352 this.refreshJob = null;
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());
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);
368 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
371 private void handleConfigurationException(ConfigurationException e) {
372 this.clearRefreshInterval();
373 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
376 private void setupDevice() throws ConfigurationException, IOException, AuthenticationException, ResponseException {
377 PJLinkDevice device = getDevice();
378 device.checkAvailability();
380 updateDeviceProperties(device);
381 updateInputChannelStates(device);
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,
395 private void updateDeviceProperties(PJLinkDevice device) throws IOException, AuthenticationException {
396 Map<String, String> properties = editProperties();
398 properties.put(PJLinkDeviceBindingConstants.PROPERTY_AUTHENTICATION_REQUIRED,
399 device.getAuthenticationRequired().toString());
402 properties.put(PJLinkDeviceBindingConstants.PROPERTY_NAME, device.getName());
403 } catch (ResponseException e) {
404 logger.debug("Error retrieving property {}", PJLinkDeviceBindingConstants.PROPERTY_NAME, e);
407 properties.put(Thing.PROPERTY_VENDOR, device.getManufacturer());
408 } catch (ResponseException e) {
409 logger.debug("Error retrieving property {}", Thing.PROPERTY_VENDOR, e);
412 properties.put(Thing.PROPERTY_MODEL_ID, device.getModel());
413 } catch (ResponseException e) {
414 logger.debug("Error retrieving property {}", Thing.PROPERTY_MODEL_ID, e);
417 properties.put(PJLinkDeviceBindingConstants.PROPERTY_CLASS, device.getPJLinkClass());
418 } catch (ResponseException e) {
419 logger.debug("Error retrieving property {}", PJLinkDeviceBindingConstants.PROPERTY_CLASS, e);
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);
428 properties.put(PJLinkDeviceBindingConstants.PROPERTY_OTHER_INFORMATION, device.getOtherInformation());
429 } catch (ResponseException e) {
430 logger.debug("Error retrieving property {}", PJLinkDeviceBindingConstants.PROPERTY_OTHER_INFORMATION, e);
433 updateProperties(properties);
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()));
444 ChannelUID channelUid = new ChannelUID(getThing().getUID(), PJLinkDeviceBindingConstants.CHANNEL_INPUT);
445 this.stateDescriptionProvider.setStateOptions(channelUid, states);