]> git.basschouten.com Git - openhab-addons.git/blob
2a44a93b80a0cb46683ad53bdf9a1be4005a6985
[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.time.Clock;
16 import java.time.Duration;
17 import java.util.Collection;
18 import java.util.HashSet;
19 import java.util.List;
20 import java.util.Map;
21 import java.util.Map.Entry;
22 import java.util.Optional;
23 import java.util.Set;
24 import java.util.concurrent.ScheduledExecutorService;
25
26 import org.eclipse.jdt.annotation.NonNullByDefault;
27 import org.openhab.core.common.ThreadPoolManager;
28 import org.openhab.core.common.registry.RegistryChangeListener;
29 import org.openhab.core.items.GroupItem;
30 import org.openhab.core.items.Item;
31 import org.openhab.core.items.ItemNotFoundException;
32 import org.openhab.core.items.ItemRegistry;
33 import org.openhab.core.items.ItemRegistryChangeListener;
34 import org.openhab.core.items.Metadata;
35 import org.openhab.core.items.MetadataKey;
36 import org.openhab.core.items.MetadataRegistry;
37 import org.openhab.core.storage.Storage;
38 import org.openhab.io.homekit.internal.accessories.HomekitAccessoryFactory;
39 import org.slf4j.Logger;
40 import org.slf4j.LoggerFactory;
41
42 import io.github.hapjava.accessories.HomekitAccessory;
43 import io.github.hapjava.server.impl.HomekitRoot;
44
45 /**
46  * Listens for changes to the item and metadata registry. When changes are detected, check
47  * for HomeKit tags and, if present, add the items to the HomekitAccessoryRegistry.
48  *
49  * @author Andy Lintner - Initial contribution
50  */
51 @NonNullByDefault
52 public class HomekitChangeListener implements ItemRegistryChangeListener {
53     private final Logger logger = LoggerFactory.getLogger(HomekitChangeListener.class);
54     private final static String REVISION_CONFIG = "revision";
55     private final static String ACCESSORY_COUNT = "accessory_count";
56     private final ItemRegistry itemRegistry;
57     private final HomekitAccessoryRegistry accessoryRegistry = new HomekitAccessoryRegistry();
58     private final MetadataRegistry metadataRegistry;
59     private final Storage<String> storage;
60     private final RegistryChangeListener<Metadata> metadataChangeListener;
61     private HomekitAccessoryUpdater updater = new HomekitAccessoryUpdater();
62     private HomekitSettings settings;
63     private int lastAccessoryCount;
64
65     private final Set<String> pendingUpdates = new HashSet<>();
66
67     private final ScheduledExecutorService scheduler = ThreadPoolManager
68             .getScheduledPool(ThreadPoolManager.THREAD_POOL_NAME_COMMON);
69
70     /**
71      * Rather than reacting to item added/removed/modified changes directly, we mark them as dirty (and the groups to
72      * which they belong)
73      *
74      * 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,
75      * rather than for each update at a time, preventing us from showing an error message with each addition until the
76      * group is complete.
77      */
78     private final Debouncer applyUpdatesDebouncer;
79
80     HomekitChangeListener(ItemRegistry itemRegistry, HomekitSettings settings, MetadataRegistry metadataRegistry,
81             Storage<String> storage) {
82         this.itemRegistry = itemRegistry;
83         this.settings = settings;
84         this.metadataRegistry = metadataRegistry;
85         this.storage = storage;
86         this.applyUpdatesDebouncer = new Debouncer("update-homekit-devices", scheduler, Duration.ofMillis(1000),
87                 Clock.systemUTC(), this::applyUpdates);
88         metadataChangeListener = new RegistryChangeListener<Metadata>() {
89             @Override
90             public void added(final Metadata metadata) {
91                 final MetadataKey uid = metadata.getUID();
92                 if (HomekitAccessoryFactory.METADATA_KEY.equalsIgnoreCase(uid.getNamespace())) {
93                     try {
94                         markDirty(itemRegistry.getItem(uid.getItemName()));
95                     } catch (ItemNotFoundException e) {
96                         logger.debug("Could not find item for metadata {}", metadata);
97                     }
98                 }
99             }
100
101             @Override
102             public void removed(final Metadata metadata) {
103                 final MetadataKey uid = metadata.getUID();
104                 if (HomekitAccessoryFactory.METADATA_KEY.equalsIgnoreCase(uid.getNamespace())) {
105                     try {
106                         markDirty(itemRegistry.getItem(uid.getItemName()));
107                     } catch (ItemNotFoundException e) {
108                         logger.debug("Could not find item for metadata {}", metadata);
109                     }
110                 }
111             }
112
113             @Override
114             public void updated(final Metadata oldMetadata, final Metadata newMetadata) {
115                 final MetadataKey oldUid = oldMetadata.getUID();
116                 final MetadataKey newUid = newMetadata.getUID();
117                 if (HomekitAccessoryFactory.METADATA_KEY.equalsIgnoreCase(oldUid.getNamespace())
118                         || HomekitAccessoryFactory.METADATA_KEY.equalsIgnoreCase(newUid.getNamespace())) {
119                     try {
120                         // the item name is same in old and new metadata, so we can take any.
121                         markDirty(itemRegistry.getItem(oldUid.getItemName()));
122                     } catch (ItemNotFoundException e) {
123                         logger.debug("Could not find item for metadata {}", oldMetadata);
124                     }
125                 }
126             }
127         };
128         itemRegistry.addRegistryChangeListener(this);
129         metadataRegistry.addRegistryChangeListener(metadataChangeListener);
130         itemRegistry.getItems().forEach(this::createRootAccessories);
131         initialiseRevision();
132         logger.info("Created {} HomeKit items.", accessoryRegistry.getAllAccessories().size());
133     }
134
135     private void initialiseRevision() {
136         int revision;
137         try {
138             String revisionString = storage.get(REVISION_CONFIG);
139             if (revisionString == null) {
140                 throw new NumberFormatException();
141             }
142             revision = Integer.parseInt(revisionString);
143         } catch (NumberFormatException e) {
144             revision = 1;
145             storage.put(REVISION_CONFIG, "" + revision);
146         }
147         try {
148             String accessoryCountString = storage.get(ACCESSORY_COUNT);
149             if (accessoryCountString == null) {
150                 throw new NumberFormatException();
151             }
152             lastAccessoryCount = Integer.parseInt(accessoryCountString);
153         } catch (NumberFormatException e) {
154             lastAccessoryCount = 0;
155             storage.put(ACCESSORY_COUNT, "" + accessoryRegistry.getAllAccessories().size());
156         }
157         accessoryRegistry.setConfigurationRevision(revision);
158     }
159
160     private boolean hasHomeKitMetadata(Item item) {
161         return metadataRegistry.get(new MetadataKey(HomekitAccessoryFactory.METADATA_KEY, item.getUID())) != null;
162     }
163
164     @Override
165     public synchronized void added(Item item) {
166         if (hasHomeKitMetadata(item)) {
167             markDirty(item);
168         }
169     }
170
171     @Override
172     public void allItemsChanged(Collection<String> oldItemNames) {
173         clearAccessories();
174     }
175
176     /**
177      * Mark an item as dirty, plus any accessory groups to which it pertains, so that after a debounce period the
178      * accessory update can be applied.
179      *
180      * @param item The item that has been changed or removed.
181      */
182     private synchronized void markDirty(Item item) {
183         logger.trace("Mark dirty item {}", item.getName());
184         pendingUpdates.add(item.getName());
185         /*
186          * If findMyAccessoryGroups fails because the accessory group has already been deleted, then we can count on a
187          * later update telling us that the accessory group was removed.
188          */
189         for (Item accessoryGroup : HomekitAccessoryFactory.getAccessoryGroups(item, itemRegistry, metadataRegistry)) {
190             pendingUpdates.add(accessoryGroup.getName());
191         }
192         applyUpdatesDebouncer.call();
193     }
194
195     @Override
196     public synchronized void removed(Item item) {
197         if (hasHomeKitMetadata(item)) {
198             markDirty(item);
199         }
200     }
201
202     private Optional<Item> getItemOptional(String name) {
203         try {
204             return Optional.of(itemRegistry.getItem(name));
205         } catch (ItemNotFoundException e) {
206             return Optional.empty();
207         }
208     }
209
210     public void makeNewConfigurationRevision() {
211         final int newRevision = accessoryRegistry.makeNewConfigurationRevision();
212         lastAccessoryCount = accessoryRegistry.getAllAccessories().size();
213         logger.trace("Make new configuration revision. new revision number {}, number of accessories {}", newRevision,
214                 lastAccessoryCount);
215         storage.put(REVISION_CONFIG, "" + newRevision);
216         storage.put(ACCESSORY_COUNT, "" + lastAccessoryCount);
217     }
218
219     private synchronized void applyUpdates() {
220         logger.trace("Apply updates");
221         for (final String name : pendingUpdates) {
222             accessoryRegistry.remove(name);
223             logger.trace(" Add items {}", name);
224             getItemOptional(name).ifPresent(this::createRootAccessories);
225         }
226         if (!pendingUpdates.isEmpty()) {
227             makeNewConfigurationRevision();
228             pendingUpdates.clear();
229         }
230     }
231
232     @Override
233     public void updated(Item oldElement, Item element) {
234         markDirty(oldElement);
235         markDirty(element);
236     }
237
238     public int getLastAccessoryCount() {
239         return lastAccessoryCount;
240     }
241
242     public synchronized void clearAccessories() {
243         accessoryRegistry.clear();
244     }
245
246     public synchronized void setBridge(HomekitRoot bridge) {
247         accessoryRegistry.setBridge(bridge);
248     }
249
250     public synchronized void unsetBridge() {
251         applyUpdatesDebouncer.stop();
252         accessoryRegistry.unsetBridge();
253     }
254
255     public void setUpdater(HomekitAccessoryUpdater updater) {
256         this.updater = updater;
257     }
258
259     public void updateSettings(HomekitSettings settings) {
260         this.settings = settings;
261     }
262
263     public void stop() {
264         this.itemRegistry.removeRegistryChangeListener(this);
265         this.metadataRegistry.removeRegistryChangeListener(metadataChangeListener);
266     }
267
268     public Map<String, HomekitAccessory> getAccessories() {
269         return this.accessoryRegistry.getAllAccessories();
270     }
271
272     public int getConfigurationRevision() {
273         return this.accessoryRegistry.getConfigurationRevision();
274     }
275
276     /**
277      * creates one or more HomeKit items for given openhab item.
278      * one OpenHAB item can linked to several HomeKit accessories or characteristics.
279      * OpenHAB Item is a good candidate for homeKit accessory IF
280      * - it has HomeKit accessory types, i.e. HomeKit accessory tag AND
281      * - has no group with HomeKit tag, i.e. single line accessory ODER
282      * - has groups with HomeKit tag, but all groups are with baseItem, e.g. Group:Switch,
283      * so that the groups already complete accessory and group members can be a standalone HomeKit accessory.
284      * In contrast, items which are part of groups without BaseItem are additional HomeKit characteristics of the
285      * accessory defined by that group and dont need to be created as RootAccessory here.
286      *
287      * Examples:
288      * // Single Line HomeKit Accessory
289      * Switch light "Light" {homekit="Lighting"}
290      *
291      * // One HomeKit accessory defined using group
292      * Group gLight "Light Group" {homekit="Lighting"}
293      * Switch light "Light" (gLight) {homekit="Lighting.OnState"}
294      *
295      * // 2 HomeKit accessories: one is switch attached to group, another one a single switch
296      * Group:Switch:OR(ON,OFF) gLight "Light Group " {homekit="Lighting"}
297      * Switch light "Light" (gLight) {homekit="Lighting.OnState"}
298      *
299      * @param item openHAB item
300      */
301     private void createRootAccessories(Item item) {
302         final List<Entry<HomekitAccessoryType, HomekitCharacteristicType>> accessoryTypes = HomekitAccessoryFactory
303                 .getAccessoryTypes(item, metadataRegistry);
304         final List<GroupItem> groups = HomekitAccessoryFactory.getAccessoryGroups(item, itemRegistry, metadataRegistry);
305         if (!accessoryTypes.isEmpty()
306                 && (groups.isEmpty() || groups.stream().noneMatch(g -> g.getBaseItem() == null))) {
307             logger.trace("Item {} is a HomeKit accessory of types {}", item.getName(), accessoryTypes);
308             final HomekitOHItemProxy itemProxy = new HomekitOHItemProxy(item);
309             accessoryTypes.forEach(rootAccessory -> createRootAccessory(new HomekitTaggedItem(itemProxy,
310                     rootAccessory.getKey(), HomekitAccessoryFactory.getItemConfiguration(item, metadataRegistry))));
311         }
312     }
313
314     private void createRootAccessory(HomekitTaggedItem taggedItem) {
315         try {
316             accessoryRegistry.addRootAccessory(taggedItem.getName(),
317                     HomekitAccessoryFactory.create(taggedItem, metadataRegistry, updater, settings));
318         } catch (HomekitException e) {
319             logger.warn("Could not add device {}: {}", taggedItem.getItem().getUID(), e.getMessage());
320         }
321     }
322 }