2 * Copyright (c) 2010-2020 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.lutron.internal.grxprg;
15 import java.io.IOException;
16 import java.time.ZoneId;
17 import java.time.ZonedDateTime;
18 import java.util.Calendar;
19 import java.util.HashMap;
21 import java.util.concurrent.ArrayBlockingQueue;
22 import java.util.concurrent.BlockingQueue;
23 import java.util.concurrent.TimeUnit;
24 import java.util.regex.Matcher;
25 import java.util.regex.Pattern;
27 import org.apache.commons.lang.NullArgumentException;
28 import org.openhab.core.library.types.DateTimeType;
29 import org.openhab.core.library.types.DecimalType;
30 import org.openhab.core.library.types.OnOffType;
31 import org.openhab.core.library.types.PercentType;
32 import org.openhab.core.library.types.StringType;
33 import org.openhab.core.thing.ThingStatus;
34 import org.openhab.core.thing.ThingStatusDetail;
35 import org.slf4j.Logger;
36 import org.slf4j.LoggerFactory;
39 * This is the protocol handler for the GRX-PRG/GRX-CI-PRG. This handler will issue the protocol commands and will
40 * process the responses from the interface. This handler was written to respond to any response that can be sent from
41 * the TCP/IP session (either in response to our own commands or in response to external events [other TCP/IP sessions,
44 * @author Tim Roberts - Initial contribution
47 class PrgProtocolHandler {
48 private Logger logger = LoggerFactory.getLogger(PrgProtocolHandler.class);
51 * The {@link SocketSession} used by this protocol handler
53 private final SocketSession session;
56 * The {@link PrgBridgeHandler} to call back to update status and state
58 private final PrgHandlerCallback phCallback;
60 // ------------------------------------------------------------------------------------------------
61 // The following are the various command formats specified by the
62 // http://www.lutron.com/TechnicalDocumentLibrary/RS232ProtocolCommandSet.040196d.pdf
63 private static final String CMD_SCENE = "A";
64 private static final String CMD_SCENELOCK = "SL";
65 private static final String CMD_SCENESTATUS = "G";
66 private static final String CMD_SCENESEQ = "SQ";
67 private static final String CMD_ZONELOCK = "ZL";
68 private static final String CMD_ZONELOWER = "D";
69 private static final String CMD_ZONELOWERSTOP = "E";
70 private static final String CMD_ZONERAISE = "B";
71 private static final String CMD_ZONERAISESTOP = "C";
72 private static final String CMD_ZONEINTENSITY = "szi";
73 private static final String CMD_ZONEINTENSITYSTATUS = "rzi";
74 private static final String CMD_SETTIME = "ST";
75 private static final String CMD_READTIME = "RT";
76 private static final String CMD_SELECTSCHEDULE = "SS";
77 private static final String CMD_REPORTSCHEDULE = "RS";
78 private static final String CMD_SUNRISESUNSET = "RA";
79 private static final String CMD_SUPERSEQUENCESTART = "QS";
80 private static final String CMD_SUPERSEQUENCEPAUSE = "QP";
81 private static final String CMD_SUPERSEQUENCERESUME = "QC";
82 private static final String CMD_SUPERSEQUENCESTATUS = "Q?";
84 // ------------------------------------------------------------------------------------------------
85 // The following are the various responses specified by the
86 // http://www.lutron.com/TechnicalDocumentLibrary/RS232ProtocolCommandSet.040196d.pdf
87 private static final Pattern RSP_FAILED = Pattern.compile("^~ERROR # (\\d+) (\\d+) OK");
88 private static final Pattern RSP_OK = Pattern.compile("^~(\\d+) OK");
89 private static final Pattern RSP_RESETTING = Pattern.compile("^~:Reseting Device... (\\d+) OK");
90 private static final Pattern RSP_RMU = Pattern
91 .compile("^~:mu (\\d) (\\d+) (\\w+) (\\w+) (\\w+) (\\w+) (\\w+) (\\w+) (\\w+)");
92 private static final Pattern RSP_SCENESTATUS = Pattern.compile("^~?:ss (\\w{8,8})( (\\d+) OK)?");
93 private static final Pattern RSP_ZONEINTENSITY = Pattern.compile(
94 "^~:zi (\\d) (\\w{1,3}) (\\w{1,3}) (\\w{1,3}) (\\w{1,3}) (\\w{1,3}) (\\w{1,3}) (\\w{1,3}) (\\w{1,3}) (\\d+) OK");
95 private static final Pattern RSP_REPORTIME = Pattern
96 .compile("^~:rt (\\d{1,2}) (\\d{1,2}) (\\d{1,2}) (\\d{1,2}) (\\d{1,2}) (\\d) (\\d+) OK");
97 private static final Pattern RSP_REPORTSCHEDULE = Pattern.compile("^~:rs (\\d) (\\d+) OK");
98 private static final Pattern RSP_SUNRISESUNSET = Pattern
99 .compile("^~:ra (\\d{1,3}) (\\d{1,3}) (\\d{1,3}) (\\d{1,3}) (\\d+) OK");
100 private static final Pattern RSP_SUPERSEQUENCESTATUS = Pattern
101 .compile("^~:s\\? (\\w) (\\d+) (\\d{1,2}) (\\d{1,2}) (\\d+) OK");
102 private static final Pattern RSP_BUTTON = Pattern.compile("^[^~:].*");
103 private static final String RSP_CONNECTION_ESTABLISHED = "connection established";
106 * A lookup between a 0-100 percentage and corresponding hex value. Note: this specifically matches the liason
109 private static final Map<Integer, String> INTENSITY_MAP = new HashMap<>();
112 * The reverse lookup for the {{@link #INTENSITY_MAP}
114 private static final Map<String, Integer> REVERSE_INTENSITY_MAP = new HashMap<>();
117 * A lookup between returned shade hex intensity to corresponding shade values
119 private static final Map<String, Integer> SHADE_INTENSITY_MAP = new HashMap<>();
122 * Cache of current zone intensities
124 private final int[] zoneIntensities = new int[8];
127 * Static method to setup the intensity lookup maps
130 INTENSITY_MAP.put(0, "0");
131 INTENSITY_MAP.put(1, "2");
132 INTENSITY_MAP.put(2, "3");
133 INTENSITY_MAP.put(3, "4");
134 INTENSITY_MAP.put(4, "6");
135 INTENSITY_MAP.put(5, "7");
136 INTENSITY_MAP.put(6, "8");
137 INTENSITY_MAP.put(7, "9");
138 INTENSITY_MAP.put(8, "B");
139 INTENSITY_MAP.put(9, "C");
140 INTENSITY_MAP.put(10, "D");
141 INTENSITY_MAP.put(11, "F");
142 INTENSITY_MAP.put(12, "10");
143 INTENSITY_MAP.put(13, "11");
144 INTENSITY_MAP.put(14, "12");
145 INTENSITY_MAP.put(15, "14");
146 INTENSITY_MAP.put(16, "15");
147 INTENSITY_MAP.put(17, "16");
148 INTENSITY_MAP.put(18, "18");
149 INTENSITY_MAP.put(19, "19");
150 INTENSITY_MAP.put(20, "1A");
151 INTENSITY_MAP.put(21, "1B");
152 INTENSITY_MAP.put(22, "1D");
153 INTENSITY_MAP.put(23, "1E");
154 INTENSITY_MAP.put(24, "1F");
155 INTENSITY_MAP.put(25, "20");
156 INTENSITY_MAP.put(26, "22");
157 INTENSITY_MAP.put(27, "23");
158 INTENSITY_MAP.put(28, "24");
159 INTENSITY_MAP.put(29, "26");
160 INTENSITY_MAP.put(30, "27");
161 INTENSITY_MAP.put(31, "28");
162 INTENSITY_MAP.put(32, "29");
163 INTENSITY_MAP.put(33, "2B");
164 INTENSITY_MAP.put(34, "2C");
165 INTENSITY_MAP.put(35, "2D");
166 INTENSITY_MAP.put(36, "2F");
167 INTENSITY_MAP.put(37, "30");
168 INTENSITY_MAP.put(38, "31");
169 INTENSITY_MAP.put(39, "32");
170 INTENSITY_MAP.put(40, "34");
171 INTENSITY_MAP.put(41, "35");
172 INTENSITY_MAP.put(42, "36");
173 INTENSITY_MAP.put(43, "38");
174 INTENSITY_MAP.put(44, "39");
175 INTENSITY_MAP.put(45, "3A");
176 INTENSITY_MAP.put(46, "3B");
177 INTENSITY_MAP.put(47, "3D");
178 INTENSITY_MAP.put(48, "3E");
179 INTENSITY_MAP.put(49, "3F");
180 INTENSITY_MAP.put(50, "40");
181 INTENSITY_MAP.put(51, "42");
182 INTENSITY_MAP.put(52, "43");
183 INTENSITY_MAP.put(53, "44");
184 INTENSITY_MAP.put(54, "46");
185 INTENSITY_MAP.put(55, "47");
186 INTENSITY_MAP.put(56, "48");
187 INTENSITY_MAP.put(57, "49");
188 INTENSITY_MAP.put(58, "4B");
189 INTENSITY_MAP.put(59, "4C");
190 INTENSITY_MAP.put(60, "4D");
191 INTENSITY_MAP.put(61, "4F");
192 INTENSITY_MAP.put(62, "50");
193 INTENSITY_MAP.put(63, "51");
194 INTENSITY_MAP.put(64, "52");
195 INTENSITY_MAP.put(65, "54");
196 INTENSITY_MAP.put(66, "55");
197 INTENSITY_MAP.put(67, "56");
198 INTENSITY_MAP.put(68, "58");
199 INTENSITY_MAP.put(69, "59");
200 INTENSITY_MAP.put(70, "5A");
201 INTENSITY_MAP.put(71, "5B");
202 INTENSITY_MAP.put(72, "5D");
203 INTENSITY_MAP.put(73, "5E");
204 INTENSITY_MAP.put(74, "5F");
205 INTENSITY_MAP.put(75, "60");
206 INTENSITY_MAP.put(76, "62");
207 INTENSITY_MAP.put(77, "63");
208 INTENSITY_MAP.put(78, "64");
209 INTENSITY_MAP.put(79, "66");
210 INTENSITY_MAP.put(80, "67");
211 INTENSITY_MAP.put(81, "68");
212 INTENSITY_MAP.put(82, "69");
213 INTENSITY_MAP.put(83, "6B");
214 INTENSITY_MAP.put(84, "6C");
215 INTENSITY_MAP.put(85, "6D");
216 INTENSITY_MAP.put(86, "6F");
217 INTENSITY_MAP.put(87, "70");
218 INTENSITY_MAP.put(88, "71");
219 INTENSITY_MAP.put(89, "72");
220 INTENSITY_MAP.put(90, "74");
221 INTENSITY_MAP.put(91, "75");
222 INTENSITY_MAP.put(92, "76");
223 INTENSITY_MAP.put(93, "78");
224 INTENSITY_MAP.put(94, "79");
225 INTENSITY_MAP.put(95, "7A");
226 INTENSITY_MAP.put(96, "7B");
227 INTENSITY_MAP.put(97, "7D");
228 INTENSITY_MAP.put(98, "7E");
229 INTENSITY_MAP.put(99, "7F");
230 INTENSITY_MAP.put(100, "7F");
232 for (int key : INTENSITY_MAP.keySet()) {
233 String value = INTENSITY_MAP.get(key);
234 REVERSE_INTENSITY_MAP.put(value, key);
237 SHADE_INTENSITY_MAP.put("0", 0);
238 SHADE_INTENSITY_MAP.put("5E", 0);
239 SHADE_INTENSITY_MAP.put("15", 1);
240 SHADE_INTENSITY_MAP.put("2D", 2);
241 SHADE_INTENSITY_MAP.put("71", 3);
242 SHADE_INTENSITY_MAP.put("72", 4);
243 SHADE_INTENSITY_MAP.put("73", 5);
244 SHADE_INTENSITY_MAP.put("5F", 1);
245 SHADE_INTENSITY_MAP.put("60", 2);
246 SHADE_INTENSITY_MAP.put("61", 3);
247 SHADE_INTENSITY_MAP.put("62", 4);
248 SHADE_INTENSITY_MAP.put("63", 5);
252 * Lookup of valid scene numbers (H is also sometimes returned - no idea what it is however)
254 private static final String VALID_SCENES = "0123456789ABCDEFG";
257 * Constructs the protocol handler from given parameters
259 * @param session a non-null {@link SocketSession} (may be connected or disconnected)
260 * @param config a non-null {@link PrgHandlerCallback}
262 PrgProtocolHandler(SocketSession session, PrgHandlerCallback callback) {
263 if (session == null) {
264 throw new IllegalArgumentException("session cannot be null");
267 if (callback == null) {
268 throw new IllegalArgumentException("callback cannot be null");
271 this.session = session;
272 this.phCallback = callback;
276 * Attempts to log into the interface.
278 * @return a null if logged in successfully. Non-null if an exception occurred.
279 * @throws IOException an IO exception occurred during login
281 String login(String username) throws Exception {
282 logger.info("Logging into the PRG interface");
283 final NoDispatchingCallback callback = new NoDispatchingCallback();
284 session.setCallback(callback);
286 String response = callback.getResponse();
287 if (response.equals("login")) {
288 session.sendCommand(username);
290 return "Protocol violation - wasn't initially a command failure or login prompt: " + response;
293 // We should have received back a connection established response
294 response = callback.getResponse();
296 // Burn the empty response if we got one (
297 if (response.equals("")) {
298 response = callback.getResponse();
301 if (RSP_CONNECTION_ESTABLISHED.equals(response)) {
305 return "login failed";
310 * Post successful login stuff - mark us online and refresh from the switch
312 * @throws IOException
314 private void postLogin() throws IOException {
315 logger.info("PRG interface now connected");
316 session.setCallback(new NormalResponseCallback());
317 phCallback.statusChanged(ThingStatus.ONLINE, ThingStatusDetail.NONE, null);
321 * Refreshes the state of the specified control unit
323 * @param controlUnit the control unit to refresh
325 void refreshState(int controlUnit) {
326 logger.debug("Refreshing control unit ({}) state", controlUnit);
330 refreshSunriseSunset();
331 reportSuperSequenceStatus();
333 // The RMU would return the zone lock, scene lock and scene seq state
334 // Unfortunately, if any of those are true - the PRG interface locks up
335 // the response until turned off - so comment out
337 // Get the current state of the zone/scene lock
338 // sendCommand("spm");
339 // sendCommand("rmu " + controlUnit);
340 // sendCommand("epm");
342 refreshZoneIntensity(controlUnit);
346 * Validate the control unit parameter
348 * @param controlUnit a control unit between 1-8
349 * @throws IllegalArgumentException if controlUnit is < 0 or > 8
351 private void validateControlUnit(int controlUnit) {
352 if (controlUnit < 1 || controlUnit > 8) {
353 throw new IllegalArgumentException("Invalid control unit (must be between 1 and 8): " + controlUnit);
358 * Validates the scene and converts it to the corresponding hex value
360 * @param scene a scene between 0 and 16
361 * @return the valid hex value of the scene
362 * @throws IllegalArgumentException if scene is < 0 or > 16
364 private char convertScene(int scene) {
365 if (scene < 0 || scene > VALID_SCENES.length()) {
366 throw new IllegalArgumentException(
367 "Invalid scene (must be between 0 and " + VALID_SCENES.length() + "): " + scene);
369 return VALID_SCENES.charAt(scene);
375 * @param zone the zone to validate
376 * @throws IllegalArgumentException if zone < 1 or > 8
378 private void validateZone(int zone) {
379 if (zone < 1 || zone > 8) {
380 throw new IllegalArgumentException("Invalid zone (must be between 1 and 8): " + zone);
385 * Validates the fade and converts it to hex
387 * @param fade the fade
388 * @return a valid fade value
389 * @throws IllegalArgumentException if fade < 0 or > 120
391 private String convertFade(int fade) {
392 if (fade < 0 || fade > 120) {
393 throw new IllegalArgumentException("Invalid fade (must be between 1 and 120): " + fade);
396 fade = (fade / 60) + 59;
398 return Integer.toHexString(fade).toUpperCase();
402 * Validates a zone intensity and returns the hex corresponding value (handles shade intensity zones as well)
404 * @param controlUnit the control unit
405 * @param zone the zone
406 * @param intensity the new intensity level
407 * @return a valid hex representation
408 * @throws IllegalArgumentException if controlUnit, zone or intensity are invalid
410 private String convertIntensity(int controlUnit, int zone, int intensity) {
411 validateControlUnit(controlUnit);
414 if (intensity < 0 || intensity > 100) {
415 throw new IllegalArgumentException("Invalid intensity (must be between 0 and 100): " + intensity);
418 final boolean isShade = phCallback.isShade(controlUnit, zone);
421 throw new IllegalArgumentException("Invalid SHADE intensity (must be between 0 and 5): " + intensity);
423 return Integer.toString(intensity);
425 final String hexNbr = INTENSITY_MAP.get(intensity);
426 if (hexNbr == null) { // this should be impossible as all 100 values are in table
427 logger.warn("Unknown zone intensity ({})", intensity);
428 return Integer.toHexString(intensity).toUpperCase();
435 * Converts a hex zone intensity back to a integer - handles shade zones as well
437 * @param controlUnit the control unit
438 * @param zone the zone
439 * @param intensity the hex intensity value
440 * @return the new intensity (between 0-100)
441 * @throws IllegalArgumentException if controlUnit, zone or intensity are invalid
443 private int convertIntensity(int controlUnit, int zone, String intensity) {
444 validateControlUnit(controlUnit);
447 final boolean isShade = phCallback.isShade(controlUnit, zone);
450 final Integer intNbr = SHADE_INTENSITY_MAP.get(intensity);
451 if (intNbr == null) {
452 logger.warn("Unknown shade intensity ({})", intensity);
453 return Integer.parseInt(intensity, 16);
457 final Integer intNbr = REVERSE_INTENSITY_MAP.get(intensity);
458 if (intNbr == null) {
459 logger.warn("Unknown zone intensity ({})", intensity);
460 return Integer.parseInt(intensity, 16);
462 zoneIntensities[zone] = intNbr;
468 * Selects a specific scene on a control unit
470 * @param controlUnit the control unit
471 * @param scene the new scene
472 * @throws IllegalArgumentException if controlUnit or scene are invalid
474 void selectScene(int controlUnit, int scene) {
475 validateControlUnit(controlUnit);
476 sendCommand(CMD_SCENE + convertScene(scene) + controlUnit);
480 * Queries the interface for the current scene status on all control units
482 void refreshScene() {
483 sendCommand(CMD_SCENESTATUS);
487 * Sets the scene locked/unlocked for the specific control unit
489 * @param controlUnit the control unit
490 * @param locked true for locked, false otherwise
491 * @throws IllegalArgumentException if controlUnit is invalid
493 void setSceneLock(int controlUnit, boolean locked) {
494 validateControlUnit(controlUnit);
495 sendCommand(CMD_SCENELOCK + (locked ? "+" : "-") + controlUnit);
499 * Sets the scene sequence on/off for the specific control unit
501 * @param controlUnit the control unit
502 * @param on true for sequencing on, false otherwise
503 * @throws IllegalArgumentException if controlUnit is invalid
505 void setSceneSequence(int controlUnit, boolean on) {
506 validateControlUnit(controlUnit);
507 sendCommand(CMD_SCENESEQ + (on ? "+" : "-") + controlUnit);
511 * Sets the zone locked/unlocked for the specific control unit
513 * @param controlUnit the control unit
514 * @param locked true for locked, false otherwise
515 * @throws IllegalArgumentException if controlUnit is invalid
517 void setZoneLock(int controlUnit, boolean locked) {
518 validateControlUnit(controlUnit);
519 sendCommand(CMD_ZONELOCK + (locked ? "+" : "-") + controlUnit);
523 * Sets the zone to lowering for the specific control unit
525 * @param controlUnit the control unit
526 * @param zone the zone to lower
527 * @throws IllegalArgumentException if controlUnit or zone is invalid
529 void setZoneLower(int controlUnit, int zone) {
530 validateControlUnit(controlUnit);
532 sendCommand(CMD_ZONELOWER + controlUnit + zone);
536 * Stops the zone lowering on all control units
538 void setZoneLowerStop() {
539 sendCommand(CMD_ZONELOWERSTOP);
543 * Sets the zone to raising for the specific control unit
545 * @param controlUnit the control unit
546 * @param zone the zone to raise
547 * @throws IllegalArgumentException if controlUnit or zone is invalid
549 void setZoneRaise(int controlUnit, int zone) {
550 validateControlUnit(controlUnit);
552 sendCommand(CMD_ZONERAISE + controlUnit + zone);
556 * Stops the zone raising on all control units
558 void setZoneRaiseStop() {
559 sendCommand(CMD_ZONERAISESTOP);
563 * Sets the zone intensity up/down by 1 with the corresponding fade time on the specific zone/control unit. Does
564 * nothing if already at floor or ceiling. If the specified zone is a shade, does nothing.
566 * @param controlUnit the control unit
567 * @param zone the zone
568 * @param fade the fade time (0-59 seconds, 60-3600 seconds converted to minutes)
569 * @param increase true to increase by 1, false otherwise
570 * @throws IllegalArgumentException if controlUnit, zone or fade is invalid
572 void setZoneIntensity(int controlUnit, int zone, int fade, boolean increase) {
573 if (phCallback.isShade(controlUnit, zone)) {
577 validateControlUnit(controlUnit);
580 int newInt = zoneIntensities[zone] += (increase ? 1 : -1);
588 setZoneIntensity(controlUnit, zone, fade, newInt);
592 * Sets the zone intensity to a specific number with the corresponding fade time on the specific zone/control unit.
593 * If a shade, only deals with intensities from 0 to 5 (stop, open close, preset 1, preset 2, preset 3).
595 * @param controlUnit the control unit
596 * @param zone the zone
597 * @param fade the fade time (0-59 seconds, 60-3600 seconds converted to minutes)
598 * @param increase true to increase by 1, false otherwise
599 * @throws IllegalArgumentException if controlUnit, zone, fade or intensity is invalid
601 void setZoneIntensity(int controlUnit, int zone, int fade, int intensity) {
602 validateControlUnit(controlUnit);
605 final String hexFade = convertFade(fade);
606 final String hexIntensity = convertIntensity(controlUnit, zone, intensity);
608 final StringBuilder sb = new StringBuilder(16);
609 for (int z = 1; z <= 8; z++) {
611 sb.append(zone == z ? hexIntensity : "*");
614 sendCommand(CMD_ZONEINTENSITY + " " + controlUnit + " " + hexFade + sb);
618 * Refreshes the current zone intensities for the control unit
620 * @param controlUnit the control unit
621 * @throws IllegalArgumentException if control unit is invalid
623 void refreshZoneIntensity(int controlUnit) {
624 validateControlUnit(controlUnit);
625 sendCommand(CMD_ZONEINTENSITYSTATUS + " " + controlUnit);
629 * Sets the time on the PRG interface
631 * @param calendar a non-null calendar to set the time to
632 * @throws NullArgumentException if calendar is null
634 void setTime(Calendar calendar) {
635 if (calendar == null) {
636 throw new NullArgumentException("calendar cannot be null");
638 final String cmd = String.format("%1 %2$tk %2$tM %2$tm %2$te %2ty %3", CMD_SETTIME, calendar,
639 calendar.get(Calendar.DAY_OF_WEEK));
644 * Refreshes the time from the PRG interface
647 sendCommand(CMD_READTIME);
651 * Selects the specific schedule (0=none, 1=weekday, 2=weekend)
653 * @param schedule the new schedule
654 * @throws IllegalArgumentException if schedule is < 0 or > 32
656 void selectSchedule(int schedule) {
657 if (schedule < 0 || schedule > 2) {
658 throw new IllegalArgumentException("Schedule invalid (must be between 0 and 2): " + schedule);
660 sendCommand(CMD_SELECTSCHEDULE + " " + schedule);
664 * Refreshes the current schedule
666 void refreshSchedule() {
667 sendCommand(CMD_REPORTSCHEDULE);
671 * Refreshs the current sunrise/sunset
673 void refreshSunriseSunset() {
674 sendCommand(CMD_SUNRISESUNSET);
678 * Starts the super sequence
680 void startSuperSequence() {
681 sendCommand(CMD_SUPERSEQUENCESTART);
682 reportSuperSequenceStatus();
686 * Pauses the super sequence
688 void pauseSuperSequence() {
689 sendCommand(CMD_SUPERSEQUENCEPAUSE);
693 * Resumes the super sequence
695 void resumeSuperSequence() {
696 sendCommand(CMD_SUPERSEQUENCERESUME);
700 * Refreshes the status of the super sequence
702 void reportSuperSequenceStatus() {
703 sendCommand(CMD_SUPERSEQUENCESTATUS);
707 * Sends the command and puts the thing into {@link ThingStatus#OFFLINE} if an IOException occurs
709 * @param command a non-null, non-empty command to send
710 * @throws IllegalArgumentException if command is null or empty
712 private void sendCommand(String command) {
713 if (command == null) {
714 throw new IllegalArgumentException("command cannot be null");
716 if (command.trim().length() == 0) {
717 throw new IllegalArgumentException("command cannot be empty");
720 logger.debug("SendCommand: {}", command);
721 session.sendCommand(command);
722 } catch (IOException e) {
723 phCallback.statusChanged(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
724 "Exception occurred sending to PRG: " + e);
729 * Handles a command failure - we simply log the response as an error (trying to convert the error number to a
730 * legible error message)
732 * @param resp the possibly null, possibly empty actual response
734 private void handleCommandFailure(Matcher m, String resp) {
736 throw new IllegalArgumentException("m (matcher) cannot be null");
738 if (m.groupCount() == 2) {
740 final int errorNbr = Integer.parseInt(m.group(1));
741 String errorMsg = "ErrorCode: " + errorNbr;
744 errorMsg = "Control Unit Raise/Lower error";
748 errorMsg = "Invalid scene selected";
752 errorMsg = "Bad command was sent";
756 errorMsg = "Not a timeclock unit (GRX-ATC or GRX-PRG)";
760 errorMsg = "Illegal time was entered";
764 errorMsg = "Invalid schedule";
768 errorMsg = "No Super Sequence has been loaded";
772 errorMsg = "Command was missing Control Units";
776 errorMsg = "Command was missing data";
780 errorMsg = "Error in command argument (improper hex value)";
784 errorMsg = "Invalid Control Unit";
788 errorMsg = "Invalid value, outside range of acceptable values";
792 errorMsg = "Invalid Accessory Control";
796 errorMsg = "Network address illegally formatted; 4 octets required (xxx.xxx.xxx.xxx)";
800 errorMsg = "Time-out error, no response received";
804 errorMsg = "Invalid Telnet login number";
808 errorMsg = "Invalid Telnet login";
812 errorMsg = "Telnet login name exceeds 8 characters";
816 errorMsg = "INvalid number of arguments";
820 errorMsg = "GRX-PRG must be in programming mode for specific commands";
824 logger.error("Error response: {} ({})", errorMsg, errorNbr);
825 } catch (NumberFormatException e) {
826 logger.error("Invalid failure response (can't parse error number): '{}'", resp);
829 logger.error("Invalid failure response: '{}'", resp);
834 * Handles the scene status response
836 * @param m the non-null {@link Matcher} that matched the response
837 * @param resp the possibly null, possibly empty actual response
839 private void handleSceneStatus(Matcher m, String resp) {
841 throw new IllegalArgumentException("m (matcher) cannot be null");
843 if (m.groupCount() >= 2) {
845 final String sceneStatus = m.group(1);
846 for (int i = 1; i <= 8; i++) {
847 char status = sceneStatus.charAt(i - 1);
849 continue; // no control unit
852 int scene = VALID_SCENES.indexOf(status);
854 logger.warn("Unknown scene status returned for zone {}: {}", i, status);
856 phCallback.stateChanged(i, PrgConstants.CHANNEL_SCENE, new DecimalType(scene));
857 refreshZoneIntensity(i); // request to get new zone intensities
860 } catch (NumberFormatException e) {
861 logger.error("Invalid scene status (can't parse scene #): '{}'", resp);
864 logger.error("Invalid scene status response: '{}'", resp);
869 * Handles the report time response
871 * @param m the non-null {@link Matcher} that matched the response
872 * @param resp the possibly null, possibly empty actual response
874 private void handleReportTime(Matcher m, String resp) {
876 throw new IllegalArgumentException("m (matcher) cannot be null");
878 if (m.groupCount() == 7) {
880 final Calendar c = Calendar.getInstance();
881 c.set(Calendar.HOUR_OF_DAY, Integer.parseInt(m.group(1)));
882 c.set(Calendar.MINUTE, Integer.parseInt(m.group(2)));
883 c.set(Calendar.MONDAY, Integer.parseInt(m.group(3)));
884 c.set(Calendar.DAY_OF_MONTH, Integer.parseInt(m.group(4)));
886 final int yr = Integer.parseInt(m.group(5));
887 c.set(Calendar.YEAR, yr + (yr < 50 ? 1900 : 2000));
889 phCallback.stateChanged(PrgConstants.CHANNEL_TIMECLOCK,
890 new DateTimeType(ZonedDateTime.ofInstant(c.toInstant(), ZoneId.systemDefault())));
891 } catch (NumberFormatException e) {
892 logger.error("Invalid time response (can't parse number): '{}'", resp);
895 logger.error("Invalid time response: '{}'", resp);
900 * Handles the report schedule response
902 * @param m the non-null {@link Matcher} that matched the response
903 * @param resp the possibly null, possibly empty actual response
905 private void handleReportSchedule(Matcher m, String resp) {
907 throw new IllegalArgumentException("m (matcher) cannot be null");
909 if (m.groupCount() == 2) {
911 int schedule = Integer.parseInt(m.group(1));
912 phCallback.stateChanged(PrgConstants.CHANNEL_SCHEDULE, new DecimalType(schedule));
913 } catch (NumberFormatException e) {
914 logger.error("Invalid schedule response (can't parse number): '{}'", resp);
917 logger.error("Invalid schedule volume response: '{}'", resp);
922 * Handles the sunrise/sunset response
924 * @param m the non-null {@link Matcher} that matched the response
925 * @param resp the possibly null, possibly empty actual response
927 private void handleSunriseSunset(Matcher m, String resp) {
929 throw new IllegalArgumentException("m (matcher) cannot be null");
931 if (m.groupCount() == 5) {
932 if (m.group(1).equals("255")) {
933 logger.warn("Sunrise/Sunset needs to be enabled via Liason Software");
937 final Calendar sunrise = Calendar.getInstance();
938 sunrise.set(Calendar.HOUR_OF_DAY, Integer.parseInt(m.group(1)));
939 sunrise.set(Calendar.MINUTE, Integer.parseInt(m.group(2)));
940 phCallback.stateChanged(PrgConstants.CHANNEL_SUNRISE,
941 new DateTimeType(ZonedDateTime.ofInstant(sunrise.toInstant(), ZoneId.systemDefault())));
943 final Calendar sunset = Calendar.getInstance();
944 sunset.set(Calendar.HOUR_OF_DAY, Integer.parseInt(m.group(3)));
945 sunset.set(Calendar.MINUTE, Integer.parseInt(m.group(4)));
946 phCallback.stateChanged(PrgConstants.CHANNEL_SUNSET,
947 new DateTimeType(ZonedDateTime.ofInstant(sunset.toInstant(), ZoneId.systemDefault())));
948 } catch (NumberFormatException e) {
949 logger.error("Invalid sunrise/sunset response (can't parse number): '{}'", resp);
952 logger.error("Invalid sunrise/sunset response: '{}'", resp);
957 * Handles the super sequence response
959 * @param m the non-null {@link Matcher} that matched the response
960 * @param resp the possibly null, possibly empty actual response
962 private void handleSuperSequenceStatus(Matcher m, String resp) {
964 throw new IllegalArgumentException("m (matcher) cannot be null");
966 if (m.groupCount() == 5) {
968 final int nextStep = Integer.parseInt(m.group(2));
969 final int nextMin = Integer.parseInt(m.group(3));
970 final int nextSec = Integer.parseInt(m.group(4));
971 phCallback.stateChanged(PrgConstants.CHANNEL_SUPERSEQUENCESTATUS, new StringType(m.group(1)));
972 phCallback.stateChanged(PrgConstants.CHANNEL_SUPERSEQUENCENEXTSTEP, new DecimalType(nextStep));
973 phCallback.stateChanged(PrgConstants.CHANNEL_SUPERSEQUENCENEXTMIN, new DecimalType(nextMin));
974 phCallback.stateChanged(PrgConstants.CHANNEL_SUPERSEQUENCENEXTSEC, new DecimalType(nextSec));
975 } catch (NumberFormatException e) {
976 logger.error("Invalid volume response (can't parse number): '{}'", resp);
979 logger.error("Invalid format volume response: '{}'", resp);
984 * Handles the zone intensity response
986 * @param m the non-null {@link Matcher} that matched the response
987 * @param resp the possibly null, possibly empty actual response
989 private void handleZoneIntensity(Matcher m, String resp) {
991 throw new IllegalArgumentException("m (matcher) cannot be null");
994 if (m.groupCount() == 10) {
996 final int controlUnit = Integer.parseInt(m.group(1));
997 for (int z = 1; z <= 8; z++) {
998 final String zi = m.group(z + 1);
999 if (zi.equals("*") || zi.equals(Integer.toString(z - 1))) {
1000 continue; // not present
1002 final int zid = convertIntensity(controlUnit, z, zi);
1004 phCallback.stateChanged(controlUnit, PrgConstants.CHANNEL_ZONEINTENSITY + z, new PercentType(zid));
1006 } catch (NumberFormatException e) {
1007 logger.error("Invalid volume response (can't parse number): '{}'", resp);
1010 logger.error("Invalid format volume response: '{}'", resp);
1015 * Handles the controller information response (currently not used).
1017 * @param m the non-null {@link Matcher} that matched the response
1018 * @param resp the possibly null, possibly empty actual response
1020 private void handleControlInfo(Matcher m, String resp) {
1022 throw new IllegalArgumentException("m (matcher) cannot be null");
1024 if (m.groupCount() == 9) {
1025 int controlUnit = 0;
1027 controlUnit = Integer.parseInt(m.group(1));
1029 final String q4 = m.group(8);
1030 final String q4bits = new StringBuilder(Integer.toBinaryString(Integer.parseInt(q4, 16))).reverse()
1032 // final boolean seqType = (q4bits.length() > 0 ? q4bits.charAt(0) : '0') == '1';
1033 final boolean seqMode = (q4bits.length() > 1 ? q4bits.charAt(1) : '0') == '1';
1034 final boolean zoneLock = (q4bits.length() > 2 ? q4bits.charAt(2) : '0') == '1';
1035 final boolean sceneLock = (q4bits.length() > 3 ? q4bits.charAt(4) : '0') == '1';
1037 phCallback.stateChanged(controlUnit, PrgConstants.CHANNEL_SCENESEQ,
1038 seqMode ? OnOffType.ON : OnOffType.OFF);
1039 phCallback.stateChanged(controlUnit, PrgConstants.CHANNEL_SCENELOCK,
1040 sceneLock ? OnOffType.ON : OnOffType.OFF);
1041 phCallback.stateChanged(controlUnit, PrgConstants.CHANNEL_ZONELOCK,
1042 zoneLock ? OnOffType.ON : OnOffType.OFF);
1043 } catch (NumberFormatException e) {
1044 logger.error("Invalid controller information response: '{}'", resp);
1047 logger.error("Invalid controller information response: '{}'", resp);
1052 * Handles the interface being reset
1054 * @param m the non-null {@link Matcher} that matched the response
1055 * @param resp the possibly null, possibly empty actual response
1057 private void handleResetting(Matcher m, String resp) {
1058 phCallback.statusChanged(ThingStatus.OFFLINE, ThingStatusDetail.DUTY_CYCLE, "Device resetting");
1062 * Handles the button press response
1064 * @param m the non-null {@link Matcher} that matched the response
1065 * @param resp the possibly null, possibly empty actual response
1067 private void handleButton(Matcher m, String resp) {
1068 phCallback.stateChanged(PrgConstants.CHANNEL_BUTTONPRESS, new StringType(resp));
1072 * Handles an unknown response (simply logs it)
1074 * @param resp the possibly null, possibly empty actual response
1076 private void handleUnknownCommand(String response) {
1077 logger.info("Unhandled response: {}", response);
1081 * This callback is our normal response callback. Should be set into the {@link SocketSession} after the login
1082 * process to handle normal responses.
1084 * @author Tim Roberts
1087 private class NormalResponseCallback implements SocketSessionCallback {
1090 public void responseReceived(String response) {
1091 // logger.debug("Response received: " + response);
1093 if (response == null || response.trim().length() == 0) {
1094 return; // simple blank - do nothing
1097 Matcher m = RSP_OK.matcher(response);
1099 // logger.debug(response);
1100 return; // nothing to do on an OK! response
1103 m = RSP_FAILED.matcher(response);
1105 handleCommandFailure(m, response);
1106 return; // nothing really to do on an error response either
1109 m = RSP_SCENESTATUS.matcher(response);
1111 handleSceneStatus(m, response);
1115 m = RSP_REPORTIME.matcher(response);
1117 handleReportTime(m, response);
1121 m = RSP_REPORTSCHEDULE.matcher(response);
1123 handleReportSchedule(m, response);
1127 m = RSP_SUNRISESUNSET.matcher(response);
1129 handleSunriseSunset(m, response);
1133 m = RSP_SUPERSEQUENCESTATUS.matcher(response);
1135 handleSuperSequenceStatus(m, response);
1139 m = RSP_ZONEINTENSITY.matcher(response);
1141 handleZoneIntensity(m, response);
1145 m = RSP_RMU.matcher(response);
1147 handleControlInfo(m, response);
1151 m = RSP_RESETTING.matcher(response);
1153 handleResetting(m, response);
1157 m = RSP_BUTTON.matcher(response);
1159 handleButton(m, response);
1163 if (RSP_CONNECTION_ESTABLISHED.equals(response)) {
1164 return; // nothing to do on connection established
1167 handleUnknownCommand(response);
1171 public void responseException(Exception exception) {
1172 phCallback.statusChanged(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
1173 "Exception occurred reading from PRG: " + exception);
1178 * Special callback used during the login process to not dispatch the responses to this class but rather give them
1179 * back at each call to {@link NoDispatchingCallback#getResponse()}
1181 * @author Tim Roberts
1184 private class NoDispatchingCallback implements SocketSessionCallback {
1187 * Cache of responses that have occurred
1189 private BlockingQueue<Object> responses = new ArrayBlockingQueue<>(5);
1192 * Will return the next response from {@link #responses}. If the response is an exception, that exception will
1193 * be thrown instead.
1195 * @return a non-null, possibly empty response
1196 * @throws Exception an exception if one occurred during reading
1198 String getResponse() throws Exception {
1199 final Object lastResponse = responses.poll(5, TimeUnit.SECONDS);
1200 if (lastResponse instanceof String) {
1201 return (String) lastResponse;
1202 } else if (lastResponse instanceof Exception) {
1203 throw (Exception) lastResponse;
1204 } else if (lastResponse == null) {
1205 throw new Exception("Didn't receive response in time");
1207 return lastResponse.toString();
1212 public void responseReceived(String response) {
1214 responses.put(response);
1215 } catch (InterruptedException e) {
1220 public void responseException(Exception e) {
1223 } catch (InterruptedException e1) {