]> git.basschouten.com Git - openhab-addons.git/blob
e885112aced50dfcfea9976c49dc2965953b87c7
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 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.velux.internal.things;
14
15 import java.util.regex.Pattern;
16
17 import org.eclipse.jdt.annotation.NonNullByDefault;
18 import org.eclipse.jdt.annotation.Nullable;
19 import org.openhab.binding.velux.internal.bridge.slip.FunctionalParameters;
20 import org.openhab.binding.velux.internal.things.VeluxKLFAPI.Command;
21 import org.openhab.binding.velux.internal.things.VeluxProductType.ActuatorType;
22 import org.slf4j.Logger;
23 import org.slf4j.LoggerFactory;
24
25 /**
26  * <B>Velux</B> product representation.
27  * <P>
28  * Combined set of information describing a single Velux product.
29  *
30  * @author Guenther Schreiner - initial contribution.
31  */
32 @NonNullByDefault
33 public class VeluxProduct {
34     private final Logger logger = LoggerFactory.getLogger(VeluxProduct.class);
35
36     // Public definition
37
38     public static final VeluxProduct UNKNOWN = new VeluxProduct();
39
40     // Type definitions
41
42     public static class ProductBridgeIndex {
43
44         // Public definition
45         public static final ProductBridgeIndex UNKNOWN = new ProductBridgeIndex(0);
46
47         // Class internal
48         private int id;
49
50         // Constructor
51         public ProductBridgeIndex(int id) {
52             this.id = id;
53         }
54
55         // Class access methods
56         public int toInt() {
57             return id;
58         }
59
60         @Override
61         public String toString() {
62             return Integer.toString(id);
63         }
64     }
65
66     /**
67      * State (of movement) of an actuator product.
68      *
69      * @author AndrewFG - Initial contribution.
70      */
71     public enum ProductState {
72         NON_EXECUTING(0),
73         ERROR(1),
74         NOT_USED(2),
75         WAITING_FOR_POWER(3),
76         EXECUTING(4),
77         DONE(5),
78         UNKNOWN(0xFF),
79         MANUAL(0b10000000);
80
81         private static final int ACTION_MASK = 0b111;
82         private static final int EQUIVALENT_MASK = ACTION_MASK | MANUAL.value;
83
84         public final int value;
85
86         private ProductState(int value) {
87             this.value = value;
88         }
89
90         /**
91          * Create a ProductState from an integer seed value.
92          *
93          * @param value the seed value.
94          * @return the ProductState.
95          */
96         public static ProductState of(int value) {
97             if ((value < NON_EXECUTING.value) || (value > UNKNOWN.value)) {
98                 return ERROR;
99             }
100             if (value == UNKNOWN.value) {
101                 return UNKNOWN;
102             }
103             if ((value & MANUAL.value) != 0) {
104                 return MANUAL;
105             }
106             int masked = value & ACTION_MASK;
107             for (ProductState state : values()) {
108                 if (state.value > DONE.value) {
109                     break;
110                 }
111                 if (masked == state.value) {
112                     return state;
113                 }
114             }
115             return ERROR;
116         }
117
118         /**
119          * Test if the masked values of two state values are operationally equivalent, including if both values are
120          * 'unknown' (0xFF).
121          *
122          * @param a first value to compare
123          * @param b second value to compare
124          * @return true if the masked values are equivalent.
125          */
126         public static boolean equivalent(int a, int b) {
127             return (a & EQUIVALENT_MASK) == (b & EQUIVALENT_MASK);
128         }
129     }
130
131     // pattern to match a Velux serial number '00:00:00:00:00:00:00:00'
132     private static final Pattern VELUX_SERIAL_NUMBER = Pattern.compile(
133             "^[A-Fa-f0-9]{2}:[A-Fa-f0-9]{2}:[A-Fa-f0-9]{2}:[A-Fa-f0-9]{2}:[A-Fa-f0-9]{2}:[A-Fa-f0-9]{2}:[A-Fa-f0-9]{2}:[A-Fa-f0-9]{2}$");
134
135     /**
136      * Indicates the data source where the product's contents came from.
137      *
138      * @author AndrewFG - Initial contribution.
139      */
140     public enum DataSource {
141         GATEWAY,
142         BINDING
143     }
144
145     // Class internal
146
147     private VeluxProductName name;
148     private VeluxProductType typeId;
149     private ActuatorType actuatorType;
150     private ProductBridgeIndex bridgeProductIndex;
151
152     private boolean v2 = false;
153     private int order = 0;
154     private int placement = 0;
155     private int velocity = 0;
156     private int variation = 0;
157     private int powerMode = 0;
158     private String serialNumber = VeluxProductSerialNo.UNKNOWN;
159     private int state = ProductState.UNKNOWN.value;
160     private int currentPosition = 0;
161     private int targetPosition = 0;
162     private @Nullable FunctionalParameters functionalParameters = null;
163     private int remainingTime = 0;
164     private int timeStamp = 0;
165     private Command creatorCommand = Command.UNDEFTYPE;
166     private DataSource dataSource = DataSource.GATEWAY;
167     private boolean isSomfyProduct;
168
169     // Constructor
170
171     /**
172      * Constructor
173      *
174      * just for the dummy VeluxProduct.
175      */
176     public VeluxProduct() {
177         logger.trace("VeluxProduct() created.");
178         this.name = VeluxProductName.UNKNOWN;
179         this.typeId = VeluxProductType.UNDEFTYPE;
180         this.bridgeProductIndex = ProductBridgeIndex.UNKNOWN;
181         this.actuatorType = ActuatorType.UNDEFTYPE;
182         this.isSomfyProduct = false;
183     }
184
185     /**
186      * Constructor
187      *
188      * @param name This field Name holds the name of the actuator, ex. “Window 1”. This field is 64 bytes
189      *            long, formatted as UTF-8 characters.
190      * @param typeId This field indicates the node type, ex. Window, Roller shutter, Light etc.
191      * @param bridgeProductIndex NodeID is an Actuator index in the system table, to get information from. It must be a
192      *            value from 0 to 199.
193      */
194     public VeluxProduct(VeluxProductName name, VeluxProductType typeId, ProductBridgeIndex bridgeProductIndex) {
195         logger.trace("VeluxProduct(v1,name={}) created.", name);
196         this.name = name;
197         this.typeId = typeId;
198         this.bridgeProductIndex = bridgeProductIndex;
199         this.actuatorType = ActuatorType.WINDOW_4_0;
200         this.isSomfyProduct = false;
201     }
202
203     /**
204      * Constructor
205      *
206      * @param name This field Name holds the name of the actuator, ex. “Window 1”. This field is 64 bytes
207      *            long, formatted as UTF-8 characters.
208      * @param typeId This field indicates the node type, ex. Window, Roller shutter, Light etc.
209      * @param bridgeProductIndex NodeID is an Actuator index in the system table, to get information from. It must be a
210      *            value from 0 to 199.
211      * @param order Order can be used to store a sort order. The sort order is used in client end, when
212      *            presenting a list of nodes for the user.
213      * @param placement Placement can be used to store a room group index or house group index number.
214      * @param velocity This field indicates what velocity the node is operation with.
215      * @param variation More detail information like top hung, kip, flat roof or sky light window.
216      * @param powerMode This field indicates the power mode of the node (ALWAYS_ALIVE/LOW_POWER_MODE).
217      * @param serialNumber This field tells the serial number of the node. This field is 8 bytes.
218      * @param state This field indicates the operating state of the node.
219      * @param currentPosition This field indicates the current position of the node.
220      * @param target This field indicates the target position of the current operation.
221      * @param functionalParameters the target Functional Parameters (may be null).
222      * @param remainingTime This field indicates the remaining time for a node activation in seconds.
223      * @param timeStamp UTC time stamp for last known position.
224      * @param creatorCommand the API command that caused this instance to be created.
225      */
226     public VeluxProduct(VeluxProductName name, VeluxProductType typeId, ActuatorType actuatorType,
227             ProductBridgeIndex bridgeProductIndex, int order, int placement, int velocity, int variation, int powerMode,
228             String serialNumber, int state, int currentPosition, int target,
229             @Nullable FunctionalParameters functionalParameters, int remainingTime, int timeStamp,
230             Command creatorCommand) {
231         logger.trace("VeluxProduct(v2, name={}) created.", name);
232         this.name = name;
233         this.typeId = typeId;
234         this.actuatorType = actuatorType;
235         this.bridgeProductIndex = bridgeProductIndex;
236         this.v2 = true;
237         this.order = order;
238         this.placement = placement;
239         this.velocity = velocity;
240         this.variation = variation;
241         this.powerMode = powerMode;
242         this.serialNumber = serialNumber;
243         this.state = state;
244         this.currentPosition = currentPosition;
245         this.targetPosition = target;
246         this.functionalParameters = functionalParameters;
247         this.remainingTime = remainingTime;
248         this.timeStamp = timeStamp;
249         this.creatorCommand = creatorCommand;
250
251         // isSomfyProduct is true if serial number not matching the '00:00:00:00:00:00:00:00' pattern
252         this.isSomfyProduct = !VELUX_SERIAL_NUMBER.matcher(serialNumber).find();
253     }
254
255     /**
256      * Constructor for a 'notification' product. Such products are used as data transfer objects to carry the limited
257      * sub
258      * set of data fields which are returned by 'GW_STATUS_REQUEST_NTF' or 'GW_NODE_STATE_POSITION_CHANGED_NTF'
259      * notifications, and to transfer those respective field values to another product that had already been created via
260      * a 'GW_GET_NODE_INFORMATION_NTF' notification, with all the other fields already filled.
261      *
262      * @param name the name of the notification command that created the product.
263      * @param bridgeProductIndex the product bridge index from the notification.
264      * @param state the actuator state from the notification.
265      * @param currentPosition the current actuator position from the notification.
266      * @param target the target position from the notification (may be VeluxProductPosition.VPP_VELUX_IGNORE).
267      * @param functionalParameters the actuator functional parameters (may be null).
268      * @param creatorCommand the API command that caused this instance to be created.
269      */
270     public VeluxProduct(VeluxProductName name, ProductBridgeIndex bridgeProductIndex, int state, int currentPosition,
271             int target, @Nullable FunctionalParameters functionalParameters, Command creatorCommand) {
272         logger.trace("VeluxProduct(v2, name={}) [notification product] created.", name);
273         this.v2 = true;
274         this.typeId = VeluxProductType.UNDEFTYPE;
275         this.actuatorType = ActuatorType.UNDEFTYPE;
276         this.name = name;
277         this.bridgeProductIndex = bridgeProductIndex;
278         this.state = state;
279         this.currentPosition = currentPosition;
280         this.targetPosition = target;
281         this.functionalParameters = functionalParameters;
282         this.isSomfyProduct = false;
283         this.creatorCommand = creatorCommand;
284     }
285
286     // Utility methods
287
288     @Override
289     public VeluxProduct clone() {
290         if (this.v2) {
291             FunctionalParameters functionalParameters = this.functionalParameters;
292             return new VeluxProduct(name, typeId, actuatorType, bridgeProductIndex, order, placement, velocity,
293                     variation, powerMode, serialNumber, state, currentPosition, targetPosition,
294                     functionalParameters == null ? null : functionalParameters.clone(), remainingTime, timeStamp,
295                     creatorCommand);
296         } else {
297             return new VeluxProduct(name, typeId, bridgeProductIndex);
298         }
299     }
300
301     // Class access methods
302
303     /**
304      * Returns the name of the current product (aka actuator) for convenience as type-specific class.
305      *
306      * @return nameOfThisProduct as type {@link VeluxProductName}.
307      */
308     public VeluxProductName getProductName() {
309         return this.name;
310     }
311
312     /**
313      * Returns the type of the current product (aka actuator) for convenience as type-specific class.
314      *
315      * @return typeOfThisProduct as type {@link VeluxProductType}.
316      */
317     public VeluxProductType getProductType() {
318         return this.typeId;
319     }
320
321     public ProductBridgeIndex getBridgeProductIndex() {
322         return this.bridgeProductIndex;
323     }
324
325     @Override
326     public String toString() {
327         if (this.v2) {
328             FunctionalParameters functionalParameters = this.functionalParameters;
329             return String.format(
330                     "VeluxProduct(v2, creator:%s, dataSource:%s, name:%s, typeId:%s, bridgeIndex:%d, state:%d, serial:%s, position:%04X, target:%04X, functionalParameters:%s)",
331                     creatorCommand.name(), dataSource.name(), name, typeId, bridgeProductIndex.toInt(), state,
332                     serialNumber, currentPosition, targetPosition,
333                     functionalParameters == null ? "null" : functionalParameters.toString());
334         } else {
335             return String.format("VeluxProduct(v1, name:%s, typeId:%s, bridgeIndex:%d)", name, typeId,
336                     bridgeProductIndex.toInt());
337         }
338     }
339
340     // Class helper methods
341
342     /**
343      * Return the product unique index.
344      * Either the serial number (for normal Velux devices), or its name (for e.g. Somfy devices).
345      *
346      * @return the serial number or its name
347      */
348     public String getProductUniqueIndex() {
349         if (!v2 || serialNumber.startsWith(VeluxProductSerialNo.UNKNOWN)) {
350             return name.toString();
351         }
352         return VeluxProductSerialNo.cleaned(serialNumber);
353     }
354
355     // Getter and Setter methods
356
357     /**
358      * @return <b>v2</b> as type boolean signals the availability of firmware version two (product) details.
359      */
360     public boolean isV2() {
361         return v2;
362     }
363
364     /**
365      * @return <b>order</b> as type int describes the user-oriented sort-order.
366      */
367     public int getOrder() {
368         return order;
369     }
370
371     /**
372      * @return <B>placement</B> as type int is used to describe a group index or house group index number.
373      */
374     public int getPlacement() {
375         return placement;
376     }
377
378     /**
379      * @return <B>velocity</B> as type int describes what velocity the node is operation with
380      */
381     public int getVelocity() {
382         return velocity;
383     }
384
385     /**
386      * @return <B>variation</B> as type int describes detail information like top hung, kip, flat roof or sky light
387      *         window.
388      */
389     public int getVariation() {
390         return variation;
391     }
392
393     /**
394      * @return <B>powerMode</B> as type int is used to show the power mode of the node (ALWAYS_ALIVE/LOW_POWER_MODE).
395      */
396     public int getPowerMode() {
397         return powerMode;
398     }
399
400     /**
401      * @return <B>serialNumber</B> as type String is the serial number of 8 bytes length of the node.
402      */
403     public String getSerialNumber() {
404         return serialNumber;
405     }
406
407     /**
408      * @return <B>state</B> as type int is used to operating state of the node.
409      */
410     public int getState() {
411         return state;
412     }
413
414     /**
415      * Get the actuator state.
416      *
417      * @return state cast to an ActuatorState enum.
418      */
419     public ProductState getProductState() {
420         return ProductState.of(state);
421     }
422
423     /**
424      * @param newState Update the operating state of the node.
425      * @return <B>modified</B> as type boolean to signal a real modification.
426      */
427     public boolean setState(int newState) {
428         if (this.state == newState) {
429             return false;
430         } else {
431             logger.trace("setState(name={},index={}) state {} replaced by {}.", name, bridgeProductIndex, state,
432                     newState);
433             this.state = newState;
434             return true;
435         }
436     }
437
438     /**
439      * @return <B>currentPosition</B> as type int signals the current position of the node.
440      */
441     public int getCurrentPosition() {
442         return currentPosition;
443     }
444
445     /**
446      * @param newCurrentPosition Update the current position of the node.
447      * @return <B>modified</B> as boolean to signal a real modification.
448      */
449     public boolean setCurrentPosition(int newCurrentPosition) {
450         if (this.currentPosition == newCurrentPosition) {
451             return false;
452         } else {
453             logger.trace("setCurrentPosition(name={},index={}) currentPosition {} replaced by {}.", name,
454                     bridgeProductIndex, currentPosition, newCurrentPosition);
455             this.currentPosition = newCurrentPosition;
456             return true;
457         }
458     }
459
460     /**
461      * @return <b>target</b> as type int shows the target position of the current operation.
462      */
463     public int getTarget() {
464         return targetPosition;
465     }
466
467     /**
468      * @param newTarget Update the target position of the current operation.
469      * @return <b>modified</b> as boolean to signal a real modification.
470      */
471     public boolean setTarget(int newTarget) {
472         if (this.targetPosition == newTarget) {
473             return false;
474         } else {
475             logger.trace("setTarget(name={},index={}) target {} replaced by {}.", name, bridgeProductIndex,
476                     targetPosition, newTarget);
477             this.targetPosition = newTarget;
478             return true;
479         }
480     }
481
482     /**
483      * @return <b>remainingTime</b> as type int describes the intended remaining time of current operation.
484      */
485     public int getRemainingTime() {
486         return remainingTime;
487     }
488
489     /**
490      * @return <b>timeStamp</b> as type int describes the current time.
491      */
492     public int getTimeStamp() {
493         return timeStamp;
494     }
495
496     /**
497      * Returns the display position of the actuator.
498      * <li>As a general rule it returns <b>currentPosition</b>, except as follows..
499      * <li>If the actuator is in a motion state it returns <b>targetPosition</b>
500      * <li>If the motion state is 'done' but the currentPosition is invalid it returns <b>targetPosition</b>
501      * <li>If the manual override flag is set it returns the <b>unknown</b> position value
502      *
503      * @return The display position of the actuator
504      */
505     public int getDisplayPosition() {
506         switch (getProductState()) {
507             case EXECUTING:
508                 if (VeluxProductPosition.isValid(targetPosition)) {
509                     return targetPosition;
510                 }
511                 break;
512             case DONE:
513                 if (!VeluxProductPosition.isValid(currentPosition) && VeluxProductPosition.isValid(targetPosition)) {
514                     return targetPosition;
515                 }
516                 break;
517             case ERROR:
518             case UNKNOWN:
519             case MANUAL:
520                 return VeluxProductPosition.VPP_VELUX_UNKNOWN;
521             default:
522         }
523         return VeluxProductPosition.isValid(currentPosition) ? currentPosition : VeluxProductPosition.VPP_VELUX_UNKNOWN;
524     }
525
526     /**
527      * Get the Functional Parameters.
528      *
529      * @return the Functional Parameters.
530      */
531     public @Nullable FunctionalParameters getFunctionalParameters() {
532         return functionalParameters;
533     }
534
535     /**
536      * Set the Functional Parameters. Calls getMergeSubstitute() to merge the existing parameters (if any) and the new
537      * parameters (if any).
538      *
539      * @param newFunctionalParameters the new values of the Functional Parameters, or null if nothing is to be set.
540      * @return <b>modified</b> if any of the Functional Parameters have been changed.
541      */
542     public boolean setFunctionalParameters(@Nullable FunctionalParameters newFunctionalParameters) {
543         if ((newFunctionalParameters == null) || newFunctionalParameters.equals(functionalParameters)) {
544             return false;
545         }
546         functionalParameters = FunctionalParameters.createMergeSubstitute(functionalParameters,
547                 newFunctionalParameters);
548         return true;
549     }
550
551     /**
552      * Determines which of the Functional Parameters contains the vane position.
553      * As defined in the Velux KLF 200 API Technical Specification Appendix 2 Table 276.
554      *
555      * @return the index of the vane position Functional Parameter, or -1 if not supported.
556      */
557     private int getVanePositionIndex() {
558         switch (actuatorType) {
559             case BLIND_1_0:
560                 return 0;
561             case ROLLERSHUTTER_2_1:
562             case BLIND_17:
563             case BLIND_18:
564                 return 2;
565             default:
566         }
567         return -1;
568     }
569
570     /**
571      * Indicates if the actuator supports a vane position.
572      *
573      * @return true if vane position is supported.
574      */
575     public boolean supportsVanePosition() {
576         return getVanePositionIndex() >= 0;
577     }
578
579     /**
580      * Return the vane position. Reads the vane position from the Functional Parameters, or returns 'UNKNOWN' if vane
581      * position is not supported.
582      *
583      * @return the vane position.
584      */
585     public int getVanePosition() {
586         FunctionalParameters functionalParameters = this.functionalParameters;
587         int index = getVanePositionIndex();
588         if ((index >= 0) && (functionalParameters != null)) {
589             return functionalParameters.getValue(index);
590         }
591         return VeluxProductPosition.VPP_VELUX_UNKNOWN;
592     }
593
594     /**
595      * Set the vane position into the appropriate Functional Parameter. If the actuator does not support vane positions
596      * then a message is logged.
597      *
598      * @param vanePosition the new vane position.
599      */
600     public void setVanePosition(int vanePosition) {
601         int index = getVanePositionIndex();
602         if ((index >= 0) && FunctionalParameters.isNormalPosition(vanePosition)) {
603             functionalParameters = new FunctionalParameters(index, vanePosition);
604         } else {
605             functionalParameters = null;
606             logger.info("setVanePosition(): actuator type {} ({}) does not support vane position {}.",
607                     ActuatorType.get(actuatorType.getNodeType()), actuatorType.getDescription(), vanePosition);
608         }
609     }
610
611     /**
612      * Get the display position of the vanes depending on the product state.
613      * See 'getDisplayPosition()'.
614      *
615      * @return the display position.
616      */
617     public int getVaneDisplayPosition() {
618         switch (getProductState()) {
619             case ERROR:
620             case UNKNOWN:
621             case MANUAL:
622                 return VeluxProductPosition.VPP_VELUX_UNKNOWN;
623             default:
624         }
625         return getVanePosition();
626     }
627
628     /**
629      * Get the actuator type.
630      *
631      * @return the actuator type.
632      */
633     public ActuatorType getActuatorType() {
634         return this.actuatorType;
635     }
636
637     /**
638      * Set the actuator type.
639      * Only allowed if the current value is undefined.
640      *
641      * @param actuatorType the new value for the actuator type.
642      */
643     public void setActuatorType(ActuatorType actuatorType) {
644         if (this.actuatorType == ActuatorType.UNDEFTYPE) {
645             this.actuatorType = actuatorType;
646             this.typeId = actuatorType.getTypeClass();
647         } else {
648             logger.debug("setActuatorType() failed: not allowed to change actuatorType from {} to {}.",
649                     this.actuatorType, actuatorType);
650         }
651     }
652
653     /**
654      * @return true if it is a Somfy product.
655      */
656     public boolean isSomfyProduct() {
657         return isSomfyProduct;
658     }
659
660     /**
661      * Return the API command that caused this instance to be created.
662      *
663      * @return the API command.
664      */
665     public Command getCreatorCommand() {
666         return creatorCommand;
667     }
668
669     /**
670      * Override the indicator of the source of the data for this product.
671      *
672      * @return the data source.
673      */
674     public VeluxProduct overrideDataSource(DataSource dataSource) {
675         this.dataSource = dataSource;
676         return this;
677     }
678 }