2 * Copyright (c) 2010-2024 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.insteon.internal.device;
15 import static org.openhab.binding.insteon.internal.InsteonBindingConstants.*;
17 import java.io.IOException;
18 import java.util.HashMap;
19 import java.util.LinkedHashMap;
20 import java.util.LinkedList;
21 import java.util.List;
23 import java.util.Objects;
24 import java.util.Optional;
25 import java.util.PriorityQueue;
26 import java.util.Queue;
28 import java.util.function.Predicate;
29 import java.util.stream.Collectors;
30 import java.util.stream.Stream;
32 import org.eclipse.jdt.annotation.NonNullByDefault;
33 import org.eclipse.jdt.annotation.Nullable;
34 import org.openhab.binding.insteon.internal.config.InsteonChannelConfiguration;
35 import org.openhab.binding.insteon.internal.device.DeviceFeature.QueryStatus;
36 import org.openhab.binding.insteon.internal.device.database.LinkDB;
37 import org.openhab.binding.insteon.internal.device.database.LinkDBChange;
38 import org.openhab.binding.insteon.internal.device.database.LinkDBRecord;
39 import org.openhab.binding.insteon.internal.device.database.ModemDB;
40 import org.openhab.binding.insteon.internal.device.database.ModemDBChange;
41 import org.openhab.binding.insteon.internal.device.database.ModemDBEntry;
42 import org.openhab.binding.insteon.internal.device.database.ModemDBRecord;
43 import org.openhab.binding.insteon.internal.device.feature.FeatureEnums.KeypadButtonToggleMode;
44 import org.openhab.binding.insteon.internal.handler.InsteonDeviceHandler;
45 import org.openhab.binding.insteon.internal.transport.message.GroupMessageStateMachine;
46 import org.openhab.binding.insteon.internal.transport.message.GroupMessageStateMachine.GroupMessageType;
47 import org.openhab.binding.insteon.internal.transport.message.Msg;
48 import org.openhab.binding.insteon.internal.utils.BinaryUtils;
49 import org.openhab.core.library.types.DecimalType;
50 import org.openhab.core.library.types.OnOffType;
51 import org.openhab.core.library.types.QuantityType;
52 import org.openhab.core.library.unit.Units;
53 import org.openhab.core.types.Command;
54 import org.openhab.core.types.State;
55 import org.openhab.core.types.UnDefType;
58 * The {@link InsteonDevice} represents an Insteon device
60 * @author Bernd Pfrommer - Initial contribution
61 * @author Rob Nielsen - Port to openHAB 2 insteon binding
62 * @author Jeremy Setton - Rewrite insteon binding
65 public class InsteonDevice extends BaseDevice<InsteonAddress, InsteonDeviceHandler> {
66 private static final int BCAST_STATE_TIMEOUT = 2000; // in milliseconds
67 private static final int DEFAULT_HEARTBEAT_TIMEOUT = 1440; // in minutes
68 private static final int FAILED_MSG_COUNT_THRESHOLD = 5;
70 private InsteonEngine engine = InsteonEngine.UNKNOWN;
71 private LinkDB linkDB;
72 private Map<String, DefaultLink> defaultLinks = new LinkedHashMap<>();
73 private List<Msg> storedMessages = new LinkedList<>();
74 private Queue<DeviceRequest> deferredQueue = new PriorityQueue<>();
75 private Map<Msg, DeviceRequest> deferredQueueHash = new HashMap<>();
76 private Map<Byte, Long> lastBroadcastReceived = new HashMap<>();
77 private Map<Integer, GroupMessageStateMachine> groupState = new HashMap<>();
78 private volatile int failedMsgCount = 0;
79 private volatile long lastMsgReceived = 0L;
81 public InsteonDevice() {
82 super(InsteonAddress.UNKNOWN);
83 this.linkDB = new LinkDB(this);
86 public InsteonEngine getInsteonEngine() {
90 public LinkDB getLinkDB() {
94 public @Nullable DefaultLink getDefaultLink(String name) {
95 synchronized (defaultLinks) {
96 return defaultLinks.get(name);
100 public List<DefaultLink> getDefaultLinks() {
101 synchronized (defaultLinks) {
102 return defaultLinks.values().stream().toList();
106 public List<Msg> getStoredMessages() {
107 synchronized (storedMessages) {
108 return storedMessages;
112 public List<DeviceFeature> getControllerFeatures() {
113 return getFeatures().stream().filter(DeviceFeature::isControllerFeature).toList();
116 public List<DeviceFeature> getResponderFeatures() {
117 return getFeatures().stream().filter(DeviceFeature::isResponderFeature).toList();
120 public List<DeviceFeature> getControllerOrResponderFeatures() {
121 return getFeatures().stream().filter(DeviceFeature::isControllerOrResponderFeature).toList();
124 public List<DeviceFeature> getFeatures(String type) {
125 return getFeatures().stream().filter(feature -> feature.getType().equals(type)).toList();
128 public @Nullable DeviceFeature getFeature(String type, int group) {
129 return getFeatures().stream().filter(feature -> feature.getType().equals(type) && feature.getGroup() == group)
130 .findFirst().orElse(null);
133 public double getLastMsgValueAsDouble(String type, int group, double defaultValue) {
134 return Optional.ofNullable(getFeature(type, group)).map(DeviceFeature::getLastMsgValue).map(Double::doubleValue)
135 .orElse(defaultValue);
138 public int getLastMsgValueAsInteger(String type, int group, int defaultValue) {
139 return Optional.ofNullable(getFeature(type, group)).map(DeviceFeature::getLastMsgValue).map(Double::intValue)
140 .orElse(defaultValue);
143 public @Nullable State getFeatureState(String type, int group) {
144 return Optional.ofNullable(getFeature(type, group)).map(DeviceFeature::getState).orElse(null);
147 public boolean isResponding() {
148 return failedMsgCount < FAILED_MSG_COUNT_THRESHOLD;
151 public boolean isBatteryPowered() {
152 return getFlag("batteryPowered", false);
155 public boolean isDeviceSyncEnabled() {
156 return getFlag("deviceSyncEnabled", false);
159 public boolean hasModemDBEntry() {
160 return getFlag("modemDBEntry", false);
163 public void setInsteonEngine(InsteonEngine engine) {
164 logger.trace("setting insteon engine for {} to {}", address, engine);
165 this.engine = engine;
166 // notify properties changed
167 propertiesChanged(false);
170 public void setHasModemDBEntry(boolean value) {
171 setFlag("modemDBEntry", value);
172 // notify status changed
176 public void setIsDeviceSyncEnabled(boolean value) {
177 setFlag("deviceSyncEnabled", value);
181 * Returns this device heartbeat timeout
183 * @return heartbeat timeout in minutes
185 public int getHeartbeatTimeout() {
186 DeviceFeature feature = getFeature(FEATURE_HEARTBEAT_INTERVAL);
187 if (feature != null) {
188 if (feature.getState() instanceof QuantityType<?> interval) {
189 return Objects.requireNonNullElse(interval.toInvertibleUnit(Units.MINUTE), interval).intValue();
193 return DEFAULT_HEARTBEAT_TIMEOUT;
197 * Returns if this device has heartbeat
199 * @return true if has heartbeat feature and heartbeat on/off feature state on when available, otherise false
201 public boolean hasHeartbeat() {
202 return hasFeature(FEATURE_HEARTBEAT) && (!hasFeature(FEATURE_HEARTBEAT_ON_OFF)
203 || OnOffType.ON.equals(getFeatureState(FEATURE_HEARTBEAT_ON_OFF)));
207 * Returns if this device is awake
209 * @return true if device not battery powered or within awake time
211 public boolean isAwake() {
212 if (isBatteryPowered()) {
213 // define awake time based on the stay awake feature state (ON => 4 minutes; OFF => 3 seconds)
214 State state = getFeatureState(FEATURE_STAY_AWAKE);
215 int awakeTime = OnOffType.ON.equals(state) ? 240000 : 3000; // in msec
216 return System.currentTimeMillis() - lastMsgReceived <= awakeTime;
222 * Returns if a broadcast message is duplicate
224 * @param cmd1 the cmd1 from the broadcast message received
225 * @param timestamp the timestamp from the broadcast message received
226 * @return true if the broadcast message is duplicate
228 public boolean isDuplicateBroadcastMsg(byte cmd1, long timestamp) {
229 synchronized (lastBroadcastReceived) {
230 long timelapse = timestamp - lastBroadcastReceived.getOrDefault(cmd1, timestamp);
231 if (timelapse > 0 && timelapse < BCAST_STATE_TIMEOUT) {
234 lastBroadcastReceived.put(cmd1, timestamp);
241 * Returns if a group message is duplicate
243 * @param cmd1 cmd1 from the group message received
244 * @param timestamp the timestamp from the broadcast message received
245 * @param group the broadcast group
246 * @param type the group message type that was received
247 * @return true if the group message is duplicate
249 public boolean isDuplicateGroupMsg(byte cmd1, long timestamp, int group, GroupMessageType type) {
250 synchronized (groupState) {
251 GroupMessageStateMachine stateMachine = groupState.get(group);
252 if (stateMachine == null) {
253 stateMachine = new GroupMessageStateMachine();
254 groupState.put(group, stateMachine);
255 logger.trace("{} created group {} state", address, group);
257 if (stateMachine.getLastCommand() == cmd1 && stateMachine.getLastTimestamp() == timestamp) {
258 logger.trace("{} using previous group {} state for {}", address, group, type);
259 return stateMachine.isDuplicate();
261 logger.trace("{} updating group {} state to {}", address, group, type);
262 return stateMachine.update(address, group, cmd1, timestamp, type);
268 * Returns if device is pollable
270 * @return true if parent pollable and not battery powered
273 public boolean isPollable() {
274 return super.isPollable() && !isBatteryPowered();
280 * @param delay scheduling delay (in milliseconds)
283 public void doPoll(long delay) {
284 // process deferred queue
285 processDeferredQueue(delay);
286 // poll insteon engine if unknown or its feature never queried
287 DeviceFeature engineFeature = getFeature(FEATURE_INSTEON_ENGINE);
288 if (engineFeature != null
289 && (engine == InsteonEngine.UNKNOWN || engineFeature.getQueryStatus() == QueryStatus.NEVER_QUERIED)) {
290 engineFeature.doPoll(delay);
291 return; // insteon engine needs to be known before enqueueing more messages
293 // load this device link db if not complete or should be reloaded
294 if (!linkDB.isComplete() || linkDB.shouldReload()) {
296 return; // link db needs to be complete before enqueueing more messages
298 // update this device link db if needed
299 if (linkDB.shouldUpdate()) {
300 linkDB.update(delay);
307 * Schedules polling for this device
309 * @param delay scheduling delay (in milliseconds)
310 * @param featureFilter feature filter to apply
311 * @return delay spacing
314 protected long schedulePoll(long delay, Predicate<DeviceFeature> featureFilter) {
315 long spacing = super.schedulePoll(delay, featureFilter);
316 // ping non-battery powered device if no other feature scheduled poll
317 if (!isBatteryPowered() && spacing == 0) {
318 Msg msg = pollFeature(FEATURE_PING, delay);
320 spacing += msg.getQuietTime();
327 * Polls all responder features for this device
329 * @param delay scheduling delay (in milliseconds)
331 public void pollResponders(long delay) {
332 schedulePoll(delay, DeviceFeature::hasResponderFeatures);
336 * Polls responder features for a controller address and group
338 * @param address the controller address
339 * @param group the controller group
340 * @param delay scheduling delay (in milliseconds)
342 public void pollResponders(InsteonAddress address, int group, long delay) {
343 // poll all responder features if link db not complete
344 if (!linkDB.isComplete()) {
345 getResponderFeatures().forEach(feature -> feature.triggerPoll(delay));
348 // poll responder features matching record component id (data 3)
349 linkDB.getResponderRecords(address, group)
350 .forEach(record -> getResponderFeatures().stream()
351 .filter(feature -> feature.getComponentId() == record.getComponentId()).findFirst()
352 .ifPresent(feature -> feature.triggerPoll(delay)));
356 * Polls related devices to a controller group
358 * @param group the controller group
359 * @param delay scheduling delay (in milliseconds)
361 public void pollRelatedDevices(int group, long delay) {
362 InsteonModem modem = getModem();
364 linkDB.getRelatedDevices(group).stream().map(modem::getInsteonDevice).filter(Objects::nonNull)
365 .map(Objects::requireNonNull).forEach(device -> {
366 logger.debug("polling related device {} to controller {} group {}", device.getAddress(),
368 device.pollResponders(address, group, delay);
374 * Adjusts responder features for a controller address and group
376 * @param address the controller address
377 * @param group the controller group
378 * @param onLevel the controller channel config
379 * @param cmd the cmd to adjust to
381 public void adjustResponders(InsteonAddress address, int group, InsteonChannelConfiguration config, Command cmd) {
382 // handle command for responder feature with group matching record component id (data 3)
383 linkDB.getResponderRecords(address, group)
384 .forEach(record -> getResponderFeatures().stream()
385 .filter(feature -> feature.getComponentId() == record.getComponentId()).findFirst()
386 .ifPresent(feature -> {
387 InsteonChannelConfiguration adjustConfig = InsteonChannelConfiguration.copyOf(config,
388 record.getOnLevel(), record.getRampRate());
389 feature.handleCommand(adjustConfig, cmd);
394 * Adjusts related devices to a controller group
396 * @param group the controller group
397 * @param config the controller channel config
398 * @param cmd the cmd to adjust to
400 public void adjustRelatedDevices(int group, InsteonChannelConfiguration config, Command cmd) {
401 InsteonModem modem = getModem();
403 linkDB.getRelatedDevices(group).stream().map(modem::getInsteonDevice).filter(Objects::nonNull)
404 .map(Objects::requireNonNull).forEach(device -> {
405 logger.debug("adjusting related device {} to controller {} group {}", device.getAddress(),
407 device.adjustResponders(address, group, config, cmd);
413 * Returns broadcast group for a controller feature
415 * @param feature the device feature
416 * @return the brodcast group if found, otherwise -1
418 public int getBroadcastGroup(DeviceFeature feature) {
419 InsteonModem modem = getModem();
421 List<InsteonAddress> relatedDevices = linkDB.getRelatedDevices(feature.getGroup());
422 // return broadcast group with matching link and modem db related devices
423 return linkDB.getBroadcastGroups(feature.getComponentId()).stream()
424 .filter(group -> modem.getDB().getRelatedDevices(group).stream()
425 .allMatch(address -> getAddress().equals(address) || relatedDevices.contains(address)))
426 .findFirst().orElse(-1);
432 * Replays a list of messages
434 public void replayMessages(List<Msg> messages) {
435 for (Msg msg : messages) {
436 logger.trace("replaying msg: {}", msg);
437 msg.setIsReplayed(true);
443 * Handles incoming message for this device by forwarding
444 * it to all features that this device supports
446 * @param msg the incoming message
449 public void handleMessage(Msg msg) {
450 // update last msg received if not failure report and more recent msg timestamp
451 if (!msg.isFailureReport() && msg.getTimestamp() > lastMsgReceived) {
452 lastMsgReceived = msg.getTimestamp();
454 // store message if no feature defined
455 if (!hasFeatures()) {
456 logger.debug("storing message for unknown device {}", address);
458 synchronized (storedMessages) {
459 storedMessages.add(msg);
463 // store current responding state
464 boolean isPrevResponding = isResponding();
465 // handle message depending if failure report or not
466 if (msg.isFailureReport()) {
467 getFeatures().stream().filter(feature -> feature.isMyDirectAckOrNack(msg)).findFirst()
468 .ifPresent(feature -> {
469 logger.debug("got a failure report reply of direct for {}", feature.getName());
470 // increase failed message counter
472 // mark feature queried as processed and never queried
473 setFeatureQueried(null);
474 feature.setQueryMessage(null);
475 feature.setQueryStatus(QueryStatus.NEVER_QUERIED);
476 // poll feature again if device is responding
477 if (isResponding()) {
482 // update non-status features
483 getFeatures().stream().filter(feature -> !feature.isStatusFeature() && feature.handleMessage(msg))
484 .findFirst().ifPresent(feature -> {
485 logger.trace("handled reply of direct for {}", feature.getName());
486 // reset failed message counter
488 // mark feature queried as processed and answered
489 setFeatureQueried(null);
490 feature.setQueryMessage(null);
491 feature.setQueryStatus(QueryStatus.QUERY_ANSWERED);
493 // update all status features (e.g. device last update time)
494 getFeatures().stream().filter(DeviceFeature::isStatusFeature)
495 .forEach(feature -> feature.handleMessage(msg));
497 // notify if responding state changed
498 if (isPrevResponding != isResponding()) {
504 * Sends a message after a delay to this device
506 * @param msg the message to be sent
507 * @param feature device feature associated to the message
508 * @param delay time (in milliseconds) to delay before sending message
511 public void sendMessage(Msg msg, DeviceFeature feature, long delay) {
513 addDeviceRequest(msg, feature, delay);
515 addDeferredRequest(msg, feature);
517 // mark feature query status as scheduled for non-broadcast request message
518 if (!msg.isAllLinkBroadcast()) {
519 feature.setQueryStatus(QueryStatus.QUERY_SCHEDULED);
524 * Processes deferred queue
526 * @param delay time (in milliseconds) to delay before sending message
528 private void processDeferredQueue(long delay) {
529 synchronized (deferredQueue) {
530 while (!deferredQueue.isEmpty()) {
531 DeviceRequest request = deferredQueue.poll();
532 if (request != null) {
533 Msg msg = request.getMessage();
534 DeviceFeature feature = request.getFeature();
535 deferredQueueHash.remove(msg);
536 request.setExpirationTime(delay);
537 logger.trace("enqueuing deferred request for {}", feature.getName());
538 addDeviceRequest(msg, feature, delay);
545 * Adds deferred request
547 * @param request device request to add
549 private void addDeferredRequest(Msg msg, DeviceFeature feature) {
550 logger.trace("deferring request for sleeping device {}", address);
552 synchronized (deferredQueue) {
553 DeviceRequest request = new DeviceRequest(feature, msg, 0L);
554 DeviceRequest prevRequest = deferredQueueHash.get(msg);
555 if (prevRequest != null) {
556 logger.trace("overwriting existing deferred request for {}: {}", feature.getName(), msg);
557 deferredQueue.remove(prevRequest);
558 deferredQueueHash.remove(msg);
560 deferredQueue.add(request);
561 deferredQueueHash.put(msg, request);
566 * Clears request queue
569 protected void clearRequestQueue() {
570 super.clearRequestQueue();
572 synchronized (deferredQueue) {
573 deferredQueue.clear();
574 deferredQueueHash.clear();
579 * Updates product data for this device
581 * @param newData the new product data to use
583 public void updateProductData(ProductData newData) {
584 ProductData productData = getProductData();
585 if (productData == null) {
586 setProductData(newData);
587 propertiesChanged(true);
589 logger.trace("updating product data for {} to {}", address, newData);
590 if (productData.update(newData)) {
591 propertiesChanged(true);
593 propertiesChanged(false);
594 resetFeaturesQueryStatus();
600 * Updates this device type
602 * @param newType the new device type to use
605 public void updateType(DeviceType newType) {
606 ProductData productData = getProductData();
607 DeviceType currentType = getType();
608 if (productData != null && !newType.equals(currentType)) {
609 logger.trace("updating device type from {} to {} for {}",
610 currentType != null ? currentType.getName() : "undefined", newType.getName(), address);
611 productData.setDeviceType(newType);
612 propertiesChanged(true);
617 * Updates the default links
619 public void updateDefaultLinks() {
620 InsteonModem modem = getModem();
621 ProductData productData = getProductData();
622 DeviceType deviceType = getType();
623 State linkFFGroup = getFeatureState(FEATURE_LINK_FF_GROUP);
624 State twoGroups = getFeatureState(FEATURE_TWO_GROUPS);
625 if (modem == null || productData == null || deviceType == null || linkFFGroup == UnDefType.NULL
626 || twoGroups == UnDefType.NULL || InsteonAddress.UNKNOWN.equals(modem.getAddress())) {
629 // clear default links
630 synchronized (defaultLinks) {
631 defaultLinks.clear();
633 // iterate over device type default links
634 deviceType.getDefaultLinks().forEach((name, link) -> {
635 // skip default link if 2Groups feature is off and its group is 2
636 if (OnOffType.OFF.equals(twoGroups) && link.getGroup() == 2) {
639 // create link db record based on FFGroup feature state
640 LinkDBRecord linkDBRecord = LinkDBRecord.create(0, modem.getAddress(),
641 OnOffType.ON.equals(linkFFGroup) ? 0xFF : link.getGroup(), link.isController(), link.getData());
642 // create modem db record
643 ModemDBRecord modemDBRecord = ModemDBRecord.create(address, link.getGroup(), !link.isController(),
644 !link.isController() ? productData.getRecordData() : new byte[3]);
645 // create default link commands
646 List<Msg> commands = link.getCommands().stream().map(command -> command.getMessage(this))
647 .filter(Objects::nonNull).map(Objects::requireNonNull).toList();
649 addDefaultLink(new DefaultLink(name, linkDBRecord, modemDBRecord, commands));
654 * Adds a default link for this device
656 * @param link the default link to add
658 private void addDefaultLink(DefaultLink link) {
659 logger.trace("adding default link {} for {}", link.getName(), address);
661 synchronized (defaultLinks) {
662 defaultLinks.put(link.getName(), link);
667 * Returns a map of missing device links for this device
669 * @return map of missing link db records based on default links
671 public Map<String, LinkDBChange> getMissingDeviceLinks() {
672 Map<String, LinkDBChange> links = new LinkedHashMap<>();
673 if (linkDB.isComplete() && hasModemDBEntry()) {
674 for (DefaultLink link : getDefaultLinks()) {
675 LinkDBRecord record = link.getLinkDBRecord();
676 if ((record.getComponentId() > 0 && !linkDB.hasComponentIdRecord(record.getComponentId(), true))
677 || !linkDB.hasGroupRecord(record.getGroup(), true)) {
678 links.put(link.getName(), LinkDBChange.forAdd(record));
686 * Returns a map of missing modem links for this device
688 * @return map of missing modem db records based on default links
690 public Map<String, ModemDBChange> getMissingModemLinks() {
691 Map<String, ModemDBChange> links = new LinkedHashMap<>();
692 InsteonModem modem = getModem();
693 if (modem != null && modem.getDB().isComplete() && hasModemDBEntry()) {
694 for (DefaultLink link : getDefaultLinks()) {
695 ModemDBRecord record = link.getModemDBRecord();
696 if (!modem.getDB().hasRecord(record.getAddress(), record.getGroup(), record.isController())) {
697 links.put(link.getName(), ModemDBChange.forAdd(record));
705 * Returns a set of missing links for this device
707 * @return a set of missing link names
709 public Set<String> getMissingLinks() {
710 return Stream.of(getMissingDeviceLinks().keySet(), getMissingModemLinks().keySet()).flatMap(Set::stream)
711 .collect(Collectors.toSet());
715 * Logs missing links for this device
717 public void logMissingLinks() {
718 Set<String> links = getMissingLinks();
719 if (!links.isEmpty()) {
721 "device {} has missing default links {}, "
722 + "run 'insteon device addMissingLinks' command via openhab console to fix.",
728 * Adds missing links to link db for this device
730 public void addMissingDeviceLinks() {
731 if (getDefaultLinks().isEmpty()) {
734 List<LinkDBChange> changes = getMissingDeviceLinks().values().stream().distinct().toList();
735 if (changes.isEmpty()) {
736 logger.debug("no missing default links from link db to add for {}", address);
738 logger.trace("adding missing default links to link db for {}", address);
739 linkDB.clearChanges();
740 changes.forEach(linkDB::addChange);
744 InsteonModem modem = getModem();
746 getMissingDeviceLinks().keySet().stream().map(this::getDefaultLink).filter(Objects::nonNull)
747 .map(Objects::requireNonNull).flatMap(link -> link.getCommands().stream()).forEach(msg -> {
749 modem.writeMessage(msg);
750 } catch (IOException e) {
751 logger.warn("message write failed for msg: {}", msg, e);
758 * Adds missing links to modem db for this device
760 public void addMissingModemLinks() {
761 InsteonModem modem = getModem();
762 if (modem == null || getDefaultLinks().isEmpty()) {
765 List<ModemDBChange> changes = getMissingModemLinks().values().stream().distinct().toList();
766 if (changes.isEmpty()) {
767 logger.debug("no missing default links from modem db to add for {}", address);
769 logger.trace("adding missing default links to modem db for {}", address);
770 ModemDB modemDB = modem.getDB();
771 modemDB.clearChanges();
772 changes.forEach(modemDB::addChange);
778 * Sets a keypad button radio group
780 * @param buttons list of button groups to set
782 public void setButtonRadioGroup(List<Integer> buttons) {
783 // set each radio button to turn off each others when turned on if should set
784 for (int buttonGroup : buttons) {
785 DeviceFeature onMaskFeature = getFeature(FEATURE_TYPE_KEYPAD_BUTTON_ON_MASK, buttonGroup);
786 DeviceFeature offMaskFeature = getFeature(FEATURE_TYPE_KEYPAD_BUTTON_OFF_MASK, buttonGroup);
788 if (onMaskFeature != null && offMaskFeature != null) {
789 int onMask = onMaskFeature.getLastMsgValueAsInteger(0);
790 int offMask = offMaskFeature.getLastMsgValueAsInteger(0);
792 for (int group : buttons) {
794 onMask = BinaryUtils.clearBit(onMask, bit);
795 offMask = BinaryUtils.updateBit(offMask, bit, buttonGroup != group);
797 onMaskFeature.handleCommand(new DecimalType(onMask));
798 offMaskFeature.handleCommand(new DecimalType(offMask));
804 * Clears a keypad button radion group
806 * @param buttons list of button groups to clear
808 public void clearButtonRadioGroup(List<Integer> buttons) {
809 List<Integer> allButtons = getFeatures(FEATURE_TYPE_KEYPAD_BUTTON).stream().map(DeviceFeature::getGroup)
811 // clear each radio button and decouple from others
812 for (int buttonGroup : allButtons) {
813 DeviceFeature onMaskFeature = getFeature(FEATURE_TYPE_KEYPAD_BUTTON_ON_MASK, buttonGroup);
814 DeviceFeature offMaskFeature = getFeature(FEATURE_TYPE_KEYPAD_BUTTON_OFF_MASK, buttonGroup);
816 if (onMaskFeature != null && offMaskFeature != null) {
817 int onMask = onMaskFeature.getLastMsgValueAsInteger(0);
818 int offMask = offMaskFeature.getLastMsgValueAsInteger(0);
820 for (int group : buttons.contains(buttonGroup) ? allButtons : buttons) {
822 onMask = BinaryUtils.clearBit(onMask, bit);
823 offMask = BinaryUtils.clearBit(offMask, bit);
825 onMaskFeature.handleCommand(new DecimalType(onMask));
826 offMaskFeature.handleCommand(new DecimalType(offMask));
832 * Sets keypad button toggle mode
834 * @param buttons list of button groups to use
835 * @param mode toggle mode to set
837 public void setButtonToggleMode(List<Integer> buttons, KeypadButtonToggleMode mode) {
838 // use the first button group if available to set toggle mode
839 int buttonGroup = !buttons.isEmpty() ? buttons.get(0) : -1;
840 DeviceFeature toggleModeFeature = getFeature(FEATURE_TYPE_KEYPAD_BUTTON_TOGGLE_MODE, buttonGroup);
842 if (toggleModeFeature != null) {
843 int nonToggleMask = toggleModeFeature.getLastMsgValueAsInteger(0) >> 8;
844 int alwaysOnOffMask = toggleModeFeature.getLastMsgValueAsInteger(0) & 0xFF;
846 for (int group : buttons) {
848 nonToggleMask = BinaryUtils.updateBit(nonToggleMask, bit, mode != KeypadButtonToggleMode.TOGGLE);
849 alwaysOnOffMask = BinaryUtils.updateBit(alwaysOnOffMask, bit, mode == KeypadButtonToggleMode.ALWAYS_ON);
851 toggleModeFeature.handleCommand(new DecimalType(nonToggleMask << 8 | alwaysOnOffMask));
856 * Initializes this device
858 public void initialize() {
859 InsteonModem modem = getModem();
860 if (modem == null || !modem.getDB().isComplete()) {
864 ModemDBEntry dbe = modem.getDB().getEntry(address);
866 logger.warn("device {} not found in the modem database. Did you forget to link?", address);
867 setHasModemDBEntry(false);
872 ProductData productData = dbe.getProductData();
873 if (productData != null) {
874 updateProductData(productData);
877 if (!hasModemDBEntry()) {
878 logger.debug("device {} found in the modem database.", address);
879 setHasModemDBEntry(true);
886 updateDefaultLinks();
890 * Refreshes this device
893 public void refresh() {
900 * Resets heartbeat monitor
902 public void resetHeartbeatMonitor() {
903 InsteonDeviceHandler handler = getHandler();
904 if (handler != null) {
905 handler.resetHeartbeatMonitor();
910 * Notifies that the link db has been updated for this device
912 public void linkDBUpdated() {
913 logger.trace("link db for {} has been updated", address);
915 if (linkDB.isComplete()) {
916 if (isBatteryPowered() && isAwake() || getStatus() == DeviceStatus.POLLING) {
917 // poll database delta feature
918 pollFeature(FEATURE_DATABASE_DELTA, 0L);
919 // poll remaining features for this device
925 // notify device handler if defined
926 InsteonDeviceHandler handler = getHandler();
927 if (handler != null) {
928 handler.deviceLinkDBUpdated(this);
933 * Notifies that the properties have changed for this device
935 * @param reset if the device should be reset
937 public void propertiesChanged(boolean reset) {
938 logger.trace("properties for {} has changed", address);
940 InsteonDeviceHandler handler = getHandler();
941 if (handler != null) {
945 handler.updateProperties(this);
951 * Notifies that the status has changed for this device
953 public void statusChanged() {
954 logger.trace("status for {} has changed", address);
956 InsteonDeviceHandler handler = getHandler();
957 if (handler != null) {
958 handler.updateStatus();
963 * Factory method for creating a InsteonDevice from a device address, modem and cache
965 * @param address the device address
966 * @param modem the device modem
967 * @param productData the device product data
968 * @return the newly created InsteonDevice
970 public static InsteonDevice makeDevice(InsteonAddress address, @Nullable InsteonModem modem,
971 @Nullable ProductData productData) {
972 InsteonDevice device = new InsteonDevice();
973 device.setAddress(address);
974 device.setModem(modem);
976 if (productData != null) {
977 DeviceType deviceType = productData.getDeviceType();
978 if (deviceType != null) {
979 device.instantiateFeatures(deviceType);
980 device.setFlags(deviceType.getFlags());
982 int location = productData.getFirstRecordLocation();
983 if (location != LinkDBRecord.LOCATION_ZERO) {
984 device.getLinkDB().setFirstRecordLocation(location);
986 device.setProductData(productData);