]> git.basschouten.com Git - openhab-addons.git/blob
72469d23f408d21d704d1023c8d1b1ac5e951433
[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.hueemulation.internal;
14
15 import java.net.InetAddress;
16 import java.net.UnknownHostException;
17 import java.util.Collections;
18 import java.util.IllegalFormatException;
19 import java.util.LinkedHashSet;
20 import java.util.Map;
21 import java.util.Set;
22 import java.util.UUID;
23 import java.util.concurrent.ScheduledExecutorService;
24 import java.util.concurrent.ScheduledFuture;
25 import java.util.concurrent.TimeUnit;
26 import java.util.stream.Collectors;
27 import java.util.stream.Stream;
28
29 import org.eclipse.jdt.annotation.NonNullByDefault;
30 import org.eclipse.jdt.annotation.Nullable;
31 import org.openhab.core.common.ThreadPoolManager;
32 import org.openhab.core.config.core.ConfigurableService;
33 import org.openhab.core.config.core.Configuration;
34 import org.openhab.core.items.Item;
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.net.CidrAddress;
39 import org.openhab.core.net.NetUtil;
40 import org.openhab.core.net.NetworkAddressService;
41 import org.openhab.io.hueemulation.internal.dto.HueAuthorizedConfig;
42 import org.openhab.io.hueemulation.internal.dto.HueDataStore;
43 import org.openhab.io.hueemulation.internal.dto.HueGroupEntry;
44 import org.openhab.io.hueemulation.internal.dto.HueLightEntry;
45 import org.openhab.io.hueemulation.internal.dto.HueRuleEntry;
46 import org.openhab.io.hueemulation.internal.dto.HueSensorEntry;
47 import org.openhab.io.hueemulation.internal.dto.response.HueSuccessGeneric;
48 import org.openhab.io.hueemulation.internal.dto.response.HueSuccessResponseStateChanged;
49 import org.osgi.service.cm.ConfigurationAdmin;
50 import org.osgi.service.component.annotations.Activate;
51 import org.osgi.service.component.annotations.Component;
52 import org.osgi.service.component.annotations.Deactivate;
53 import org.osgi.service.component.annotations.Modified;
54 import org.osgi.service.component.annotations.Reference;
55 import org.osgi.service.event.Event;
56 import org.osgi.service.event.EventAdmin;
57 import org.slf4j.Logger;
58 import org.slf4j.LoggerFactory;
59
60 import com.google.gson.Gson;
61 import com.google.gson.GsonBuilder;
62
63 /**
64  * This component sets up the hue data store and gets the service configuration.
65  * It also determines the address for the upnp service by the given configuration.
66  * <p>
67  * Also manages the pairing timeout. The service is restarted after a pairing timeout, due to the ConfigAdmin
68  * configuration change.
69  * <p>
70  * This is a central component and required by all other components and may not
71  * depend on anything in this bundle.
72  *
73  * @author David Graeff - Initial contribution
74  */
75 @Component(immediate = false, service = ConfigStore.class, configurationPid = HueEmulationService.CONFIG_PID)
76 @ConfigurableService(category = "io", label = "Hue Emulation", description_uri = "io:hueemulation")
77 @NonNullByDefault
78 public class ConfigStore {
79
80     public static final String METAKEY = "HUEEMU";
81     public static final String EVENT_ADDRESS_CHANGED = "HUE_EMU_CONFIG_ADDR_CHANGED";
82
83     private final Logger logger = LoggerFactory.getLogger(ConfigStore.class);
84
85     public HueDataStore ds = new HueDataStore();
86
87     protected @NonNullByDefault({}) ScheduledExecutorService scheduler;
88     private @Nullable ScheduledFuture<?> pairingOffFuture;
89     private @Nullable ScheduledFuture<?> writeUUIDFuture;
90
91     /**
92      * This is the main gson instance, to be obtained by all components that operate on the dto data fields
93      */
94     public final Gson gson = new GsonBuilder().registerTypeAdapter(HueLightEntry.class, new HueLightEntry.Serializer())
95             .registerTypeAdapter(HueSensorEntry.class, new HueSensorEntry.Serializer())
96             .registerTypeAdapter(HueRuleEntry.Condition.class, new HueRuleEntry.SerializerCondition())
97             .registerTypeAdapter(HueAuthorizedConfig.class, new HueAuthorizedConfig.Serializer())
98             .registerTypeAdapter(HueSuccessGeneric.class, new HueSuccessGeneric.Serializer())
99             .registerTypeAdapter(HueSuccessResponseStateChanged.class, new HueSuccessResponseStateChanged.Serializer())
100             .registerTypeAdapter(HueGroupEntry.class, new HueGroupEntry.Serializer(this)).create();
101
102     @Reference
103     protected @NonNullByDefault({}) ConfigurationAdmin configAdmin;
104
105     @Reference
106     protected @NonNullByDefault({}) NetworkAddressService networkAddressService;
107
108     @Reference
109     protected @NonNullByDefault({}) MetadataRegistry metadataRegistry;
110
111     @Reference
112     protected @NonNullByDefault({}) EventAdmin eventAdmin;
113
114     //// objects, set within activate()
115     private Set<InetAddress> discoveryIps = Collections.emptySet();
116     protected volatile @NonNullByDefault({}) HueEmulationConfig config;
117
118     public Set<String> switchFilter = Collections.emptySet();
119     public Set<String> colorFilter = Collections.emptySet();
120     public Set<String> whiteFilter = Collections.emptySet();
121     public Set<String> ignoreItemsFilter = Collections.emptySet();
122
123     private int highestAssignedHueID = 1;
124
125     private String hueIDPrefix = "";
126
127     public ConfigStore() {
128         scheduler = ThreadPoolManager.getScheduledPool(ThreadPoolManager.THREAD_POOL_NAME_COMMON);
129     }
130
131     /**
132      * For test dependency injection
133      *
134      * @param networkAddressService The network address service
135      * @param configAdmin The configuration admin service
136      * @param metadataRegistry The metadataRegistry service
137      */
138     public ConfigStore(NetworkAddressService networkAddressService, ConfigurationAdmin configAdmin,
139             @Nullable MetadataRegistry metadataRegistry, ScheduledExecutorService scheduler) {
140         this.networkAddressService = networkAddressService;
141         this.configAdmin = configAdmin;
142         this.metadataRegistry = metadataRegistry;
143         this.scheduler = scheduler;
144     }
145
146     @Activate
147     public void activate(Map<String, Object> properties) {
148         this.config = new Configuration(properties).as(HueEmulationConfig.class);
149
150         determineHighestAssignedHueID();
151
152         if (config.uuid.isEmpty()) {
153             config.uuid = UUID.randomUUID().toString();
154             writeUUIDFuture = scheduler.schedule(() -> {
155                 logger.info("No unique ID assigned yet. Assigning {} and restarting...", config.uuid);
156                 WriteConfig.setUUID(configAdmin, config.uuid);
157             }, 100, TimeUnit.MILLISECONDS);
158             return;
159         } else {
160             modified(properties);
161         }
162     }
163
164     private @Nullable InetAddress byName(@Nullable String address) {
165         if (address == null) {
166             return null;
167         }
168         try {
169             return InetAddress.getByName(address);
170         } catch (UnknownHostException e) {
171             logger.warn("Given IP address could not be resolved: {}", address, e);
172             return null;
173         }
174     }
175
176     @Modified
177     public void modified(Map<String, Object> properties) {
178         this.config = new Configuration(properties).as(HueEmulationConfig.class);
179
180         switchFilter = Collections.unmodifiableSet(
181                 Stream.of(config.restrictToTagsSwitches.split(",")).map(String::trim).collect(Collectors.toSet()));
182
183         colorFilter = Collections.unmodifiableSet(
184                 Stream.of(config.restrictToTagsColorLights.split(",")).map(String::trim).collect(Collectors.toSet()));
185
186         whiteFilter = Collections.unmodifiableSet(
187                 Stream.of(config.restrictToTagsWhiteLights.split(",")).map(String::trim).collect(Collectors.toSet()));
188
189         ignoreItemsFilter = Collections.unmodifiableSet(
190                 Stream.of(config.ignoreItemsWithTags.split(",")).map(String::trim).collect(Collectors.toSet()));
191
192         // Use either the user configured
193         InetAddress configuredAddress = null;
194         int networkPrefixLength = 24; // Default for most networks: 255.255.255.0
195
196         if (config.discoveryIp != null) {
197             discoveryIps = Collections.unmodifiableSet(Stream.of(config.discoveryIp.split(",")).map(String::trim)
198                     .map(this::byName).filter(e -> e != null).collect(Collectors.toSet()));
199         } else {
200             discoveryIps = new LinkedHashSet<>();
201             configuredAddress = byName(networkAddressService.getPrimaryIpv4HostAddress());
202             if (configuredAddress != null) {
203                 discoveryIps.add(configuredAddress);
204             }
205             for (CidrAddress a : NetUtil.getAllInterfaceAddresses()) {
206                 if (a.getAddress().equals(configuredAddress)) {
207                     networkPrefixLength = a.getPrefix();
208                 } else {
209                     discoveryIps.add(a.getAddress());
210                 }
211             }
212         }
213
214         if (discoveryIps.isEmpty()) {
215             try {
216                 logger.info("No discovery ip specified. Trying to determine the host address");
217                 configuredAddress = InetAddress.getLocalHost();
218             } catch (Exception e) {
219                 logger.info("Host address cannot be determined. Trying loopback address");
220                 configuredAddress = InetAddress.getLoopbackAddress();
221             }
222         } else {
223             configuredAddress = discoveryIps.iterator().next();
224         }
225
226         logger.info("Using discovery ip {}", configuredAddress.getHostAddress());
227
228         // Get and apply configurations
229         ds.config.createNewUserOnEveryEndpoint = config.createNewUserOnEveryEndpoint;
230         ds.config.networkopenduration = config.pairingTimeout;
231         ds.config.devicename = config.devicename;
232
233         ds.config.uuid = config.uuid;
234         ds.config.bridgeid = config.uuid.replace("-", "").toUpperCase();
235         if (ds.config.bridgeid.length() > 12) {
236             ds.config.bridgeid = ds.config.bridgeid.substring(0, 12);
237         }
238
239         hueIDPrefix = getHueIDPrefixFromUUID(config.uuid);
240
241         if (config.permanentV1bridge) {
242             ds.config.makeV1bridge();
243         }
244
245         setLinkbutton(config.pairingEnabled, config.createNewUserOnEveryEndpoint, config.temporarilyEmulateV1bridge);
246         ds.config.mac = NetworkUtils.getMAC(configuredAddress);
247         ds.config.ipaddress = getConfiguredHostAddress(configuredAddress);
248         ds.config.netmask = networkPrefixLength < 32 ? NetUtil.networkPrefixLengthToNetmask(networkPrefixLength)
249                 : "255.255.255.0";
250
251         if (eventAdmin != null) {
252             eventAdmin.postEvent(new Event(EVENT_ADDRESS_CHANGED, Collections.emptyMap()));
253         }
254     }
255
256     private String getConfiguredHostAddress(InetAddress configuredAddress) {
257         String hostAddress = configuredAddress.getHostAddress();
258         int percentIndex = hostAddress.indexOf("%");
259         if (percentIndex != -1) {
260             return hostAddress.substring(0, percentIndex);
261         } else {
262             return hostAddress;
263         }
264     }
265
266     /**
267      * Get the prefix used to create a unique id
268      *
269      * @param uuid The uuid
270      * @return The prefix in the format of AA:BB:CC:DD:EE:FF:00:11 if uuid is a valid UUID, otherwise uuid is returned.
271      */
272     private String getHueIDPrefixFromUUID(final String uuid) {
273         // Hue API example of a unique id is AA:BB:CC:DD:EE:FF:00:11-XX
274         // 00:11-XX is generated from the item.
275         String prefix = uuid;
276         try {
277             // Generate prefix if uuid is a randomly generated UUID
278             if (UUID.fromString(uuid).version() == 4) {
279                 final StringBuilder sb = new StringBuilder(17);
280                 sb.append(uuid, 0, 2).append(":").append(uuid, 2, 4).append(":").append(uuid, 4, 6).append(":")
281                         .append(uuid, 6, 8).append(":").append(uuid, 9, 11).append(":").append(uuid, 11, 13);
282                 prefix = sb.toString().toUpperCase();
283             }
284         } catch (final IllegalArgumentException e) {
285             // uuid is not a valid UUID
286         }
287
288         return prefix;
289     }
290
291     @Deactivate
292     public void deactive(int reason) {
293         ScheduledFuture<?> future = pairingOffFuture;
294         if (future != null) {
295             future.cancel(false);
296         }
297         future = writeUUIDFuture;
298         if (future != null) {
299             future.cancel(false);
300         }
301     }
302
303     protected void determineHighestAssignedHueID() {
304         for (Metadata metadata : metadataRegistry.getAll()) {
305             if (!metadata.getUID().getNamespace().equals(METAKEY)) {
306                 continue;
307             }
308             try {
309                 int hueId = Integer.parseInt(metadata.getValue());
310                 if (hueId > highestAssignedHueID) {
311                     highestAssignedHueID = hueId;
312                 }
313             } catch (NumberFormatException e) {
314                 logger.warn("A non numeric hue ID '{}' was assigned. Ignoring!", metadata.getValue());
315             }
316         }
317     }
318
319     /**
320      * Although hue IDs are strings, a lot of implementations out there assume them to be numbers. Therefore
321      * we map each item to a number and store that in the meta data provider.
322      *
323      * @param item The item to map
324      * @return A stringified integer number
325      */
326     public String mapItemUIDtoHueID(Item item) {
327         MetadataKey key = new MetadataKey(METAKEY, item.getUID());
328         Metadata metadata = metadataRegistry.get(key);
329         int hueId = 0;
330         if (metadata != null) {
331             try {
332                 hueId = Integer.parseInt(metadata.getValue());
333             } catch (NumberFormatException e) {
334                 logger.warn("A non numeric hue ID '{}' was assigned. Ignore and reassign a different id now!",
335                         metadata.getValue());
336             }
337         }
338         if (hueId == 0) {
339             ++highestAssignedHueID;
340             hueId = highestAssignedHueID;
341             metadataRegistry.add(new Metadata(key, String.valueOf(hueId), null));
342         }
343
344         return String.valueOf(hueId);
345     }
346
347     /**
348      * Get the unique id
349      *
350      * @param hueId The item hueID
351      * @return The unique id
352      */
353     public String getHueUniqueId(final String hueId) {
354         String unique;
355
356         try {
357             final String id = String.format("%06X", Integer.valueOf(hueId));
358             final StringBuilder sb = new StringBuilder(26);
359             sb.append(hueIDPrefix).append(":").append(id, 0, 2).append(":").append(id, 2, 4).append("-").append(id, 4,
360                     6);
361             unique = sb.toString();
362         } catch (final NumberFormatException | IllegalFormatException e) {
363             // Use the hueId as is
364             unique = hueIDPrefix + "-" + hueId;
365         }
366
367         return unique;
368     }
369
370     public boolean isReady() {
371         return !discoveryIps.isEmpty();
372     }
373
374     public HueEmulationConfig getConfig() {
375         return config;
376     }
377
378     public int getHighestAssignedHueID() {
379         return highestAssignedHueID;
380     }
381
382     /**
383      * Sets the link button state.
384      *
385      * Starts a pairing timeout thread if set to true.
386      * Stops any already running timers.
387      *
388      * @param linkbutton New link button state
389      */
390     public void setLinkbutton(boolean linkbutton, boolean createUsersOnEveryEndpoint,
391             boolean temporarilyEmulateV1bridge) {
392         ds.config.linkbutton = linkbutton;
393         config.createNewUserOnEveryEndpoint = createUsersOnEveryEndpoint;
394         if (temporarilyEmulateV1bridge) {
395             ds.config.makeV1bridge();
396         } else if (!config.permanentV1bridge) {
397             ds.config.makeV2bridge();
398         }
399         ScheduledFuture<?> future = pairingOffFuture;
400         if (future != null) {
401             future.cancel(false);
402         }
403         if (!linkbutton) {
404             logger.info("Hue Emulation pairing disabled");
405             return;
406         }
407
408         logger.info("Hue Emulation pairing enabled for {}s", ds.config.networkopenduration);
409         pairingOffFuture = scheduler.schedule(() -> {
410             logger.info("Hue Emulation disable pairing...");
411             if (!config.permanentV1bridge) { // Restore bridge version
412                 ds.config.makeV2bridge();
413             }
414             config.createNewUserOnEveryEndpoint = false;
415             config.temporarilyEmulateV1bridge = false;
416             WriteConfig.unsetPairingMode(configAdmin);
417         }, ds.config.networkopenduration * 1000, TimeUnit.MILLISECONDS);
418     }
419
420     public Set<InetAddress> getDiscoveryIps() {
421         return discoveryIps;
422     }
423 }