]> git.basschouten.com Git - openhab-addons.git/blob
acef6914d0f708d39bdf95e62be1e24da04316eb
[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.io.homekit.internal;
14
15 import java.lang.reflect.InvocationTargetException;
16 import java.time.Clock;
17 import java.time.Duration;
18 import java.util.ArrayList;
19 import java.util.Collection;
20 import java.util.HashMap;
21 import java.util.HashSet;
22 import java.util.List;
23 import java.util.Map;
24 import java.util.Map.Entry;
25 import java.util.Optional;
26 import java.util.Set;
27 import java.util.concurrent.ExecutionException;
28 import java.util.concurrent.ScheduledExecutorService;
29 import java.util.stream.Collectors;
30
31 import org.eclipse.jdt.annotation.NonNullByDefault;
32 import org.eclipse.jdt.annotation.Nullable;
33 import org.openhab.core.common.ThreadPoolManager;
34 import org.openhab.core.common.registry.RegistryChangeListener;
35 import org.openhab.core.items.GroupItem;
36 import org.openhab.core.items.Item;
37 import org.openhab.core.items.ItemNotFoundException;
38 import org.openhab.core.items.ItemRegistry;
39 import org.openhab.core.items.ItemRegistryChangeListener;
40 import org.openhab.core.items.Metadata;
41 import org.openhab.core.items.MetadataKey;
42 import org.openhab.core.items.MetadataRegistry;
43 import org.openhab.core.storage.Storage;
44 import org.openhab.io.homekit.internal.accessories.AbstractHomekitAccessoryImpl;
45 import org.openhab.io.homekit.internal.accessories.DummyHomekitAccessory;
46 import org.openhab.io.homekit.internal.accessories.HomekitAccessoryFactory;
47 import org.slf4j.Logger;
48 import org.slf4j.LoggerFactory;
49
50 import io.github.hapjava.accessories.HomekitAccessory;
51 import io.github.hapjava.characteristics.impl.common.NameCharacteristic;
52 import io.github.hapjava.server.impl.HomekitRoot;
53
54 /**
55  * Listens for changes to the item and metadata registry. When changes are detected, check
56  * for HomeKit tags and, if present, add the items to the HomekitAccessoryRegistry.
57  *
58  * @author Andy Lintner - Initial contribution
59  */
60 @NonNullByDefault
61 public class HomekitChangeListener implements ItemRegistryChangeListener {
62     private final Logger logger = LoggerFactory.getLogger(HomekitChangeListener.class);
63     private final static String REVISION_CONFIG = "revision";
64     private final static String ACCESSORY_COUNT = "accessory_count";
65     private final static String KNOWN_ACCESSORIES = "known_accessories";
66     private final ItemRegistry itemRegistry;
67     private final HomekitAccessoryRegistry accessoryRegistry = new HomekitAccessoryRegistry();
68     private final MetadataRegistry metadataRegistry;
69     private final Storage<Object> storage;
70     private final RegistryChangeListener<Metadata> metadataChangeListener;
71     private HomekitAccessoryUpdater updater = new HomekitAccessoryUpdater();
72     private HomekitSettings settings;
73     private int lastAccessoryCount;
74     private Map<String, String> knownAccessories = new HashMap<>();
75     private int instance;
76     private List<String> priorDummies = new ArrayList<>();
77
78     private final Set<String> pendingUpdates = new HashSet<>();
79
80     private final ScheduledExecutorService scheduler = ThreadPoolManager
81             .getScheduledPool(ThreadPoolManager.THREAD_POOL_NAME_COMMON);
82
83     /**
84      * Rather than reacting to item added/removed/modified changes directly, we mark them as dirty (and the groups to
85      * which they belong)
86      *
87      * We wait for a second to pass until no more items are changed. This allows us to add a group of items all at once,
88      * rather than for each update at a time, preventing us from showing an error message with each addition until the
89      * group is complete.
90      */
91     private final Debouncer applyUpdatesDebouncer;
92
93     HomekitChangeListener(ItemRegistry itemRegistry, HomekitSettings settings, MetadataRegistry metadataRegistry,
94             Storage<Object> storage, int instance) {
95         this.itemRegistry = itemRegistry;
96         this.settings = settings;
97         this.metadataRegistry = metadataRegistry;
98         this.storage = storage;
99         this.instance = instance;
100         this.applyUpdatesDebouncer = new Debouncer("update-homekit-devices-" + instance, scheduler,
101                 Duration.ofMillis(1000), Clock.systemUTC(), this::applyUpdates);
102         metadataChangeListener = new RegistryChangeListener<Metadata>() {
103             @Override
104             public void added(final Metadata metadata) {
105                 final MetadataKey uid = metadata.getUID();
106                 if (HomekitAccessoryFactory.METADATA_KEY.equalsIgnoreCase(uid.getNamespace())) {
107                     try {
108                         markDirty(itemRegistry.getItem(uid.getItemName()));
109                     } catch (ItemNotFoundException e) {
110                         logger.trace("Could not find item for metadata {}", metadata);
111                     }
112                 }
113             }
114
115             @Override
116             public void removed(final Metadata metadata) {
117                 final MetadataKey uid = metadata.getUID();
118                 if (HomekitAccessoryFactory.METADATA_KEY.equalsIgnoreCase(uid.getNamespace())) {
119                     try {
120                         markDirty(itemRegistry.getItem(uid.getItemName()));
121                     } catch (ItemNotFoundException e) {
122                         logger.trace("Could not find item for metadata {}", metadata);
123                     }
124                 }
125             }
126
127             @Override
128             public void updated(final Metadata oldMetadata, final Metadata newMetadata) {
129                 final MetadataKey oldUid = oldMetadata.getUID();
130                 final MetadataKey newUid = newMetadata.getUID();
131                 if (HomekitAccessoryFactory.METADATA_KEY.equalsIgnoreCase(oldUid.getNamespace())
132                         || HomekitAccessoryFactory.METADATA_KEY.equalsIgnoreCase(newUid.getNamespace())) {
133                     try {
134                         // the item name is same in old and new metadata, so we can take any.
135                         markDirty(itemRegistry.getItem(oldUid.getItemName()));
136                     } catch (ItemNotFoundException e) {
137                         logger.debug("Could not find item for metadata {}", oldMetadata);
138                     }
139                 }
140             }
141         };
142         itemRegistry.addRegistryChangeListener(this);
143         metadataRegistry.addRegistryChangeListener(metadataChangeListener);
144         initialiseRevision();
145         boolean changed = false;
146         for (var i : itemRegistry.getItems()) {
147             String oldValue = knownAccessories.get(i.getName());
148             createRootAccessories(i);
149             if (accessoryChanged(i.getName(), oldValue)) {
150                 logger.debug("Accessory {} changed:\n{}\n{}", i.getName(), oldValue, knownAccessories.get(i.getName()));
151                 changed = true;
152             }
153         }
154         // order of this conditional is important - checkMissingAccessories has side effects that need to always happen
155         if (checkMissingAccessories() || changed) {
156             makeNewConfigurationRevision();
157         } else {
158             logger.info("Created {} HomeKit items in instance {} (no change from prior configuration).",
159                     accessoryRegistry.getAllAccessories().size(), instance);
160             if (settings.useDummyAccessories) {
161                 checkForDummyAccessories();
162             }
163         }
164     }
165
166     private void initialiseRevision() {
167         int revision = 1;
168         try {
169             String revisionString = (String) storage.get(REVISION_CONFIG);
170             if (revisionString == null) {
171                 throw new NumberFormatException();
172             }
173             revision = Integer.parseInt(revisionString);
174         } catch (NumberFormatException e) {
175         }
176         accessoryRegistry.setConfigurationRevision(revision);
177
178         lastAccessoryCount = 0;
179         var localKnownAccessories = (Map<String, String>) storage.get(KNOWN_ACCESSORIES);
180         if (localKnownAccessories == null) {
181             knownAccessories = new HashMap<>();
182             // Back-compat
183             try {
184                 String accessoryCountString = (String) storage.get(ACCESSORY_COUNT);
185                 if (accessoryCountString == null) {
186                     throw new NumberFormatException();
187                 }
188                 lastAccessoryCount = Integer.parseInt(accessoryCountString);
189             } catch (NumberFormatException e) {
190             }
191         } else {
192             knownAccessories = localKnownAccessories;
193             lastAccessoryCount = knownAccessories.size();
194         }
195     }
196
197     private boolean hasHomeKitMetadata(Item item) {
198         return metadataRegistry.get(new MetadataKey(HomekitAccessoryFactory.METADATA_KEY, item.getUID())) != null;
199     }
200
201     @Override
202     public synchronized void added(Item item) {
203         if (hasHomeKitMetadata(item)) {
204             markDirty(item);
205         }
206     }
207
208     @Override
209     public void allItemsChanged(Collection<String> oldItemNames) {
210         clearAccessories();
211     }
212
213     /**
214      * Mark an item as dirty, plus any accessory groups to which it pertains, so that after a debounce period the
215      * accessory update can be applied.
216      *
217      * @param item The item that has been changed or removed.
218      */
219     private synchronized void markDirty(Item item) {
220         logger.trace("Mark dirty item {}", item.getName());
221         pendingUpdates.add(item.getName());
222         /*
223          * If findMyAccessoryGroups fails because the accessory group has already been deleted, then we can count on a
224          * later update telling us that the accessory group was removed.
225          */
226         for (Item accessoryGroup : HomekitAccessoryFactory.getAccessoryGroups(item, itemRegistry, metadataRegistry)) {
227             pendingUpdates.add(accessoryGroup.getName());
228         }
229
230         /*
231          * if metadata of a group item was changed, mark all group member as dirty.
232          */
233         if (item instanceof GroupItem) {
234             ((GroupItem) item).getMembers().forEach(groupMember -> pendingUpdates.add(groupMember.getName()));
235         }
236         applyUpdatesDebouncer.call();
237     }
238
239     @Override
240     public synchronized void removed(Item item) {
241         if (hasHomeKitMetadata(item)) {
242             markDirty(item);
243         }
244     }
245
246     private Optional<Item> getItemOptional(String name) {
247         try {
248             return Optional.of(itemRegistry.getItem(name));
249         } catch (ItemNotFoundException e) {
250             return Optional.empty();
251         }
252     }
253
254     public void makeNewConfigurationRevision() {
255         final int newRevision = accessoryRegistry.makeNewConfigurationRevision();
256         lastAccessoryCount = accessoryRegistry.getAllAccessories().size();
257         logger.info("Created {} HomeKit items in instance {}.", accessoryRegistry.getAllAccessories().size(), instance);
258         logger.trace("Making new configuration revision {}", newRevision);
259         storage.put(REVISION_CONFIG, "" + newRevision);
260         storage.put(KNOWN_ACCESSORIES, knownAccessories);
261     }
262
263     public synchronized void pruneDummyAccessories() {
264         boolean removed = false;
265         for (HomekitAccessory accessory : accessoryRegistry.getAllAccessories().values()
266                 .toArray(new HomekitAccessory[0])) {
267             if (accessory instanceof DummyHomekitAccessory) {
268                 try {
269                     String name = accessory.getName().get();
270                     logger.info("Pruning dummy accessory {}.", name);
271                     knownAccessories.remove(name);
272                     accessoryRegistry.remove(name);
273                     removed = true;
274                 } catch (ExecutionException | InterruptedException e) {
275                     // will never happen; it's a always completed future
276                 }
277             }
278         }
279         if (removed) {
280             makeNewConfigurationRevision();
281         }
282     }
283
284     private synchronized void applyUpdates() {
285         logger.trace("Apply updates");
286
287         HomekitRoot bridge = accessoryRegistry.getBridge();
288         if (bridge != null) {
289             bridge.batchUpdate();
290         }
291
292         try {
293             boolean changed = false;
294             boolean removed = false;
295             for (final String name : pendingUpdates) {
296                 String oldValue = knownAccessories.get(name);
297                 accessoryRegistry.remove(name);
298                 logger.trace(" Add items {}", name);
299                 getItemOptional(name).ifPresent(this::createRootAccessories);
300                 if (accessoryChanged(name, oldValue)) {
301                     changed = true;
302                 }
303             }
304             pendingUpdates.clear();
305             if (checkMissingAccessories() || changed) {
306                 makeNewConfigurationRevision();
307             }
308             checkForDummyAccessories();
309         } finally {
310             if (bridge != null) {
311                 bridge.completeUpdateBatch();
312             }
313         }
314     }
315
316     private boolean accessoryChanged(String name, @Nullable String oldValue) {
317         String newValue = knownAccessories.get(name);
318         if (oldValue == null && newValue == null) {
319             return false;
320         }
321         return oldValue == null && newValue != null || oldValue != null && newValue == null
322                 || !oldValue.equals(newValue);
323     }
324
325     @Override
326     public void updated(Item oldElement, Item element) {
327         markDirty(oldElement);
328         markDirty(element);
329     }
330
331     public int getLastAccessoryCount() {
332         return lastAccessoryCount;
333     }
334
335     public synchronized void clearAccessories() {
336         accessoryRegistry.clear();
337     }
338
339     public synchronized void setBridge(HomekitRoot bridge) {
340         accessoryRegistry.setBridge(bridge);
341     }
342
343     public void setUpdater(HomekitAccessoryUpdater updater) {
344         this.updater = updater;
345     }
346
347     public void updateSettings(HomekitSettings settings) {
348         boolean wasUsingDummyAccessories = this.settings.useDummyAccessories;
349         this.settings = settings;
350         // If they turned off dummy accessories, immediately prune them
351         if (wasUsingDummyAccessories && !settings.useDummyAccessories) {
352             pruneDummyAccessories();
353         }
354     }
355
356     public synchronized void stop() {
357         this.itemRegistry.removeRegistryChangeListener(this);
358         this.metadataRegistry.removeRegistryChangeListener(metadataChangeListener);
359         applyUpdatesDebouncer.stop();
360         accessoryRegistry.unsetBridge();
361     }
362
363     public Map<String, HomekitAccessory> getAccessories() {
364         return this.accessoryRegistry.getAllAccessories();
365     }
366
367     public int getConfigurationRevision() {
368         return this.accessoryRegistry.getConfigurationRevision();
369     }
370
371     /**
372      * select primary accessory type from list of types.
373      * selection logic:
374      * - if accessory has only one type, it is the primary type
375      * - if accessory has no primary type defined per configuration, then the first type on the list is the primary type
376      * - if accessory has primary type defined per configuration and this type is on the list of types, then it is the
377      * primary
378      * - if accessory has primary type defined per configuration and this type is NOT on the list of types, then the
379      * first type on the list is the primary type
380      *
381      * @param item openhab item
382      * @param accessoryTypes list of accessory type attached to the item
383      * @return primary accessory type
384      */
385     private HomekitAccessoryType getPrimaryAccessoryType(Item item,
386             List<Entry<HomekitAccessoryType, HomekitCharacteristicType>> accessoryTypes,
387             @Nullable Map<String, Object> configuration) {
388         if (accessoryTypes.size() > 1 && configuration != null) {
389             final @Nullable Object value = configuration.get(HomekitTaggedItem.PRIMARY_SERVICE);
390             if (value instanceof String) {
391                 return accessoryTypes.stream()
392                         .filter(aType -> ((String) value).equalsIgnoreCase(aType.getKey().getTag())).findAny()
393                         .orElse(accessoryTypes.get(0)).getKey();
394             }
395         }
396         // no primary accessory found or there is only one type, so return the first type from the list
397         return accessoryTypes.get(0).getKey();
398     }
399
400     /**
401      * creates one or more HomeKit items for given openhab item.
402      * one OpenHAB item can be linked to several HomeKit accessories.
403      * OpenHAB item is a good candidate for a HomeKit accessory
404      * IF
405      * - it has HomeKit accessory types defined using HomeKit accessory metadata
406      * - AND is not part of a group with HomeKit metadata
407      * e.g.
408      * Switch light "Light" {homekit="Lighting"}
409      * Group gLight "Light Group" {homekit="Lighting"}
410      *
411      * OR
412      * - it has HomeKit accessory types defined using HomeKit accessory metadata
413      * - AND is part of groups with HomeKit metadata, but all groups have baseItem
414      * e.g.
415      * Group:Switch:OR(ON,OFF) gLight "Light Group " {homekit="Lighting"}
416      * Switch light "Light" (gLight) {homekit="Lighting.OnState"}
417      *
418      *
419      * In contrast, items which are part of groups without BaseItem are additional HomeKit characteristics of the
420      * accessory defined by that group and don't need to be created as accessory here.
421      * e.g.
422      * Group gLight "Light Group " {homekit="Lighting"}
423      * Switch light "Light" (gLight) {homekit="Lighting.OnState"}
424      * is not the root accessory but only a characteristic "OnState"
425      *
426      * Examples:
427      * // Single line HomeKit Accessory
428      * Switch light "Light" {homekit="Lighting"}
429      *
430      * // One HomeKit accessory defined using group
431      * Group gLight "Light Group" {homekit="Lighting"}
432      * Switch light "Light" (gLight) {homekit="Lighting.OnState"}
433      *
434      * // 2 HomeKit accessories: one is switch attached to group, another one a single switch
435      * Group:Switch:OR(ON,OFF) gLight "Light Group " {homekit="Lighting"}
436      * Switch light "Light" (gLight) {homekit="Lighting.OnState"}
437      *
438      * @param item openHAB item
439      */
440     private void createRootAccessories(Item item) {
441         final List<Entry<HomekitAccessoryType, HomekitCharacteristicType>> accessoryTypes = HomekitAccessoryFactory
442                 .getAccessoryTypes(item, metadataRegistry);
443         if (accessoryTypes.isEmpty()) {
444             return;
445         }
446
447         final List<GroupItem> groups = HomekitAccessoryFactory.getAccessoryGroups(item, itemRegistry, metadataRegistry);
448         // Don't create accessories that are sub-accessories of other accessories
449         if (groups.stream().anyMatch(g -> !HomekitAccessoryFactory.getAccessoryTypes(g, metadataRegistry).isEmpty())) {
450             return;
451         }
452
453         final @Nullable Map<String, Object> itemConfiguration = HomekitAccessoryFactory.getItemConfiguration(item,
454                 metadataRegistry);
455         if (!itemIsForThisBridge(item, itemConfiguration)) {
456             return;
457         }
458
459         final HomekitAccessoryType primaryAccessoryType = getPrimaryAccessoryType(item, accessoryTypes,
460                 itemConfiguration);
461         logger.trace("Item {} is a HomeKit accessory of types {}. Primary type is {}", item.getName(), accessoryTypes,
462                 primaryAccessoryType);
463         final HomekitOHItemProxy itemProxy = new HomekitOHItemProxy(item);
464         final HomekitTaggedItem taggedItem = new HomekitTaggedItem(itemProxy, primaryAccessoryType, itemConfiguration);
465         try {
466             final AbstractHomekitAccessoryImpl accessory = HomekitAccessoryFactory.create(taggedItem, metadataRegistry,
467                     updater, settings);
468             if (accessory.isLinkedServiceOnly()) {
469                 logger.warn("Item '{}' is a '{}' which must be nested another another accessory.", taggedItem.getName(),
470                         primaryAccessoryType);
471                 return;
472             }
473
474             accessoryTypes.stream().filter(aType -> !primaryAccessoryType.equals(aType.getKey()))
475                     .forEach(additionalAccessoryType -> {
476                         final HomekitTaggedItem additionalTaggedItem = new HomekitTaggedItem(itemProxy,
477                                 additionalAccessoryType.getKey(), itemConfiguration);
478                         try {
479                             final AbstractHomekitAccessoryImpl additionalAccessory = HomekitAccessoryFactory
480                                     .create(additionalTaggedItem, metadataRegistry, updater, settings);
481                             // Secondary accessories that don't explicitly specify a name will implicitly
482                             // get a name characteristic based on the item's name
483                             if (!additionalAccessory.getCharacteristic(HomekitCharacteristicType.NAME).isPresent()) {
484                                 try {
485                                     additionalAccessory.addCharacteristic(
486                                             new NameCharacteristic(() -> additionalAccessory.getName()));
487                                 } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
488                                     // This should never happen; all services should support NameCharacteristic as an
489                                     // optional Characteristic.
490                                     // If HAP-Java defined a service that doesn't support
491                                     // addOptionalCharacteristic(NameCharacteristic), then it's a bug there, and we're
492                                     // just going to ignore the exception here.
493                                 }
494                             }
495                             accessory.getServices().add(additionalAccessory.getPrimaryService());
496                         } catch (HomekitException e) {
497                             logger.warn("Cannot create additional accessory {}", additionalTaggedItem);
498                         }
499                     });
500             knownAccessories.put(taggedItem.getName(), accessory.toJson());
501             accessoryRegistry.addRootAccessory(taggedItem.getName(), accessory);
502         } catch (HomekitException e) {
503             logger.warn("Cannot create accessory {}", taggedItem);
504         }
505     }
506
507     private boolean itemIsForThisBridge(Item item, @Nullable Map<String, Object> configuration) {
508         // non-tagged accessories belong to the first instance
509         if (configuration == null) {
510             return (instance == 1);
511         }
512
513         final @Nullable Object value = configuration.get(HomekitTaggedItem.INSTANCE);
514         if (value == null) {
515             return (instance == 1);
516         }
517         if (value instanceof Number) {
518             return (instance == ((Number) value).intValue());
519         }
520         logger.warn("Unrecognized instance tag {} ({}) for item {}; assigning to default instance.", value,
521                 value.getClass(), item.getName());
522         return (instance == 1);
523     }
524
525     /**
526      * Check for any missing accessories.
527      *
528      * If there are, return true so we know to increment the config version. UNLESS
529      * we're configured to use dummy accessories, in which case backfill it with a dummy.
530      *
531      * @return if we need to increment the configuration version
532      */
533     private boolean checkMissingAccessories() {
534         List<String> toRemove = new ArrayList<>();
535         for (Map.Entry<String, String> accessory : knownAccessories.entrySet()) {
536             if (!accessoryRegistry.getAllAccessories().containsKey(accessory.getKey())) {
537                 if (settings.useDummyAccessories) {
538                     logger.debug("Creating dummy accessory for missing item {}.", accessory.getKey());
539                     accessoryRegistry.addRootAccessory(accessory.getKey(),
540                             new DummyHomekitAccessory(accessory.getKey(), accessory.getValue()));
541                 } else {
542                     toRemove.add(accessory.getKey());
543                 }
544             }
545         }
546
547         toRemove.forEach(k -> knownAccessories.remove(k));
548         return !toRemove.isEmpty();
549     }
550
551     private void checkForDummyAccessories() {
552         List<String> currentDummies = accessoryRegistry.getAllAccessories().values().stream()
553                 .filter(a -> a instanceof DummyHomekitAccessory).map(a -> {
554                     try {
555                         return a.getSerialNumber().get();
556                     } catch (InterruptedException | ExecutionException e) {
557                         return "<unknown>";
558                     }
559                 }).collect(Collectors.toList());
560
561         List<String> resolvedDummies = new ArrayList(priorDummies);
562         resolvedDummies.removeAll(currentDummies);
563         List<String> newDummies = new ArrayList(currentDummies);
564         newDummies.removeAll(priorDummies);
565
566         if (resolvedDummies.size() <= 5) {
567             for (String item : resolvedDummies) {
568                 logger.info("{} has been resolved to an actual accessory, and is no longer a dummy.", item);
569             }
570         } else if (currentDummies.isEmpty() && !resolvedDummies.isEmpty()) {
571             logger.info("All dummy accessories have been resolved to actual accessories.");
572         } else if (!resolvedDummies.isEmpty()) {
573             logger.info("{} dummy accessories have been resolved to actual accessories.", resolvedDummies.size());
574         }
575
576         if (newDummies.size() <= 5) {
577             for (String item : newDummies) {
578                 logger.warn(
579                         "{} has been replaced with a dummy. See https://www.openhab.org/addons/integrations/homekit/#dummy-accessories for more information.",
580                         item);
581             }
582         } else if (!newDummies.isEmpty()) {
583             logger.warn(
584                     "{} accessories have been replaced with dummies. See https://www.openhab.org/addons/integrations/homekit/#dummy-accessories for more information.",
585                     newDummies.size());
586         } else if (!currentDummies.isEmpty()) {
587             logger.warn(
588                     "{} accessories are still dummies. See https://www.openhab.org/addons/integrations/homekit/#dummy-accessories for more information.",
589                     currentDummies.size());
590         }
591         priorDummies.clear();
592         priorDummies.addAll(currentDummies);
593     }
594 }