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.epsonprojector.internal;
15 import java.time.Duration;
16 import java.util.concurrent.ScheduledExecutorService;
17 import java.util.concurrent.ScheduledFuture;
18 import java.util.concurrent.TimeUnit;
20 import org.eclipse.jdt.annotation.NonNullByDefault;
21 import org.eclipse.jdt.annotation.Nullable;
22 import org.openhab.binding.epsonprojector.internal.configuration.EpsonProjectorConfiguration;
23 import org.openhab.binding.epsonprojector.internal.connector.EpsonProjectorConnector;
24 import org.openhab.binding.epsonprojector.internal.connector.EpsonProjectorSerialConnector;
25 import org.openhab.binding.epsonprojector.internal.connector.EpsonProjectorTcpConnector;
26 import org.openhab.binding.epsonprojector.internal.enums.AspectRatio;
27 import org.openhab.binding.epsonprojector.internal.enums.Background;
28 import org.openhab.binding.epsonprojector.internal.enums.ColorMode;
29 import org.openhab.binding.epsonprojector.internal.enums.ErrorMessage;
30 import org.openhab.binding.epsonprojector.internal.enums.Gamma;
31 import org.openhab.binding.epsonprojector.internal.enums.Luminance;
32 import org.openhab.binding.epsonprojector.internal.enums.PowerStatus;
33 import org.openhab.binding.epsonprojector.internal.enums.Switch;
34 import org.openhab.core.cache.ExpiringCache;
35 import org.openhab.core.io.transport.serial.SerialPortManager;
36 import org.slf4j.Logger;
37 import org.slf4j.LoggerFactory;
40 * Provide high level interface to Epson projector.
42 * @author Pauli Anttila - Initial contribution
43 * @author Yannick Schaus - Refactoring
44 * @author Michael Lobstein - Improvements for OH3
47 public class EpsonProjectorDevice {
48 private static final int[] MAP64 = new int[] { 0, 3, 7, 11, 15, 19, 23, 27, 31, 35, 39, 43, 47, 51, 55, 59, 63, 66,
49 70, 74, 78, 82, 86, 90, 94, 98, 102, 106, 110, 114, 118, 122, 126, 129, 133, 137, 141, 145, 149, 153, 157,
50 161, 165, 169, 173, 177, 181, 185, 189, 192, 196, 200, 204, 208, 212, 216, 220, 224, 228, 232, 236, 240,
53 private static final int[] MAP60 = new int[] { 0, 4, 8, 12, 16, 20, 25, 29, 33, 37, 41, 46, 50, 54, 58, 62, 67, 71,
54 75, 79, 83, 88, 92, 96, 100, 104, 109, 113, 117, 121, 125, 130, 134, 138, 142, 146, 151, 155, 159, 163, 167,
55 172, 176, 180, 184, 188, 193, 197, 201, 205, 209, 214, 218, 222, 226, 230, 235, 239, 243, 247, 251 };
57 private static final int[] MAP49 = new int[] { 0, 5, 10, 15, 20, 25, 30, 35, 40, 46, 51, 56, 61, 66, 71, 76, 81, 87,
58 92, 97, 102, 107, 112, 117, 122, 128, 133, 138, 143, 148, 153, 158, 163, 168, 174, 179, 184, 189, 194, 199,
59 204, 209, 215, 220, 225, 230, 235, 240, 245, 250 };
61 private static final int[] MAP48 = new int[] { 0, 5, 10, 15, 20, 26, 31, 36, 41, 47, 52, 57, 62, 67, 73, 78, 83, 88,
62 94, 99, 104, 109, 114, 120, 125, 130, 135, 141, 146, 151, 156, 161, 167, 172, 177, 182, 188, 193, 198, 203,
63 208, 214, 219, 224, 229, 235, 240, 245, 250 };
65 private static final int[] MAP40 = new int[] { 0, 6, 12, 18, 24, 31, 37, 43, 49, 56, 62, 68, 74, 81, 87, 93, 99,
66 106, 112, 118, 124, 131, 137, 143, 149, 156, 162, 168, 174, 181, 187, 193, 199, 206, 212, 218, 224, 231,
69 private static final int[] MAP20 = new int[] { 0, 12, 24, 36, 48, 60, 73, 85, 97, 109, 121, 134, 146, 158, 170, 182,
70 195, 207, 219, 231, 243 };
72 private static final int[] MAP18 = new int[] { 0, 13, 26, 40, 53, 67, 80, 94, 107, 121, 134, 148, 161, 175, 188,
75 private static final int[] MAP_COLOR_TEMP = new int[] { 0, 25, 51, 76, 102, 128, 153, 179, 204, 230 };
76 private static final int[] MAP_FLESH_COLOR = new int[] { 0, 36, 73, 109, 146, 182, 219 };
78 private static final int DEFAULT_TIMEOUT = 5 * 1000;
79 private static final int POWER_ON_TIMEOUT = 100 * 1000;
80 private static final int POWER_OFF_TIMEOUT = 130 * 1000;
81 private static final int LAMP_REFRESH_WAIT_MINUTES = 5;
83 private static final String ON = "ON";
84 private static final String ERR = "ERR";
85 private static final String IMEVENT = "IMEVENT";
87 private final Logger logger = LoggerFactory.getLogger(EpsonProjectorDevice.class);
89 private @Nullable ScheduledExecutorService scheduler = null;
90 private @Nullable ScheduledFuture<?> timeoutJob;
92 private EpsonProjectorConnector connection;
93 private ExpiringCache<Integer> cachedLampHours = new ExpiringCache<>(Duration.ofMinutes(LAMP_REFRESH_WAIT_MINUTES),
95 private boolean connected = false;
96 private boolean ready = true;
98 public EpsonProjectorDevice(SerialPortManager serialPortManager, EpsonProjectorConfiguration config) {
99 connection = new EpsonProjectorSerialConnector(serialPortManager, config.serialPort);
102 public EpsonProjectorDevice(EpsonProjectorConfiguration config) {
103 connection = new EpsonProjectorTcpConnector(config.host, config.port);
106 public boolean isReady() {
110 public void setScheduler(ScheduledExecutorService scheduler) {
111 this.scheduler = scheduler;
114 private synchronized @Nullable String sendQuery(String query, int timeout)
115 throws EpsonProjectorCommandException, EpsonProjectorException {
116 logger.debug("Query: '{}'", query);
117 String response = connection.sendMessage(query, timeout);
119 if (response.length() == 0) {
120 throw new EpsonProjectorException("No response received");
123 response = response.replace("\r:", "");
124 logger.debug("Response: '{}'", response);
126 if (ERR.equals(response) || response.startsWith(IMEVENT)) {
127 throw new EpsonProjectorCommandException("Error response received for command: " + query);
130 if ("PWR OFF".equals(query) && ":".equals(response)) {
131 // When PWR OFF command is sent, next command can be sent 10 seconds after the colon is received
132 logger.debug("Refusing further commands for 10 seconds to power OFF completion");
134 ScheduledExecutorService scheduler = this.scheduler;
135 if (scheduler != null) {
136 timeoutJob = scheduler.schedule(() -> {
138 }, 10, TimeUnit.SECONDS);
145 private String splitResponse(@Nullable String response)
146 throws EpsonProjectorCommandException, EpsonProjectorException {
147 if (response != null && !"".equals(response)) {
148 String[] pieces = response.split("=");
150 if (pieces.length < 2) {
151 throw new EpsonProjectorCommandException("Invalid response from projector: " + response);
154 return pieces[1].trim();
156 throw new EpsonProjectorException("No response received");
160 protected void sendCommand(String command, int timeout)
161 throws EpsonProjectorCommandException, EpsonProjectorException {
162 sendQuery(command, timeout);
165 protected void sendCommand(String command) throws EpsonProjectorCommandException, EpsonProjectorException {
166 sendCommand(command, DEFAULT_TIMEOUT);
169 protected int queryInt(String query, int timeout, int radix)
170 throws EpsonProjectorCommandException, EpsonProjectorException {
171 String response = sendQuery(query, timeout);
173 String str = splitResponse(response);
175 // if the response has two number groups, get the first one (Aspect Ratio does this)
176 if (str.contains(" ")) {
177 String[] subStr = str.split(" ");
182 return Integer.parseInt(str, radix);
183 } catch (NumberFormatException nfe) {
184 throw new EpsonProjectorCommandException(
185 "Unable to parse response '" + str + "' as Integer for command: " + query);
189 protected int queryInt(String query, int timeout) throws EpsonProjectorCommandException, EpsonProjectorException {
190 return queryInt(query, timeout, 10);
193 protected int queryInt(String query) throws EpsonProjectorCommandException, EpsonProjectorException {
194 return queryInt(query, DEFAULT_TIMEOUT, 10);
197 protected int queryHexInt(String query, int timeout)
198 throws EpsonProjectorCommandException, EpsonProjectorException {
199 return queryInt(query, timeout, 16);
202 protected int queryHexInt(String query) throws EpsonProjectorCommandException, EpsonProjectorException {
203 return queryInt(query, DEFAULT_TIMEOUT, 16);
206 protected String queryString(String query) throws EpsonProjectorCommandException, EpsonProjectorException {
207 String response = sendQuery(query, DEFAULT_TIMEOUT);
208 return splitResponse(response);
211 public void connect() throws EpsonProjectorException {
212 connection.connect();
216 public void disconnect() throws EpsonProjectorException {
217 connection.disconnect();
220 ScheduledFuture<?> timeoutJob = this.timeoutJob;
221 if (timeoutJob != null) {
222 timeoutJob.cancel(true);
223 this.timeoutJob = null;
227 public boolean isConnected() {
234 public PowerStatus getPowerStatus() throws EpsonProjectorCommandException, EpsonProjectorException {
235 int val = queryInt("PWR?");
236 return PowerStatus.forValue(val);
239 public void setPower(Switch value) throws EpsonProjectorCommandException, EpsonProjectorException {
240 sendCommand(String.format("PWR %s", value.name()), value == Switch.ON ? POWER_ON_TIMEOUT : POWER_OFF_TIMEOUT);
246 public void sendKeyCode(String value) throws EpsonProjectorCommandException, EpsonProjectorException {
247 sendCommand(String.format("KEY %s", value));
253 public int getVerticalKeystone() throws EpsonProjectorCommandException, EpsonProjectorException {
254 int vkey = queryInt("VKEYSTONE?");
255 for (int i = 0; i < MAP60.length; i++) {
256 if (vkey == MAP60[i]) {
263 public void setVerticalKeystone(int value) throws EpsonProjectorCommandException, EpsonProjectorException {
265 if (value >= 0 && value <= 60) {
266 sendCommand(String.format("VKEYSTONE %d", MAP60[value]));
271 * Horizontal Keystone
273 public int getHorizontalKeystone() throws EpsonProjectorCommandException, EpsonProjectorException {
274 int hkey = queryInt("HKEYSTONE?");
275 for (int i = 0; i < MAP60.length; i++) {
276 if (hkey == MAP60[i]) {
283 public void setHorizontalKeystone(int value) throws EpsonProjectorCommandException, EpsonProjectorException {
285 if (value >= 0 && value <= 60) {
286 sendCommand(String.format("HKEYSTONE %d", MAP60[value]));
294 public Switch getAutoKeystone() throws EpsonProjectorCommandException, EpsonProjectorException {
295 String val = queryString("AUTOKEYSTONE?");
296 return val.equals(ON) ? Switch.ON : Switch.OFF;
299 public void setAutoKeystone(Switch value) throws EpsonProjectorCommandException, EpsonProjectorException {
300 sendCommand(String.format("AUTOKEYSTONE %s", value.name()), DEFAULT_TIMEOUT);
306 public Switch getFreeze() throws EpsonProjectorCommandException, EpsonProjectorException {
307 String val = queryString("FREEZE?");
308 return val.equals(ON) ? Switch.ON : Switch.OFF;
311 public void setFreeze(Switch value) throws EpsonProjectorCommandException, EpsonProjectorException {
312 sendCommand(String.format("FREEZE %s", value.name()), DEFAULT_TIMEOUT);
318 public AspectRatio getAspectRatio() throws EpsonProjectorCommandException, EpsonProjectorException {
319 int val = queryHexInt("ASPECT?");
320 return AspectRatio.forValue(val);
323 public void setAspectRatio(AspectRatio value) throws EpsonProjectorCommandException, EpsonProjectorException {
324 sendCommand(String.format("ASPECT %02X", value.toInt()));
330 public Luminance getLuminance() throws EpsonProjectorCommandException, EpsonProjectorException {
331 int val = queryHexInt("LUMINANCE?");
332 return Luminance.forValue(val);
335 public void setLuminance(Luminance value) throws EpsonProjectorCommandException, EpsonProjectorException {
336 sendCommand(String.format("LUMINANCE %02X", value.toInt()));
342 public String getSource() throws EpsonProjectorCommandException, EpsonProjectorException {
343 return queryString("SOURCE?");
346 public void setSource(String value) throws EpsonProjectorCommandException, EpsonProjectorException {
347 sendCommand(String.format("SOURCE %s", value));
353 public int getBrightness() throws EpsonProjectorCommandException, EpsonProjectorException {
354 int brt = queryInt("BRIGHT?");
355 for (int i = 0; i < MAP48.length; i++) {
356 if (brt == MAP48[i]) {
363 public void setBrightness(int value) throws EpsonProjectorCommandException, EpsonProjectorException {
365 if (value >= 0 && value <= 48) {
366 sendCommand(String.format("BRIGHT %d", MAP48[value]));
373 public int getContrast() throws EpsonProjectorCommandException, EpsonProjectorException {
374 int con = queryInt("CONTRAST?");
375 for (int i = 0; i < MAP48.length; i++) {
376 if (con == MAP48[i]) {
383 public void setContrast(int value) throws EpsonProjectorCommandException, EpsonProjectorException {
385 if (value >= 0 && value <= 48) {
386 sendCommand(String.format("CONTRAST %d", MAP48[value]));
393 public int getDensity() throws EpsonProjectorCommandException, EpsonProjectorException {
394 int den = queryInt("DENSITY?");
395 for (int i = 0; i < MAP64.length; i++) {
396 if (den == MAP64[i]) {
403 public void setDensity(int value) throws EpsonProjectorCommandException, EpsonProjectorException {
405 if (value >= 0 && value <= 64) {
406 sendCommand(String.format("DENSITY %d", MAP64[value]));
413 public int getTint() throws EpsonProjectorCommandException, EpsonProjectorException {
414 int tint = queryInt("TINT?");
415 for (int i = 0; i < MAP64.length; i++) {
416 if (tint == MAP64[i]) {
423 public void setTint(int value) throws EpsonProjectorCommandException, EpsonProjectorException {
425 if (value >= 0 && value <= 64) {
426 sendCommand(String.format("TINT %d", MAP64[value]));
433 public int getColorTemperature() throws EpsonProjectorCommandException, EpsonProjectorException {
434 int ctemp = queryInt("CTEMP?");
435 for (int i = 0; i < MAP_COLOR_TEMP.length; i++) {
436 if (ctemp == MAP_COLOR_TEMP[i]) {
443 public void setColorTemperature(int value) throws EpsonProjectorCommandException, EpsonProjectorException {
444 if (value >= 0 && value <= 9) {
445 sendCommand(String.format("CTEMP %d", MAP_COLOR_TEMP[value]));
452 public int getFleshColor() throws EpsonProjectorCommandException, EpsonProjectorException {
453 int fclr = queryInt("FCOLOR?");
454 for (int i = 0; i < MAP_FLESH_COLOR.length; i++) {
455 if (fclr == MAP_FLESH_COLOR[i]) {
462 public void setFleshColor(int value) throws EpsonProjectorCommandException, EpsonProjectorException {
463 if (value >= 0 && value <= 6) {
464 sendCommand(String.format("FCOLOR %d", MAP_FLESH_COLOR[value]));
471 public ColorMode getColorMode() throws EpsonProjectorCommandException, EpsonProjectorException {
472 int val = queryHexInt("CMODE?");
473 return ColorMode.forValue(val);
476 public void setColorMode(ColorMode value) throws EpsonProjectorCommandException, EpsonProjectorException {
477 sendCommand(String.format("CMODE %02X", value.toInt()));
481 * Horizontal Position
483 public int getHorizontalPosition() throws EpsonProjectorCommandException, EpsonProjectorException {
484 int hpos = queryInt("HPOS?");
485 for (int i = 0; i < MAP49.length; i++) {
486 if (hpos == MAP49[i]) {
493 public void setHorizontalPosition(int value) throws EpsonProjectorCommandException, EpsonProjectorException {
495 if (value >= 0 && value <= 49) {
496 sendCommand(String.format("HPOS %d", MAP49[value]));
503 public int getVerticalPosition() throws EpsonProjectorCommandException, EpsonProjectorException {
504 int vpos = queryInt("VPOS?");
505 for (int i = 0; i < MAP18.length; i++) {
506 if (vpos == MAP18[i]) {
513 public void setVerticalPosition(int value) throws EpsonProjectorCommandException, EpsonProjectorException {
515 if (value >= 0 && value <= 18) {
516 sendCommand(String.format("VPOS %d", MAP18[value]));
523 public Gamma getGamma() throws EpsonProjectorCommandException, EpsonProjectorException {
524 int val = queryHexInt("GAMMA?");
525 return Gamma.forValue(val);
528 public void setGamma(Gamma value) throws EpsonProjectorCommandException, EpsonProjectorException {
529 sendCommand(String.format("GAMMA %02X", value.toInt()));
535 public int getVolume(int maxVolume) throws EpsonProjectorCommandException, EpsonProjectorException {
536 int vol = this.queryInt("VOL?");
539 return this.getMappingValue(MAP20, vol);
541 return this.getMappingValue(MAP40, vol);
546 private int getMappingValue(int[] map, int value) {
547 for (int i = 0; i < map.length; i++) {
548 if (value == map[i]) {
555 public void setVolume(int value, int maxVolume) throws EpsonProjectorCommandException, EpsonProjectorException {
556 if (value >= 0 && value <= maxVolume) {
559 this.sendCommand(String.format("VOL %d", MAP20[value]));
562 this.sendCommand(String.format("VOL %d", MAP40[value]));
571 public Switch getMute() throws EpsonProjectorCommandException, EpsonProjectorException {
572 String val = queryString("MUTE?");
573 return val.equals(ON) ? Switch.ON : Switch.OFF;
576 public void setMute(Switch value) throws EpsonProjectorCommandException, EpsonProjectorException {
577 sendCommand(String.format("MUTE %s", value.name()));
583 public Switch getHorizontalReverse() throws EpsonProjectorCommandException, EpsonProjectorException {
584 String val = queryString("HREVERSE?");
585 return val.equals(ON) ? Switch.ON : Switch.OFF;
588 public void setHorizontalReverse(Switch value) throws EpsonProjectorCommandException, EpsonProjectorException {
589 sendCommand(String.format("HREVERSE %s", value.name()));
595 public Switch getVerticalReverse() throws EpsonProjectorCommandException, EpsonProjectorException {
596 String val = queryString("VREVERSE?");
597 return val.equals(ON) ? Switch.ON : Switch.OFF;
600 public void setVerticalReverse(Switch value) throws EpsonProjectorCommandException, EpsonProjectorException {
601 sendCommand(String.format("VREVERSE %s", value.name()));
605 * Background Select for AV Mute
607 public Background getBackground() throws EpsonProjectorCommandException, EpsonProjectorException {
608 int val = queryHexInt("MSEL?");
609 return Background.forValue(val);
612 public void setBackground(Background value) throws EpsonProjectorCommandException, EpsonProjectorException {
613 sendCommand(String.format("MSEL %02X", value.toInt()));
617 * Lamp Time (hours) - get from cache
619 public int getLampTime() throws EpsonProjectorCommandException, EpsonProjectorException {
620 Integer lampHours = cachedLampHours.getValue();
622 if (lampHours != null) {
623 return lampHours.intValue();
625 throw new EpsonProjectorCommandException("cachedLampHours returned null");
632 private @Nullable Integer queryLamp() {
634 return Integer.valueOf(queryInt("LAMP?"));
635 } catch (EpsonProjectorCommandException | EpsonProjectorException e) {
636 logger.debug("Error executing command LAMP?", e);
644 public int getError() throws EpsonProjectorCommandException, EpsonProjectorException {
645 return queryHexInt("ERR?");
649 * Error Code Description
651 public String getErrorString() throws EpsonProjectorCommandException, EpsonProjectorException {
652 int err = queryHexInt("ERR?");
653 return ErrorMessage.forCode(err);