2 * Copyright (c) 2010-2022 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.io.homekit.internal;
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;
21 import java.util.Map.Entry;
22 import java.util.Optional;
24 import java.util.concurrent.ScheduledExecutorService;
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;
42 import io.github.hapjava.accessories.HomekitAccessory;
43 import io.github.hapjava.server.impl.HomekitRoot;
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.
49 * @author Andy Lintner - Initial contribution
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;
65 private final Set<String> pendingUpdates = new HashSet<>();
67 private final ScheduledExecutorService scheduler = ThreadPoolManager
68 .getScheduledPool(ThreadPoolManager.THREAD_POOL_NAME_COMMON);
71 * Rather than reacting to item added/removed/modified changes directly, we mark them as dirty (and the groups to
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
78 private final Debouncer applyUpdatesDebouncer;
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>() {
90 public void added(final Metadata metadata) {
91 final MetadataKey uid = metadata.getUID();
92 if (HomekitAccessoryFactory.METADATA_KEY.equalsIgnoreCase(uid.getNamespace())) {
94 markDirty(itemRegistry.getItem(uid.getItemName()));
95 } catch (ItemNotFoundException e) {
96 logger.debug("Could not find item for metadata {}", metadata);
102 public void removed(final Metadata metadata) {
103 final MetadataKey uid = metadata.getUID();
104 if (HomekitAccessoryFactory.METADATA_KEY.equalsIgnoreCase(uid.getNamespace())) {
106 markDirty(itemRegistry.getItem(uid.getItemName()));
107 } catch (ItemNotFoundException e) {
108 logger.debug("Could not find item for metadata {}", metadata);
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())) {
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);
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());
135 private void initialiseRevision() {
138 String revisionString = storage.get(REVISION_CONFIG);
139 if (revisionString == null) {
140 throw new NumberFormatException();
142 revision = Integer.parseInt(revisionString);
143 } catch (NumberFormatException e) {
145 storage.put(REVISION_CONFIG, "" + revision);
148 String accessoryCountString = storage.get(ACCESSORY_COUNT);
149 if (accessoryCountString == null) {
150 throw new NumberFormatException();
152 lastAccessoryCount = Integer.parseInt(accessoryCountString);
153 } catch (NumberFormatException e) {
154 lastAccessoryCount = 0;
155 storage.put(ACCESSORY_COUNT, "" + accessoryRegistry.getAllAccessories().size());
157 accessoryRegistry.setConfigurationRevision(revision);
160 private boolean hasHomeKitMetadata(Item item) {
161 return metadataRegistry.get(new MetadataKey(HomekitAccessoryFactory.METADATA_KEY, item.getUID())) != null;
165 public synchronized void added(Item item) {
166 if (hasHomeKitMetadata(item)) {
172 public void allItemsChanged(Collection<String> oldItemNames) {
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.
180 * @param item The item that has been changed or removed.
182 private synchronized void markDirty(Item item) {
183 logger.trace("Mark dirty item {}", item.getName());
184 pendingUpdates.add(item.getName());
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.
189 for (Item accessoryGroup : HomekitAccessoryFactory.getAccessoryGroups(item, itemRegistry, metadataRegistry)) {
190 pendingUpdates.add(accessoryGroup.getName());
192 applyUpdatesDebouncer.call();
196 public synchronized void removed(Item item) {
197 if (hasHomeKitMetadata(item)) {
202 private Optional<Item> getItemOptional(String name) {
204 return Optional.of(itemRegistry.getItem(name));
205 } catch (ItemNotFoundException e) {
206 return Optional.empty();
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,
215 storage.put(REVISION_CONFIG, "" + newRevision);
216 storage.put(ACCESSORY_COUNT, "" + lastAccessoryCount);
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);
226 if (!pendingUpdates.isEmpty()) {
227 makeNewConfigurationRevision();
228 pendingUpdates.clear();
233 public void updated(Item oldElement, Item element) {
234 markDirty(oldElement);
238 public int getLastAccessoryCount() {
239 return lastAccessoryCount;
242 public synchronized void clearAccessories() {
243 accessoryRegistry.clear();
246 public synchronized void setBridge(HomekitRoot bridge) {
247 accessoryRegistry.setBridge(bridge);
250 public synchronized void unsetBridge() {
251 applyUpdatesDebouncer.stop();
252 accessoryRegistry.unsetBridge();
255 public void setUpdater(HomekitAccessoryUpdater updater) {
256 this.updater = updater;
259 public void updateSettings(HomekitSettings settings) {
260 this.settings = settings;
264 this.itemRegistry.removeRegistryChangeListener(this);
265 this.metadataRegistry.removeRegistryChangeListener(metadataChangeListener);
268 public Map<String, HomekitAccessory> getAccessories() {
269 return this.accessoryRegistry.getAllAccessories();
272 public int getConfigurationRevision() {
273 return this.accessoryRegistry.getConfigurationRevision();
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.
288 * // Single Line HomeKit Accessory
289 * Switch light "Light" {homekit="Lighting"}
291 * // One HomeKit accessory defined using group
292 * Group gLight "Light Group" {homekit="Lighting"}
293 * Switch light "Light" (gLight) {homekit="Lighting.OnState"}
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"}
299 * @param item openHAB item
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))));
314 private void createRootAccessory(HomekitTaggedItem taggedItem) {
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());