]> git.basschouten.com Git - openhab-addons.git/blob
c1e0e46e1399f50369679fa0644fa74cd381a4c0
[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.eclipse.jdt.annotation.Nullable;
28 import org.openhab.core.common.ThreadPoolManager;
29 import org.openhab.core.common.registry.RegistryChangeListener;
30 import org.openhab.core.items.GroupItem;
31 import org.openhab.core.items.Item;
32 import org.openhab.core.items.ItemNotFoundException;
33 import org.openhab.core.items.ItemRegistry;
34 import org.openhab.core.items.ItemRegistryChangeListener;
35 import org.openhab.core.items.Metadata;
36 import org.openhab.core.items.MetadataKey;
37 import org.openhab.core.items.MetadataRegistry;
38 import org.openhab.core.storage.Storage;
39 import org.openhab.io.homekit.internal.accessories.HomekitAccessoryFactory;
40 import org.slf4j.Logger;
41 import org.slf4j.LoggerFactory;
42
43 import io.github.hapjava.accessories.HomekitAccessory;
44 import io.github.hapjava.server.impl.HomekitRoot;
45
46 /**
47  * Listens for changes to the item and metadata registry. When changes are detected, check
48  * for HomeKit tags and, if present, add the items to the HomekitAccessoryRegistry.
49  *
50  * @author Andy Lintner - Initial contribution
51  */
52 @NonNullByDefault
53 public class HomekitChangeListener implements ItemRegistryChangeListener {
54     private final Logger logger = LoggerFactory.getLogger(HomekitChangeListener.class);
55     private final static String REVISION_CONFIG = "revision";
56     private final static String ACCESSORY_COUNT = "accessory_count";
57     private final ItemRegistry itemRegistry;
58     private final HomekitAccessoryRegistry accessoryRegistry = new HomekitAccessoryRegistry();
59     private final MetadataRegistry metadataRegistry;
60     private final Storage<String> storage;
61     private final RegistryChangeListener<Metadata> metadataChangeListener;
62     private HomekitAccessoryUpdater updater = new HomekitAccessoryUpdater();
63     private HomekitSettings settings;
64     private int lastAccessoryCount;
65     private int instance;
66
67     private final Set<String> pendingUpdates = new HashSet<>();
68
69     private final ScheduledExecutorService scheduler = ThreadPoolManager
70             .getScheduledPool(ThreadPoolManager.THREAD_POOL_NAME_COMMON);
71
72     /**
73      * Rather than reacting to item added/removed/modified changes directly, we mark them as dirty (and the groups to
74      * which they belong)
75      *
76      * 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,
77      * rather than for each update at a time, preventing us from showing an error message with each addition until the
78      * group is complete.
79      */
80     private final Debouncer applyUpdatesDebouncer;
81
82     HomekitChangeListener(ItemRegistry itemRegistry, HomekitSettings settings, MetadataRegistry metadataRegistry,
83             Storage<String> storage, int instance) {
84         this.itemRegistry = itemRegistry;
85         this.settings = settings;
86         this.metadataRegistry = metadataRegistry;
87         this.storage = storage;
88         this.instance = instance;
89         this.applyUpdatesDebouncer = new Debouncer("update-homekit-devices", scheduler, Duration.ofMillis(1000),
90                 Clock.systemUTC(), this::applyUpdates);
91         metadataChangeListener = new RegistryChangeListener<Metadata>() {
92             @Override
93             public void added(final Metadata metadata) {
94                 final MetadataKey uid = metadata.getUID();
95                 if (HomekitAccessoryFactory.METADATA_KEY.equalsIgnoreCase(uid.getNamespace())) {
96                     try {
97                         markDirty(itemRegistry.getItem(uid.getItemName()));
98                     } catch (ItemNotFoundException e) {
99                         logger.debug("Could not find item for metadata {}", metadata);
100                     }
101                 }
102             }
103
104             @Override
105             public void removed(final Metadata metadata) {
106                 final MetadataKey uid = metadata.getUID();
107                 if (HomekitAccessoryFactory.METADATA_KEY.equalsIgnoreCase(uid.getNamespace())) {
108                     try {
109                         markDirty(itemRegistry.getItem(uid.getItemName()));
110                     } catch (ItemNotFoundException e) {
111                         logger.debug("Could not find item for metadata {}", metadata);
112                     }
113                 }
114             }
115
116             @Override
117             public void updated(final Metadata oldMetadata, final Metadata newMetadata) {
118                 final MetadataKey oldUid = oldMetadata.getUID();
119                 final MetadataKey newUid = newMetadata.getUID();
120                 if (HomekitAccessoryFactory.METADATA_KEY.equalsIgnoreCase(oldUid.getNamespace())
121                         || HomekitAccessoryFactory.METADATA_KEY.equalsIgnoreCase(newUid.getNamespace())) {
122                     try {
123                         // the item name is same in old and new metadata, so we can take any.
124                         markDirty(itemRegistry.getItem(oldUid.getItemName()));
125                     } catch (ItemNotFoundException e) {
126                         logger.debug("Could not find item for metadata {}", oldMetadata);
127                     }
128                 }
129             }
130         };
131         itemRegistry.addRegistryChangeListener(this);
132         metadataRegistry.addRegistryChangeListener(metadataChangeListener);
133         itemRegistry.getItems().forEach(this::createRootAccessories);
134         initialiseRevision();
135         makeNewConfigurationRevision();
136         logger.info("Created {} HomeKit items in instance {}.", accessoryRegistry.getAllAccessories().size(), instance);
137     }
138
139     private void initialiseRevision() {
140         int revision;
141         try {
142             String revisionString = storage.get(REVISION_CONFIG);
143             if (revisionString == null) {
144                 throw new NumberFormatException();
145             }
146             revision = Integer.parseInt(revisionString);
147         } catch (NumberFormatException e) {
148             revision = 1;
149             storage.put(REVISION_CONFIG, "" + revision);
150         }
151         try {
152             String accessoryCountString = storage.get(ACCESSORY_COUNT);
153             if (accessoryCountString == null) {
154                 throw new NumberFormatException();
155             }
156             lastAccessoryCount = Integer.parseInt(accessoryCountString);
157         } catch (NumberFormatException e) {
158             lastAccessoryCount = 0;
159             storage.put(ACCESSORY_COUNT, "" + accessoryRegistry.getAllAccessories().size());
160         }
161         accessoryRegistry.setConfigurationRevision(revision);
162     }
163
164     private boolean hasHomeKitMetadata(Item item) {
165         return metadataRegistry.get(new MetadataKey(HomekitAccessoryFactory.METADATA_KEY, item.getUID())) != null;
166     }
167
168     @Override
169     public synchronized void added(Item item) {
170         if (hasHomeKitMetadata(item)) {
171             markDirty(item);
172         }
173     }
174
175     @Override
176     public void allItemsChanged(Collection<String> oldItemNames) {
177         clearAccessories();
178     }
179
180     /**
181      * Mark an item as dirty, plus any accessory groups to which it pertains, so that after a debounce period the
182      * accessory update can be applied.
183      *
184      * @param item The item that has been changed or removed.
185      */
186     private synchronized void markDirty(Item item) {
187         logger.trace("Mark dirty item {}", item.getName());
188         pendingUpdates.add(item.getName());
189         /*
190          * If findMyAccessoryGroups fails because the accessory group has already been deleted, then we can count on a
191          * later update telling us that the accessory group was removed.
192          */
193         for (Item accessoryGroup : HomekitAccessoryFactory.getAccessoryGroups(item, itemRegistry, metadataRegistry)) {
194             pendingUpdates.add(accessoryGroup.getName());
195         }
196
197         /*
198          * if metadata of a group item was changed, mark all group member as dirty.
199          */
200         if (item instanceof GroupItem) {
201             ((GroupItem) item).getMembers().forEach(groupMember -> pendingUpdates.add(groupMember.getName()));
202         }
203         applyUpdatesDebouncer.call();
204     }
205
206     @Override
207     public synchronized void removed(Item item) {
208         if (hasHomeKitMetadata(item)) {
209             markDirty(item);
210         }
211     }
212
213     private Optional<Item> getItemOptional(String name) {
214         try {
215             return Optional.of(itemRegistry.getItem(name));
216         } catch (ItemNotFoundException e) {
217             return Optional.empty();
218         }
219     }
220
221     public void makeNewConfigurationRevision() {
222         final int newRevision = accessoryRegistry.makeNewConfigurationRevision();
223         lastAccessoryCount = accessoryRegistry.getAllAccessories().size();
224         logger.trace("Make new configuration revision. new revision number {}, number of accessories {}", newRevision,
225                 lastAccessoryCount);
226         storage.put(REVISION_CONFIG, "" + newRevision);
227         storage.put(ACCESSORY_COUNT, "" + lastAccessoryCount);
228     }
229
230     private synchronized void applyUpdates() {
231         logger.trace("Apply updates");
232         for (final String name : pendingUpdates) {
233             accessoryRegistry.remove(name);
234             logger.trace(" Add items {}", name);
235             getItemOptional(name).ifPresent(this::createRootAccessories);
236         }
237         if (!pendingUpdates.isEmpty()) {
238             makeNewConfigurationRevision();
239             pendingUpdates.clear();
240         }
241     }
242
243     @Override
244     public void updated(Item oldElement, Item element) {
245         markDirty(oldElement);
246         markDirty(element);
247     }
248
249     public int getLastAccessoryCount() {
250         return lastAccessoryCount;
251     }
252
253     public synchronized void clearAccessories() {
254         accessoryRegistry.clear();
255     }
256
257     public synchronized void setBridge(HomekitRoot bridge) {
258         accessoryRegistry.setBridge(bridge);
259     }
260
261     public void setUpdater(HomekitAccessoryUpdater updater) {
262         this.updater = updater;
263     }
264
265     public void updateSettings(HomekitSettings settings) {
266         this.settings = settings;
267     }
268
269     public synchronized void stop() {
270         this.itemRegistry.removeRegistryChangeListener(this);
271         this.metadataRegistry.removeRegistryChangeListener(metadataChangeListener);
272         applyUpdatesDebouncer.stop();
273         accessoryRegistry.unsetBridge();
274     }
275
276     public Map<String, HomekitAccessory> getAccessories() {
277         return this.accessoryRegistry.getAllAccessories();
278     }
279
280     public int getConfigurationRevision() {
281         return this.accessoryRegistry.getConfigurationRevision();
282     }
283
284     /**
285      * select primary accessory type from list of types.
286      * selection logic:
287      * - if accessory has only one type, it is the primary type
288      * - if accessory has no primary type defined per configuration, then the first type on the list is the primary type
289      * - if accessory has primary type defined per configuration and this type is on the list of types, then it is the
290      * primary
291      * - if accessory has primary type defined per configuration and this type is NOT on the list of types, then the
292      * first type on the list is the primary type
293      *
294      * @param item openhab item
295      * @param accessoryTypes list of accessory type attached to the item
296      * @return primary accessory type
297      */
298     private HomekitAccessoryType getPrimaryAccessoryType(Item item,
299             List<Entry<HomekitAccessoryType, HomekitCharacteristicType>> accessoryTypes,
300             @Nullable Map<String, Object> configuration) {
301         if (accessoryTypes.size() > 1 && configuration != null) {
302             final @Nullable Object value = configuration.get(HomekitTaggedItem.PRIMARY_SERVICE);
303             if (value instanceof String) {
304                 return accessoryTypes.stream()
305                         .filter(aType -> ((String) value).equalsIgnoreCase(aType.getKey().getTag())).findAny()
306                         .orElse(accessoryTypes.get(0)).getKey();
307             }
308         }
309         // no primary accessory found or there is only one type, so return the first type from the list
310         return accessoryTypes.get(0).getKey();
311     }
312
313     /**
314      * creates one or more HomeKit items for given openhab item.
315      * one OpenHAB item can be linked to several HomeKit accessories.
316      * OpenHAB item is a good candidate for a HomeKit accessory
317      * IF
318      * - it has HomeKit accessory types defined using HomeKit accessory metadata
319      * - AND is not part of a group with HomeKit metadata
320      * e.g.
321      * Switch light "Light" {homekit="Lighting"}
322      * Group gLight "Light Group" {homekit="Lighting"}
323      *
324      * OR
325      * - it has HomeKit accessory types defined using HomeKit accessory metadata
326      * - AND is part of groups with HomeKit metadata, but all groups have baseItem
327      * e.g.
328      * Group:Switch:OR(ON,OFF) gLight "Light Group " {homekit="Lighting"}
329      * Switch light "Light" (gLight) {homekit="Lighting.OnState"}
330      *
331      *
332      * In contrast, items which are part of groups without BaseItem are additional HomeKit characteristics of the
333      * accessory defined by that group and don't need to be created as accessory here.
334      * e.g.
335      * Group gLight "Light Group " {homekit="Lighting"}
336      * Switch light "Light" (gLight) {homekit="Lighting.OnState"}
337      * is not the root accessory but only a characteristic "OnState"
338      *
339      * Examples:
340      * // Single line HomeKit Accessory
341      * Switch light "Light" {homekit="Lighting"}
342      *
343      * // One HomeKit accessory defined using group
344      * Group gLight "Light Group" {homekit="Lighting"}
345      * Switch light "Light" (gLight) {homekit="Lighting.OnState"}
346      *
347      * // 2 HomeKit accessories: one is switch attached to group, another one a single switch
348      * Group:Switch:OR(ON,OFF) gLight "Light Group " {homekit="Lighting"}
349      * Switch light "Light" (gLight) {homekit="Lighting.OnState"}
350      *
351      * @param item openHAB item
352      */
353     private void createRootAccessories(Item item) {
354         final List<Entry<HomekitAccessoryType, HomekitCharacteristicType>> accessoryTypes = HomekitAccessoryFactory
355                 .getAccessoryTypes(item, metadataRegistry);
356         final List<GroupItem> groups = HomekitAccessoryFactory.getAccessoryGroups(item, itemRegistry, metadataRegistry);
357         final @Nullable Map<String, Object> itemConfiguration = HomekitAccessoryFactory.getItemConfiguration(item,
358                 metadataRegistry);
359         if (accessoryTypes.isEmpty() || !(groups.isEmpty() || groups.stream().noneMatch(g -> g.getBaseItem() == null))
360                 || !itemIsForThisBridge(item, itemConfiguration)) {
361             return;
362         }
363
364         final HomekitAccessoryType primaryAccessoryType = getPrimaryAccessoryType(item, accessoryTypes,
365                 itemConfiguration);
366         logger.trace("Item {} is a HomeKit accessory of types {}. Primary type is {}", item.getName(), accessoryTypes,
367                 primaryAccessoryType);
368         final HomekitOHItemProxy itemProxy = new HomekitOHItemProxy(item);
369         final HomekitTaggedItem taggedItem = new HomekitTaggedItem(new HomekitOHItemProxy(item), primaryAccessoryType,
370                 itemConfiguration);
371         try {
372             final HomekitAccessory accessory = HomekitAccessoryFactory.create(taggedItem, metadataRegistry, updater,
373                     settings);
374
375             accessoryTypes.stream().filter(aType -> !primaryAccessoryType.equals(aType.getKey()))
376                     .forEach(additionalAccessoryType -> {
377                         final HomekitTaggedItem additionalTaggedItem = new HomekitTaggedItem(itemProxy,
378                                 additionalAccessoryType.getKey(), itemConfiguration);
379                         try {
380                             final HomekitAccessory additionalAccessory = HomekitAccessoryFactory
381                                     .create(additionalTaggedItem, metadataRegistry, updater, settings);
382                             accessory.getServices().add(additionalAccessory.getPrimaryService());
383                         } catch (HomekitException e) {
384                             logger.warn("Cannot create additional accessory {}", additionalTaggedItem);
385                         }
386                     });
387             accessoryRegistry.addRootAccessory(taggedItem.getName(), accessory);
388         } catch (HomekitException e) {
389             logger.warn("Cannot create accessory {}", taggedItem);
390         }
391     }
392
393     private boolean itemIsForThisBridge(Item item, @Nullable Map<String, Object> configuration) {
394         // non-tagged accessories belong to the first instance
395         if (configuration == null) {
396             return (instance == 1);
397         }
398
399         final @Nullable Object value = configuration.get(HomekitTaggedItem.INSTANCE);
400         if (value == null) {
401             return (instance == 1);
402         }
403         if (value instanceof Number) {
404             return (instance == ((Number) value).intValue());
405         }
406         logger.warn("Unrecognized instance tag {} ({}) for item {}; assigning to default instance.", value,
407                 value.getClass(), item.getName());
408         return (instance == 1);
409     }
410 }