]> git.basschouten.com Git - openhab-addons.git/blob
a495ee93549d4f6d4dd572d9ba3c8f4eb12371a2
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2022 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.lcn.internal.common;
14
15 import org.eclipse.jdt.annotation.NonNullByDefault;
16 import org.openhab.binding.lcn.internal.LcnBindingConstants;
17 import org.slf4j.Logger;
18 import org.slf4j.LoggerFactory;
19
20 /**
21  * Helpers to generate LCN-PCK commands.
22  * <p>
23  * LCN-PCK is the command-syntax used by LCN-PCHK to send and receive LCN commands.
24  *
25  * @author Tobias Jüttner - Initial Contribution
26  * @author Fabian Wolter - Migration to OH2
27  */
28 @NonNullByDefault
29 public final class PckGenerator {
30     private static final Logger LOGGER = LoggerFactory.getLogger(PckGenerator.class);
31     /** Termination character after a PCK message */
32     public static final String TERMINATION = "\n";
33
34     /**
35      * Generates a keep-alive.
36      * LCN-PCHK will close the connection if it does not receive any commands from
37      * an open {@link Connection} for a specific period (10 minutes by default).
38      *
39      * @param counter the current ping's id (optional, but "best practice"). Should start with 1
40      * @return the PCK command as text
41      */
42     public static String ping(int counter) {
43         return String.format("^ping%d", counter);
44     }
45
46     /**
47      * Generates a PCK command that will set the LCN-PCHK connection's operation mode.
48      * This influences how output-port commands and status are interpreted and must be
49      * in sync with the LCN bus.
50      *
51      * @param dimMode see {@link LcnDefs.OutputPortDimMode}
52      * @param statusMode see {@link LcnDefs.OutputPortStatusMode}
53      * @return the PCK command as text
54      */
55     public static String setOperationMode(LcnDefs.OutputPortDimMode dimMode, LcnDefs.OutputPortStatusMode statusMode) {
56         return "!OM" + (dimMode == LcnDefs.OutputPortDimMode.NATIVE200 ? "1" : "0")
57                 + (statusMode == LcnDefs.OutputPortStatusMode.PERCENT ? "P" : "N");
58     }
59
60     /**
61      * Generates a PCK address header.
62      * Used for commands to LCN modules and groups.
63      *
64      * @param addr the target's address (module or group)
65      * @param localSegId the local segment id where the physical bus connection is located
66      * @param wantsAck true to claim an acknowledge / receipt from the target
67      * @return the PCK address header as text
68      */
69     public static String generateAddressHeader(LcnAddr addr, int localSegId, boolean wantsAck) {
70         return String.format(">%s%03d%03d%s", addr.isGroup() ? "G" : "M", addr.getPhysicalSegmentId(localSegId),
71                 addr.getId(), wantsAck ? "!" : ".");
72     }
73
74     /**
75      * Generates a scan-command for LCN segment-couplers.
76      * Used to detect the local segment (where the physical bus connection is located).
77      *
78      * @return the PCK command (without address header) as text
79      */
80     public static String segmentCouplerScan() {
81         return "SK";
82     }
83
84     /**
85      * Generates a firmware/serial-number request.
86      *
87      * @return the PCK command (without address header) as text
88      */
89     public static String requestSn() {
90         return "SN";
91     }
92
93     /**
94      * Generates a command to request a part of a name of a module.
95      *
96      * @param partNumber 0..1
97      * @return the PCK command (without address header) as text
98      */
99     public static String requestModuleName(int partNumber) {
100         return "NMN" + (partNumber + 1);
101     }
102
103     /**
104      * Generates an output-port status request.
105      *
106      * @param outputId 0..3
107      * @return the PCK command (without address header) as text
108      * @throws LcnException if out of range
109      */
110     public static String requestOutputStatus(int outputId) throws LcnException {
111         if (outputId < 0 || outputId > 3) {
112             throw new LcnException();
113         }
114         return String.format("SMA%d", outputId + 1);
115     }
116
117     /**
118      * Generates a dim command for a single output-port.
119      *
120      * @param outputId 0..3
121      * @param percent 0..100
122      * @param rampMs ramp in milliseconds
123      * @return the PCK command (without address header) as text
124      * @throws LcnException if out of range
125      */
126     public static String dimOutput(int outputId, double percent, int rampMs) throws LcnException {
127         if (outputId < 0 || outputId > 3) {
128             throw new LcnException();
129         }
130         int rampNative = PckGenerator.timeToRampValue(rampMs);
131         int n = (int) Math.round(percent * 2);
132         if ((n % 2) == 0) { // Use the percent command (supported by all LCN-PCHK versions)
133             return String.format("A%dDI%03d%03d", outputId + 1, n / 2, rampNative);
134         } else { // We have a ".5" value. Use the native command (supported since LCN-PCHK 2.3)
135             return String.format("O%dDI%03d%03d", outputId + 1, n, rampNative);
136         }
137     }
138
139     /**
140      * Generates a dim command for all output-ports.
141      *
142      * Attention: This command is supported since module firmware version 180501 AND LCN-PCHK 2.61
143      *
144      * @param firstPercent dimmer value of the first output 0..100
145      * @param secondPercent dimmer value of the first output 0..100
146      * @param thirdPercent dimmer value of the first output 0..100
147      * @param fourthPercent dimmer value of the first output 0..100
148      * @param rampMs ramp in milliseconds
149      * @return the PCK command (without address header) as text
150      */
151     public static String dimAllOutputs(double firstPercent, double secondPercent, double thirdPercent,
152             double fourthPercent, int rampMs) {
153         long n1 = Math.round(firstPercent * 2);
154         long n2 = Math.round(secondPercent * 2);
155         long n3 = Math.round(thirdPercent * 2);
156         long n4 = Math.round(fourthPercent * 2);
157
158         return String.format("OY%03d%03d%03d%03d%03d", n1, n2, n3, n4, timeToRampValue(rampMs));
159     }
160
161     /**
162      * Generates a control command for switching all outputs ON or OFF with a fixed ramp of 0.5s.
163      *
164      * @param percent 0..100
165      * @returnthe PCK command (without address header) as text
166      */
167     public static String controlAllOutputs(double percent) {
168         return String.format("AH%03d", Math.round(percent));
169     }
170
171     /**
172      * Generates a control command for switching dimmer output 1 and 2 both ON or OFF with a fixed ramp of 0.5s or
173      * without ramp.
174      *
175      * @param on true, if outputs shall be switched on
176      * @param ramp true, if the ramp shall be 0.5s, else 0s
177      * @return the PCK command (without address header) as text
178      */
179     public static String controlOutputs12(boolean on, boolean ramp) {
180         int commandByte;
181         if (on) {
182             commandByte = ramp ? 0xC8 : 0xFD;
183         } else {
184             commandByte = ramp ? 0x00 : 0xFC;
185         }
186         return String.format("X2%03d%03d%03d", 1, commandByte, commandByte);
187     }
188
189     /**
190      * Generates a dim command for setting the brightness of dimmer output 1 and 2 with a fixed ramp of 0.5s.
191      *
192      * @param percent brightness of both outputs 0..100
193      * @return the PCK command (without address header) as text
194      */
195     public static String dimOutputs12(double percent) {
196         long localPercent = Math.round(percent);
197         return String.format("AY%03d%03d", localPercent, localPercent);
198     }
199
200     /**
201      * Let an output flicker.
202      *
203      * @param outputId output id 0..3
204      * @param depth flicker depth, the higher the deeper 0..2
205      * @param ramp the flicker speed 0..2
206      * @param count number of flashes 1..15
207      * @return the PCK command (without address header) as text
208      * @throws LcnException when the input values are out of range
209      */
210     public static String flickerOutput(int outputId, int depth, int ramp, int count) throws LcnException {
211         if (outputId < 0 || outputId > 3) {
212             throw new LcnException("Output number out of range");
213         }
214         if (count < 1 || count > 15) {
215             throw new LcnException("Number of flashes out of range");
216         }
217         String depthString;
218         switch (depth) {
219             case 0:
220                 depthString = "G";
221                 break;
222             case 1:
223                 depthString = "M";
224                 break;
225             case 2:
226                 depthString = "S";
227                 break;
228             default:
229                 throw new LcnException("Depth out of range");
230         }
231         String rampString;
232         switch (ramp) {
233             case 0:
234                 rampString = "L";
235                 break;
236             case 1:
237                 rampString = "M";
238                 break;
239             case 2:
240                 rampString = "S";
241                 break;
242             default:
243                 throw new LcnException("Ramp out of range");
244         }
245         return String.format("A%dFL%s%s%02d", outputId + 1, depthString, rampString, count);
246     }
247
248     /**
249      * Generates a command to change the value of an output-port.
250      *
251      * @param outputId 0..3
252      * @param percent -100..100
253      * @return the PCK command (without address header) as text
254      * @throws LcnException if out of range
255      */
256     public static String relOutput(int outputId, double percent) throws LcnException {
257         if (outputId < 0 || outputId > 3) {
258             throw new LcnException();
259         }
260         int n = (int) Math.round(percent * 2);
261         if ((n % 2) == 0) { // Use the percent command (supported by all LCN-PCHK versions)
262             return String.format("A%d%s%03d", outputId + 1, percent >= 0 ? "AD" : "SB", Math.abs(n / 2));
263         } else { // We have a ".5" value. Use the native command (supported since LCN-PCHK 2.3)
264             return String.format("O%d%s%03d", outputId + 1, percent >= 0 ? "AD" : "SB", Math.abs(n));
265         }
266     }
267
268     /**
269      * Generates a command that toggles a single output-port (on->off, off->on).
270      *
271      * @param outputId 0..3
272      * @param ramp see {@link PckGenerator#timeToRampValue(int)}
273      * @return the PCK command (without address header) as text
274      * @throws LcnException if out of range
275      */
276     public static String toggleOutput(int outputId, int ramp) throws LcnException {
277         if (outputId < 0 || outputId > 3) {
278             throw new LcnException();
279         }
280         return String.format("A%dTA%03d", outputId + 1, ramp);
281     }
282
283     /**
284      * Generates a command that toggles all output-ports (on->off, off->on).
285      *
286      * @param ramp see {@link PckGenerator#timeToRampValue(int)}
287      * @return the PCK command (without address header) as text
288      */
289     public static String toggleAllOutputs(int ramp) {
290         return String.format("AU%03d", ramp);
291     }
292
293     /**
294      * Generates a relays-status request.
295      *
296      * @return the PCK command (without address header) as text
297      */
298     public static String requestRelaysStatus() {
299         return "SMR";
300     }
301
302     /**
303      * Generates a command to control relays.
304      *
305      * @param states the 8 modifiers for the relay states
306      * @return the PCK command (without address header) as text
307      * @throws LcnException if out of range
308      */
309     public static String controlRelays(LcnDefs.RelayStateModifier[] states) throws LcnException {
310         if (states.length != 8) {
311             throw new LcnException();
312         }
313         StringBuilder ret = new StringBuilder("R8");
314         for (int i = 0; i < 8; ++i) {
315             switch (states[i]) {
316                 case ON:
317                     ret.append("1");
318                     break;
319                 case OFF:
320                     ret.append("0");
321                     break;
322                 case TOGGLE:
323                     ret.append("U");
324                     break;
325                 case NOCHANGE:
326                     ret.append("-");
327                     break;
328                 default:
329                     throw new LcnException();
330             }
331         }
332         return ret.toString();
333     }
334
335     /**
336      * Generates a binary-sensors status request.
337      *
338      * @return the PCK command (without address header) as text
339      */
340     public static String requestBinSensorsStatus() {
341         return "SMB";
342     }
343
344     /**
345      * Generates a command that sets a variable absolute.
346      *
347      * @param number regulator number 0..1
348      * @param value the absolute value to set
349      * @return the PCK command (without address header) as text
350      * @throws LcnException
351      */
352     public static String setSetpointAbsolute(int number, int value) {
353         int internalValue = value;
354         // Set absolute (not in PCK yet)
355         int b1 = number << 6; // 01000000
356         b1 |= 0x20; // xx10xxxx (set absolute)
357         if (value < 1000) {
358             internalValue = 1000 - internalValue;
359             b1 |= 8;
360         } else {
361             internalValue -= 1000;
362         }
363         b1 |= (internalValue >> 8) & 0x0f; // xxxx1111
364         int b2 = internalValue & 0xff;
365         return String.format("X2%03d%03d%03d", 30, b1, b2);
366     }
367
368     /**
369      * Generates a command to change the value of a variable.
370      *
371      * @param variable the target variable to change
372      * @param type the reference-point
373      * @param value the native LCN value to add/subtract (can be negative)
374      * @return the PCK command (without address header) as text
375      * @throws LcnException if command is not supported
376      */
377     public static String setVariableRelative(Variable variable, LcnDefs.RelVarRef type, int value) {
378         if (variable.getNumber() == 0) {
379             // Old command for variable 1 / T-var (compatible with all modules)
380             return String.format("Z%s%d", value >= 0 ? "A" : "S", Math.abs(value));
381         } else { // New command for variable 1-12 (compatible with all modules, since LCN-PCHK 2.8)
382             return String.format("Z%s%03d%d", value >= 0 ? "+" : "-", variable.getNumber() + 1, Math.abs(value));
383         }
384     }
385
386     /**
387      * Generates a command the change the value of a regulator setpoint relative.
388      *
389      * @param number 0..1
390      * @param type relative to the current or to the programmed value
391      * @param value the relative value -4000..+4000
392      * @return the PCK command (without address header) as text
393      */
394     public static String setSetpointRelative(int number, LcnDefs.RelVarRef type, int value) {
395         return String.format("RE%sS%s%s%d", number == 0 ? "A" : "B", type == LcnDefs.RelVarRef.CURRENT ? "A" : "P",
396                 value >= 0 ? "+" : "-", Math.abs(value));
397     }
398
399     /**
400      * Generates a command the change the value of a threshold relative.
401      *
402      * @param variable the threshold to change
403      * @param type relative to the current or to the programmed value
404      * @param value the relative value -4000..+4000
405      * @param is2013 true, if the LCN module's firmware is equal to or newer than 2013
406      * @return the PCK command (without address header) as text
407      */
408     public static String setThresholdRelative(Variable variable, LcnDefs.RelVarRef type, int value, boolean is2013)
409             throws LcnException {
410         if (is2013) { // New command for registers 1-4 (since 170206, LCN-PCHK 2.8)
411             return String.format("SS%s%04d%sR%d%d", type == LcnDefs.RelVarRef.CURRENT ? "R" : "E", Math.abs(value),
412                     value >= 0 ? "A" : "S", variable.getNumber() + 1, variable.getThresholdNumber().get() + 1);
413         } else if (variable.getNumber() == 0) { // Old command for register 1 (before 170206)
414             return String.format("SS%s%04d%s%s%s%s%s%s", type == LcnDefs.RelVarRef.CURRENT ? "R" : "E", Math.abs(value),
415                     value >= 0 ? "A" : "S", variable.getThresholdNumber().get() == 0 ? "1" : "0",
416                     variable.getThresholdNumber().get() == 1 ? "1" : "0",
417                     variable.getThresholdNumber().get() == 2 ? "1" : "0",
418                     variable.getThresholdNumber().get() == 3 ? "1" : "0",
419                     variable.getThresholdNumber().get() == 4 ? "1" : "0");
420         } else {
421             throw new LcnException(
422                     "Module does not have threshold register " + (variable.getThresholdNumber().get() + 1));
423         }
424     }
425
426     /**
427      * Generates a variable value request.
428      *
429      * @param variable the variable to request
430      * @param firmwareVersion the target module's firmware version
431      * @return the PCK command (without address header) as text
432      * @throws LcnException if command is not supported
433      */
434     public static String requestVarStatus(Variable variable, int firmwareVersion) throws LcnException {
435         if (firmwareVersion >= LcnBindingConstants.FIRMWARE_2013) {
436             int id = variable.getNumber();
437             switch (variable.getType()) {
438                 case UNKNOWN:
439                     throw new LcnException("Variable unknown");
440                 case VARIABLE:
441                     return String.format("MWT%03d", id + 1);
442                 case REGULATOR:
443                     return String.format("MWS%03d", id + 1);
444                 case THRESHOLD:
445                     return String.format("SE%03d", id + 1); // Whole register
446                 case S0INPUT:
447                     return String.format("MWC%03d", id + 1);
448             }
449             throw new LcnException("Unsupported variable type: " + variable);
450         } else {
451             switch (variable) {
452                 case VARIABLE1:
453                     return "MWV";
454                 case VARIABLE2:
455                     return "MWTA";
456                 case VARIABLE3:
457                     return "MWTB";
458                 case RVARSETPOINT1:
459                     return "MWSA";
460                 case RVARSETPOINT2:
461                     return "MWSB";
462                 case THRESHOLDREGISTER11:
463                 case THRESHOLDREGISTER12:
464                 case THRESHOLDREGISTER13:
465                 case THRESHOLDREGISTER14:
466                 case THRESHOLDREGISTER15:
467                     return "SL1"; // Whole register
468                 default:
469                     throw new LcnException("Unsupported variable type: " + variable);
470             }
471         }
472     }
473
474     /**
475      * Generates a request for LED and logic-operations states.
476      *
477      * @return the PCK command (without address header) as text
478      */
479     public static String requestLedsAndLogicOpsStatus() {
480         return "SMT";
481     }
482
483     /**
484      * Generates a command to the set the state of a single LED.
485      *
486      * @param ledId 0..11
487      * @param state the state to set
488      * @return the PCK command (without address header) as text
489      * @throws LcnException if out of range
490      */
491     public static String controlLed(int ledId, LcnDefs.LedStatus state) throws LcnException {
492         if (ledId < 0 || ledId > 11) {
493             throw new LcnException();
494         }
495         return String.format("LA%03d%s", ledId + 1, state == LcnDefs.LedStatus.OFF ? "A"
496                 : state == LcnDefs.LedStatus.ON ? "E" : state == LcnDefs.LedStatus.BLINK ? "B" : "F");
497     }
498
499     /**
500      * Generates a command to send LCN keys.
501      *
502      * @param cmds the 4 concrete commands to send for the tables (A-D)
503      * @param keys the tables' 8 key-states (true means "send")
504      * @return the PCK command (without address header) as text
505      * @throws LcnException if out of range
506      */
507     public static String sendKeys(LcnDefs.SendKeyCommand[] cmds, boolean[] keys) throws LcnException {
508         if (cmds.length != 4 || keys.length != 8) {
509             throw new LcnException();
510         }
511         StringBuilder ret = new StringBuilder("TS");
512         for (int i = 0; i < 4; ++i) {
513             switch (cmds[i]) {
514                 case HIT:
515                     ret.append("K");
516                     break;
517                 case MAKE:
518                     ret.append("L");
519                     break;
520                 case BREAK:
521                     ret.append("O");
522                     break;
523                 case DONTSEND:
524                     // By skipping table D (if it is not used), we use the old command
525                     // for table A-C which is compatible with older LCN modules
526                     if (i < 3) {
527                         ret.append("-");
528                     }
529                     break;
530                 default:
531                     throw new LcnException();
532             }
533         }
534         for (int i = 0; i < 8; ++i) {
535             ret.append(keys[i] ? "1" : "0");
536         }
537         return ret.toString();
538     }
539
540     /**
541      * Generates a command to send LCN keys deferred / delayed.
542      *
543      * @param tableId 0(A)..3(D)
544      * @param time the delay time
545      * @param timeUnit the time unit
546      * @param keys the key-states (true means "send")
547      * @return the PCK command (without address header) as text
548      * @throws LcnException if out of range
549      */
550     public static String sendKeysHitDefered(int tableId, int time, LcnDefs.TimeUnit timeUnit, boolean[] keys)
551             throws LcnException {
552         if (tableId < 0 || tableId > 3 || keys.length != 8) {
553             throw new LcnException();
554         }
555         StringBuilder ret = new StringBuilder("TV");
556         switch (tableId) {
557             case 0:
558                 ret.append("A");
559                 break;
560             case 1:
561                 ret.append("B");
562                 break;
563             case 2:
564                 ret.append("C");
565                 break;
566             case 3:
567                 ret.append("D");
568                 break;
569             default:
570                 throw new LcnException();
571         }
572         ret.append(String.format("%03d", time));
573         switch (timeUnit) {
574             case SECONDS:
575                 if (time < 1 || time > 60) {
576                     throw new LcnException();
577                 }
578                 ret.append("S");
579                 break;
580             case MINUTES:
581                 if (time < 1 || time > 90) {
582                     throw new LcnException();
583                 }
584                 ret.append("M");
585                 break;
586             case HOURS:
587                 if (time < 1 || time > 50) {
588                     throw new LcnException();
589                 }
590                 ret.append("H");
591                 break;
592             case DAYS:
593                 if (time < 1 || time > 45) {
594                     throw new LcnException();
595                 }
596                 ret.append("D");
597                 break;
598             default:
599                 throw new LcnException();
600         }
601         for (int i = 0; i < 8; ++i) {
602             ret.append(keys[i] ? "1" : "0");
603         }
604         return ret.toString();
605     }
606
607     /**
608      * Generates a request for key-lock states.
609      * Always requests table A-D. Supported since LCN-PCHK 2.8.
610      *
611      * @return the PCK command (without address header) as text
612      */
613     public static String requestKeyLocksStatus() {
614         return "STX";
615     }
616
617     /**
618      * Generates a command to lock keys.
619      *
620      * @param tableId 0(A)..3(D)
621      * @param states the 8 key-lock modifiers
622      * @return the PCK command (without address header) as text
623      * @throws LcnException if out of range
624      */
625     public static String lockKeys(int tableId, LcnDefs.KeyLockStateModifier[] states) throws LcnException {
626         if (tableId < 0 || tableId > 3 || states.length != 8) {
627             throw new LcnException();
628         }
629         StringBuilder ret = new StringBuilder(
630                 String.format("TX%s", tableId == 0 ? "A" : tableId == 1 ? "B" : tableId == 2 ? "C" : "D"));
631         for (int i = 0; i < 8; ++i) {
632             switch (states[i]) {
633                 case ON:
634                     ret.append("1");
635                     break;
636                 case OFF:
637                     ret.append("0");
638                     break;
639                 case TOGGLE:
640                     ret.append("U");
641                     break;
642                 case NOCHANGE:
643                     ret.append("-");
644                     break;
645                 default:
646                     throw new LcnException();
647             }
648         }
649         return ret.toString();
650     }
651
652     /**
653      * Generates a command to lock keys for table A temporary.
654      * There is no hardware-support for locking tables B-D.
655      *
656      * @param time the lock time
657      * @param timeUnit the time unit
658      * @param keys the 8 key-lock states (true means lock)
659      * @return the PCK command (without address header) as text
660      * @throws LcnException if out of range
661      */
662     public static String lockKeyTabATemporary(int time, LcnDefs.TimeUnit timeUnit, boolean[] keys) throws LcnException {
663         if (keys.length != 8) {
664             throw new LcnException();
665         }
666         StringBuilder ret = new StringBuilder(String.format("TXZA%03d", time));
667         switch (timeUnit) {
668             case SECONDS:
669                 if (time < 1 || time > 60) {
670                     throw new LcnException();
671                 }
672                 ret.append("S");
673                 break;
674             case MINUTES:
675                 if (time < 1 || time > 90) {
676                     throw new LcnException();
677                 }
678                 ret.append("M");
679                 break;
680             case HOURS:
681                 if (time < 1 || time > 50) {
682                     throw new LcnException();
683                 }
684                 ret.append("H");
685                 break;
686             case DAYS:
687                 if (time < 1 || time > 45) {
688                     throw new LcnException();
689                 }
690                 ret.append("D");
691                 break;
692             default:
693                 throw new LcnException();
694         }
695         for (int i = 0; i < 8; ++i) {
696             ret.append(keys[i] ? "1" : "0");
697         }
698         return ret.toString();
699     }
700
701     /**
702      * Generates the command header / start for sending dynamic texts.
703      * Used by LCN-GTxD periphery (supports 4 text rows).
704      * To complete the command, the text to send must be appended (UTF-8 encoding).
705      * Texts are split up into up to 5 parts with 12 "UTF-8 bytes" each.
706      *
707      * @param row 0..3
708      * @param part 0..4
709      * @return the PCK command (without address header) as text
710      * @throws LcnException if out of range
711      */
712     public static String dynTextHeader(int row, int part) throws LcnException {
713         if (row < 0 || row > 3 || part < 0 || part > 4) {
714             throw new LcnException("Row number is out of range: " + (row + 1));
715         }
716         return String.format("GTDT%d%d", row + 1, part + 1);
717     }
718
719     /**
720      * Generates a command to lock a regulator.
721      *
722      * @param regId 0..1
723      * @param state the lock state
724      * @return the PCK command (without address header) as text
725      * @throws LcnException if out of range
726      */
727     public static String lockRegulator(int regId, boolean state) throws LcnException {
728         if (regId < 0 || regId > 1) {
729             throw new LcnException();
730         }
731         return String.format("RE%sX%s", regId == 0 ? "A" : "B", state ? "S" : "A");
732     }
733
734     /**
735      * Generates a command to start a relay timer
736      *
737      * @param relayNumber number of relay (1..8)
738      * @param duration duration in milliseconds
739      * @return the PCK command (without address header) as text
740      * @throws LcnException if out of range
741      */
742     public static String startRelayTimer(int relayNumber, double duration) throws LcnException {
743         if (relayNumber < 1 || relayNumber > 8 || duration < 30 || duration > 240960) {
744             throw new LcnException();
745         }
746         StringBuilder command = new StringBuilder("R8T");
747         command.append(String.format("%03d", convertMsecToLCNTimer(duration)));
748         StringBuilder data = new StringBuilder("--------");
749         data.setCharAt(relayNumber - 1, '1');
750         command.append(data);
751         return command.toString();
752     }
753
754     /**
755      * Generates a null command, used for broadcast messages.
756      *
757      * @return the PCK command (without address header) as text
758      */
759     public static String nullCommand() {
760         return "LEER";
761     }
762
763     /**
764      * Converts the given time into an LCN ramp value.
765      *
766      * @param timeMSec the time in milliseconds
767      * @return the (LCN-internal) ramp value (0..250)
768      */
769     private static int timeToRampValue(int timeMSec) {
770         int ret;
771         if (timeMSec < 250) {
772             ret = 0;
773         } else if (timeMSec < 500) {
774             ret = 1;
775         } else if (timeMSec < 660) {
776             ret = 2;
777         } else if (timeMSec < 1000) {
778             ret = 3;
779         } else if (timeMSec < 1400) {
780             ret = 4;
781         } else if (timeMSec < 2000) {
782             ret = 5;
783         } else if (timeMSec < 3000) {
784             ret = 6;
785         } else if (timeMSec < 4000) {
786             ret = 7;
787         } else if (timeMSec < 5000) {
788             ret = 8;
789         } else if (timeMSec < 6000) {
790             ret = 9;
791         } else {
792             ret = (timeMSec / 1000 - 6) / 2 + 10;
793             if (ret > 250) {
794                 ret = 250;
795                 LOGGER.warn("Ramp value is too high. Limiting value to 486s.");
796             }
797         }
798         return ret;
799     }
800
801     /**
802      * Converts duration in milliseconds to lcntimer value
803      * Source: https://www.symcon.de/forum/threads/38603-LCN-Relais-Kurzzeit-Timer-umrechnen
804      *
805      * @param ms time in milliseconds
806      * @return lcn timer value
807      */
808     private static int convertMsecToLCNTimer(double ms) {
809         Integer lcntimer = -1;
810         if (ms >= 0 && ms <= 240960) {
811             double a = ms / 1000 / 0.03;
812             double b = (a / 32.0) + 1.0;
813             double c = Math.log(b) / Math.log(2);
814             double mod = Math.floor(c);
815             double faktor = 32 * (Math.pow(2, mod) - 1);
816             double offset = (a - faktor) / Math.pow(2, mod);
817             lcntimer = (int) (offset + mod * 32);
818         } else {
819             LOGGER.warn("Timer not in [0,240960] ms");
820         }
821         return lcntimer;
822     }
823 }