2 * Copyright (c) 2010-2023 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.velux.internal.things;
15 import java.util.regex.Pattern;
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;
26 * <B>Velux</B> product representation.
28 * Combined set of information describing a single Velux product.
30 * @author Guenther Schreiner - initial contribution.
33 public class VeluxProduct {
34 private final Logger logger = LoggerFactory.getLogger(VeluxProduct.class);
38 public static final VeluxProduct UNKNOWN = new VeluxProduct();
42 public static class ProductBridgeIndex {
45 public static final ProductBridgeIndex UNKNOWN = new ProductBridgeIndex(0);
51 public ProductBridgeIndex(int id) {
55 // Class access methods
61 public String toString() {
62 return Integer.toString(id);
67 * State (of movement) of an actuator product.
69 * @author AndrewFG - Initial contribution.
71 public enum ProductState {
81 private static final int ACTION_MASK = 0b111;
82 private static final int EQUIVALENT_MASK = ACTION_MASK | MANUAL.value;
84 public final int value;
86 private ProductState(int value) {
91 * Create a ProductState from an integer seed value.
93 * @param value the seed value.
94 * @return the ProductState.
96 public static ProductState of(int value) {
97 if ((value < NON_EXECUTING.value) || (value > UNKNOWN.value)) {
100 if (value == UNKNOWN.value) {
103 if ((value & MANUAL.value) != 0) {
106 int masked = value & ACTION_MASK;
107 for (ProductState state : values()) {
108 if (state.value > DONE.value) {
111 if (masked == state.value) {
119 * Test if the masked values of two state values are operationally equivalent, including if both values are
122 * @param a first value to compare
123 * @param b second value to compare
124 * @return true if the masked values are equivalent.
126 public static boolean equivalent(int a, int b) {
127 return (a & EQUIVALENT_MASK) == (b & EQUIVALENT_MASK);
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}$");
136 * Indicates the data source where the product's contents came from.
138 * @author AndrewFG - Initial contribution.
140 public enum DataSource {
147 private VeluxProductName name;
148 private VeluxProductType typeId;
149 private ActuatorType actuatorType;
150 private ProductBridgeIndex bridgeProductIndex;
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;
174 * just for the dummy VeluxProduct.
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;
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.
194 public VeluxProduct(VeluxProductName name, VeluxProductType typeId, ProductBridgeIndex bridgeProductIndex) {
195 logger.trace("VeluxProduct(v1,name={}) created.", name);
197 this.typeId = typeId;
198 this.bridgeProductIndex = bridgeProductIndex;
199 this.actuatorType = ActuatorType.WINDOW_4_0;
200 this.isSomfyProduct = false;
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.
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);
233 this.typeId = typeId;
234 this.actuatorType = actuatorType;
235 this.bridgeProductIndex = bridgeProductIndex;
238 this.placement = placement;
239 this.velocity = velocity;
240 this.variation = variation;
241 this.powerMode = powerMode;
242 this.serialNumber = serialNumber;
244 this.currentPosition = currentPosition;
245 this.targetPosition = target;
246 this.functionalParameters = functionalParameters;
247 this.remainingTime = remainingTime;
248 this.timeStamp = timeStamp;
249 this.creatorCommand = creatorCommand;
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();
256 * Constructor for a 'notification' product. Such products are used as data transfer objects to carry the limited
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.
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.
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);
274 this.typeId = VeluxProductType.UNDEFTYPE;
275 this.actuatorType = ActuatorType.UNDEFTYPE;
277 this.bridgeProductIndex = bridgeProductIndex;
279 this.currentPosition = currentPosition;
280 this.targetPosition = target;
281 this.functionalParameters = functionalParameters;
282 this.isSomfyProduct = false;
283 this.creatorCommand = creatorCommand;
289 public VeluxProduct clone() {
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,
297 return new VeluxProduct(name, typeId, bridgeProductIndex);
301 // Class access methods
304 * Returns the name of the current product (aka actuator) for convenience as type-specific class.
306 * @return nameOfThisProduct as type {@link VeluxProductName}.
308 public VeluxProductName getProductName() {
313 * Returns the type of the current product (aka actuator) for convenience as type-specific class.
315 * @return typeOfThisProduct as type {@link VeluxProductType}.
317 public VeluxProductType getProductType() {
321 public ProductBridgeIndex getBridgeProductIndex() {
322 return this.bridgeProductIndex;
326 public String toString() {
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());
335 return String.format("VeluxProduct(v1, name:%s, typeId:%s, bridgeIndex:%d)", name, typeId,
336 bridgeProductIndex.toInt());
340 // Class helper methods
343 * Return the product unique index.
344 * Either the serial number (for normal Velux devices), or its name (for e.g. Somfy devices).
346 * @return the serial number or its name
348 public String getProductUniqueIndex() {
349 if (!v2 || serialNumber.startsWith(VeluxProductSerialNo.UNKNOWN)) {
350 return name.toString();
352 return VeluxProductSerialNo.cleaned(serialNumber);
355 // Getter and Setter methods
358 * @return <b>v2</b> as type boolean signals the availability of firmware version two (product) details.
360 public boolean isV2() {
365 * @return <b>order</b> as type int describes the user-oriented sort-order.
367 public int getOrder() {
372 * @return <B>placement</B> as type int is used to describe a group index or house group index number.
374 public int getPlacement() {
379 * @return <B>velocity</B> as type int describes what velocity the node is operation with
381 public int getVelocity() {
386 * @return <B>variation</B> as type int describes detail information like top hung, kip, flat roof or sky light
389 public int getVariation() {
394 * @return <B>powerMode</B> as type int is used to show the power mode of the node (ALWAYS_ALIVE/LOW_POWER_MODE).
396 public int getPowerMode() {
401 * @return <B>serialNumber</B> as type String is the serial number of 8 bytes length of the node.
403 public String getSerialNumber() {
408 * @return <B>state</B> as type int is used to operating state of the node.
410 public int getState() {
415 * Get the actuator state.
417 * @return state cast to an ActuatorState enum.
419 public ProductState getProductState() {
420 return ProductState.of(state);
424 * @param newState Update the operating state of the node.
425 * @return <B>modified</B> as type boolean to signal a real modification.
427 public boolean setState(int newState) {
428 if (this.state == newState) {
431 logger.trace("setState(name={},index={}) state {} replaced by {}.", name, bridgeProductIndex, state,
433 this.state = newState;
439 * @return <B>currentPosition</B> as type int signals the current position of the node.
441 public int getCurrentPosition() {
442 return currentPosition;
446 * @param newCurrentPosition Update the current position of the node.
447 * @return <B>modified</B> as boolean to signal a real modification.
449 public boolean setCurrentPosition(int newCurrentPosition) {
450 if (this.currentPosition == newCurrentPosition) {
453 logger.trace("setCurrentPosition(name={},index={}) currentPosition {} replaced by {}.", name,
454 bridgeProductIndex, currentPosition, newCurrentPosition);
455 this.currentPosition = newCurrentPosition;
461 * @return <b>target</b> as type int shows the target position of the current operation.
463 public int getTarget() {
464 return targetPosition;
468 * @param newTarget Update the target position of the current operation.
469 * @return <b>modified</b> as boolean to signal a real modification.
471 public boolean setTarget(int newTarget) {
472 if (this.targetPosition == newTarget) {
475 logger.trace("setTarget(name={},index={}) target {} replaced by {}.", name, bridgeProductIndex,
476 targetPosition, newTarget);
477 this.targetPosition = newTarget;
483 * @return <b>remainingTime</b> as type int describes the intended remaining time of current operation.
485 public int getRemainingTime() {
486 return remainingTime;
490 * @return <b>timeStamp</b> as type int describes the current time.
492 public int getTimeStamp() {
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
503 * @return The display position of the actuator
505 public int getDisplayPosition() {
506 switch (getProductState()) {
508 if (VeluxProductPosition.isValid(targetPosition)) {
509 return targetPosition;
513 if (!VeluxProductPosition.isValid(currentPosition) && VeluxProductPosition.isValid(targetPosition)) {
514 return targetPosition;
520 return VeluxProductPosition.VPP_VELUX_UNKNOWN;
523 return VeluxProductPosition.isValid(currentPosition) ? currentPosition : VeluxProductPosition.VPP_VELUX_UNKNOWN;
527 * Get the Functional Parameters.
529 * @return the Functional Parameters.
531 public @Nullable FunctionalParameters getFunctionalParameters() {
532 return functionalParameters;
536 * Set the Functional Parameters. Calls getMergeSubstitute() to merge the existing parameters (if any) and the new
537 * parameters (if any).
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.
542 public boolean setFunctionalParameters(@Nullable FunctionalParameters newFunctionalParameters) {
543 if ((newFunctionalParameters == null) || newFunctionalParameters.equals(functionalParameters)) {
546 functionalParameters = FunctionalParameters.createMergeSubstitute(functionalParameters,
547 newFunctionalParameters);
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.
555 * @return the index of the vane position Functional Parameter, or -1 if not supported.
557 private int getVanePositionIndex() {
558 switch (actuatorType) {
561 case ROLLERSHUTTER_2_1:
571 * Indicates if the actuator supports a vane position.
573 * @return true if vane position is supported.
575 public boolean supportsVanePosition() {
576 return getVanePositionIndex() >= 0;
580 * Return the vane position. Reads the vane position from the Functional Parameters, or returns 'UNKNOWN' if vane
581 * position is not supported.
583 * @return the vane position.
585 public int getVanePosition() {
586 FunctionalParameters functionalParameters = this.functionalParameters;
587 int index = getVanePositionIndex();
588 if ((index >= 0) && (functionalParameters != null)) {
589 return functionalParameters.getValue(index);
591 return VeluxProductPosition.VPP_VELUX_UNKNOWN;
595 * Set the vane position into the appropriate Functional Parameter. If the actuator does not support vane positions
596 * then a message is logged.
598 * @param vanePosition the new vane position.
600 public void setVanePosition(int vanePosition) {
601 int index = getVanePositionIndex();
602 if ((index >= 0) && FunctionalParameters.isNormalPosition(vanePosition)) {
603 functionalParameters = new FunctionalParameters(index, vanePosition);
605 functionalParameters = null;
606 logger.info("setVanePosition(): actuator type {} ({}) does not support vane position {}.",
607 ActuatorType.get(actuatorType.getNodeType()), actuatorType.getDescription(), vanePosition);
612 * Get the display position of the vanes depending on the product state.
613 * See 'getDisplayPosition()'.
615 * @return the display position.
617 public int getVaneDisplayPosition() {
618 switch (getProductState()) {
622 return VeluxProductPosition.VPP_VELUX_UNKNOWN;
625 return getVanePosition();
629 * Get the actuator type.
631 * @return the actuator type.
633 public ActuatorType getActuatorType() {
634 return this.actuatorType;
638 * Set the actuator type.
639 * Only allowed if the current value is undefined.
641 * @param actuatorType the new value for the actuator type.
643 public void setActuatorType(ActuatorType actuatorType) {
644 if (this.actuatorType == ActuatorType.UNDEFTYPE) {
645 this.actuatorType = actuatorType;
646 this.typeId = actuatorType.getTypeClass();
648 logger.debug("setActuatorType() failed: not allowed to change actuatorType from {} to {}.",
649 this.actuatorType, actuatorType);
654 * @return true if it is a Somfy product.
656 public boolean isSomfyProduct() {
657 return isSomfyProduct;
661 * Return the API command that caused this instance to be created.
663 * @return the API command.
665 public Command getCreatorCommand() {
666 return creatorCommand;
670 * Override the indicator of the source of the data for this product.
672 * @return the data source.
674 public VeluxProduct overrideDataSource(DataSource dataSource) {
675 this.dataSource = dataSource;