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