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