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