2 * Copyright (c) 2010-2023 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.util.Arrays;
16 import java.util.Collection;
17 import java.util.List;
18 import java.util.concurrent.ExecutionException;
20 import org.eclipse.jdt.annotation.NonNullByDefault;
21 import org.eclipse.jdt.annotation.Nullable;
22 import org.openhab.core.io.console.Console;
23 import org.openhab.core.io.console.ConsoleCommandCompleter;
24 import org.openhab.core.io.console.StringsCompleter;
25 import org.openhab.core.io.console.extensions.AbstractConsoleCommandExtension;
26 import org.openhab.core.io.console.extensions.ConsoleCommandExtension;
27 import org.openhab.io.homekit.Homekit;
28 import org.openhab.io.homekit.internal.accessories.DummyHomekitAccessory;
29 import org.osgi.service.component.annotations.Component;
30 import org.osgi.service.component.annotations.Reference;
31 import org.slf4j.Logger;
32 import org.slf4j.LoggerFactory;
34 import io.github.hapjava.accessories.HomekitAccessory;
35 import io.github.hapjava.services.Service;
38 * Console commands for interacting with the HomeKit integration
40 * @author Andy Lintner - Initial contribution
42 @Component(service = ConsoleCommandExtension.class)
44 public class HomekitCommandExtension extends AbstractConsoleCommandExtension {
45 private static final String SUBCMD_CLEAR_PAIRINGS = "clearPairings";
46 private static final String SUBCMD_LIST_ACCESSORIES = "list";
47 private static final String SUBCMD_PRINT_ACCESSORY = "show";
48 private static final String SUBCMD_ALLOW_UNAUTHENTICATED = "allowUnauthenticated";
49 private static final String SUBCMD_PRUNE_DUMMY_ACCESSORIES = "pruneDummyAccessories";
50 private static final String SUBCMD_LIST_DUMMY_ACCESSORIES = "listDummyAccessories";
51 private static final StringsCompleter SUBCMD_COMPLETER = new StringsCompleter(
52 List.of(SUBCMD_CLEAR_PAIRINGS, SUBCMD_LIST_ACCESSORIES, SUBCMD_PRINT_ACCESSORY,
53 SUBCMD_ALLOW_UNAUTHENTICATED, SUBCMD_PRUNE_DUMMY_ACCESSORIES, SUBCMD_LIST_DUMMY_ACCESSORIES),
56 private static final String PARAM_INSTANCE = "--instance";
57 private static final String PARAM_INSTANCE_HELP = " [--instance <instance id>]";
59 private class CommandCompleter implements ConsoleCommandCompleter {
61 public boolean complete(String[] args, int cursorArgumentIndex, int cursorPosition, List<String> candidates) {
62 if (cursorArgumentIndex == 0) {
63 return SUBCMD_COMPLETER.complete(args, cursorArgumentIndex, cursorPosition, candidates);
69 private final Logger logger = LoggerFactory.getLogger(HomekitCommandExtension.class);
71 private @NonNullByDefault({}) Homekit homekit;
73 public HomekitCommandExtension() {
74 super("homekit", "Interact with the HomeKit integration.");
78 public void execute(String[] argsArray, Console console) {
79 if (argsArray.length > 0) {
80 List<String> args = Arrays.asList(argsArray);
81 Integer instance = null;
83 // capture the common instance argument and take it out of args
84 for (int i = 0; i < args.size() - 1; ++i) {
85 if (PARAM_INSTANCE.equals(args.get(i))) {
86 instance = Integer.parseInt(args.get(i + 1));
87 int instanceCount = homekit.getInstanceCount();
88 if (instance < 1 || instance > instanceCount) {
89 console.println("Instance " + args.get(i + 1) + " out of range 1.." + instanceCount);
93 List<String> newArgs = args.subList(0, i);
94 if (i < args.size() - 2) {
95 newArgs.addAll(args.subList(i + 2, args.size() - 1));
102 String subCommand = args.get(0);
103 switch (subCommand) {
104 case SUBCMD_CLEAR_PAIRINGS:
105 if (args.size() != 1) {
106 console.println("Unknown arguments; not clearing pairings");
108 clearHomekitPairings(console, instance);
112 case SUBCMD_ALLOW_UNAUTHENTICATED:
113 if (args.size() > 1) {
114 boolean allow = Boolean.parseBoolean(args.get(1));
115 allowUnauthenticatedHomekitRequests(allow, console);
117 console.println("true/false is required as an argument");
120 case SUBCMD_LIST_ACCESSORIES:
121 listAccessories(console, instance);
123 case SUBCMD_PRINT_ACCESSORY:
124 if (args.size() > 1) {
125 printAccessory(args.get(1), console, instance);
127 console.println("accessory id or name is required as an argument");
130 case SUBCMD_PRUNE_DUMMY_ACCESSORIES:
131 if (args.size() != 1) {
132 console.println("Unknown arguments; not pruning dummy accessories");
134 pruneDummyAccessories(console, instance);
137 case SUBCMD_LIST_DUMMY_ACCESSORIES:
138 listDummyAccessories(console, instance);
141 console.println("Unknown command '" + subCommand + "'");
151 public List<String> getUsages() {
152 return Arrays.asList(
153 buildCommandUsage(SUBCMD_LIST_ACCESSORIES + PARAM_INSTANCE_HELP,
154 "list all HomeKit accessories, optionally for a specific instance."),
155 buildCommandUsage(SUBCMD_PRINT_ACCESSORY + PARAM_INSTANCE_HELP + " <accessory id | accessory name>",
156 "print additional details of the accessories which partially match provided ID or name, optionally searching a specific instance."),
157 buildCommandUsage(SUBCMD_CLEAR_PAIRINGS + PARAM_INSTANCE_HELP,
158 "removes all pairings with HomeKit clients, optionally for a specific instance."),
159 buildCommandUsage(SUBCMD_ALLOW_UNAUTHENTICATED + " <boolean>",
160 "enables or disables unauthenticated access to facilitate debugging"),
161 buildCommandUsage(SUBCMD_PRUNE_DUMMY_ACCESSORIES + PARAM_INSTANCE_HELP,
162 "removes dummy accessories whose items no longer exist, optionally for a specific instance."),
163 buildCommandUsage(SUBCMD_LIST_DUMMY_ACCESSORIES + PARAM_INSTANCE_HELP,
164 "list dummy accessories whose items no longer exist, optionally for a specific instance."));
168 public void setHomekit(Homekit homekit) {
169 this.homekit = homekit;
173 public @Nullable ConsoleCommandCompleter getCompleter() {
174 return new CommandCompleter();
177 private void clearHomekitPairings(Console console, @Nullable Integer instance) {
178 if (instance != null) {
179 homekit.clearHomekitPairings(instance);
180 console.println("Cleared HomeKit pairings for instance " + instance);
182 homekit.clearHomekitPairings();
183 console.println("Cleared HomeKit pairings");
187 private void allowUnauthenticatedHomekitRequests(boolean allow, Console console) {
188 homekit.allowUnauthenticatedRequests(allow);
189 console.println((allow ? "Enabled " : "Disabled ") + "unauthenticated HomeKit access");
192 private void pruneDummyAccessories(Console console, @Nullable Integer instance) {
193 if (instance != null) {
194 homekit.pruneDummyAccessories(instance);
195 console.println("Dummy accessories pruned for instance " + instance);
197 homekit.pruneDummyAccessories();
198 console.println("Dummy accessories pruned");
202 private void listAccessories(Console console, @Nullable Integer instance) {
203 getInstanceAccessories(instance).forEach(v -> {
205 console.println(v.getId() + " " + v.getName().get());
206 } catch (InterruptedException | ExecutionException e) {
207 logger.warn("Cannot list accessories", e);
212 private void listDummyAccessories(Console console, @Nullable Integer instance) {
213 getInstanceAccessories(instance).forEach(v -> {
215 if (v instanceof DummyHomekitAccessory) {
216 console.println(v.getSerialNumber().get());
218 } catch (InterruptedException | ExecutionException e) {
219 logger.warn("Cannot list accessories", e);
224 private void printService(Console console, Service service, int indent) {
225 console.println(" ".repeat(indent) + "Service Type: " + service.getClass().getSimpleName() + " ("
226 + service.getType() + ")");
227 console.println(" ".repeat(indent + 2) + "Characteristics:");
228 service.getCharacteristics().forEach((c) -> {
231 " ".repeat(indent + 4) + c.getClass().getSimpleName() + ": " + c.toJson(0).get().toString());
232 } catch (InterruptedException | ExecutionException e) {
235 if (service.getLinkedServices().isEmpty()) {
238 console.println(" ".repeat(indent + 2) + "Linked Services:");
239 service.getLinkedServices().forEach((s) -> printService(console, s, indent + 2));
242 private void printAccessory(String id, Console console, @Nullable Integer instance) {
243 getInstanceAccessories(instance).forEach(v -> {
245 if (("" + v.getId()).contains(id) || ((v.getName().get() != null)
246 && (v.getName().get().toUpperCase().contains(id.toUpperCase())))) {
247 console.println(v.getId() + " " + v.getName().get());
248 console.println("Services:");
249 v.getServices().forEach(s -> printService(console, s, 2));
252 } catch (InterruptedException | ExecutionException e) {
253 logger.warn("Cannot print accessory", e);
259 * Get in-scope accessories
261 * @param instance if null, means all accessories from all instances
263 private Collection<HomekitAccessory> getInstanceAccessories(@Nullable Integer instance) {
264 if (instance != null) {
265 return homekit.getAccessories(instance);
267 return homekit.getAccessories();