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.hueemulation.internal;
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;
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;
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;
60 import com.google.gson.Gson;
61 import com.google.gson.GsonBuilder;
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.
67 * Also manages the pairing timeout. The service is restarted after a pairing timeout, due to the ConfigAdmin
68 * configuration change.
70 * This is a central component and required by all other components and may not
71 * depend on anything in this bundle.
73 * @author David Graeff - Initial contribution
75 @Component(immediate = false, service = ConfigStore.class, configurationPid = HueEmulationService.CONFIG_PID)
76 @ConfigurableService(category = "io", label = "Hue Emulation", description_uri = "io:hueemulation")
78 public class ConfigStore {
80 public static final String METAKEY = "HUEEMU";
81 public static final String EVENT_ADDRESS_CHANGED = "HUE_EMU_CONFIG_ADDR_CHANGED";
83 private final Logger logger = LoggerFactory.getLogger(ConfigStore.class);
85 public HueDataStore ds = new HueDataStore();
87 protected @NonNullByDefault({}) ScheduledExecutorService scheduler;
88 private @Nullable ScheduledFuture<?> pairingOffFuture;
89 private @Nullable ScheduledFuture<?> writeUUIDFuture;
92 * This is the main gson instance, to be obtained by all components that operate on the dto data fields
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();
103 protected @NonNullByDefault({}) ConfigurationAdmin configAdmin;
106 protected @NonNullByDefault({}) NetworkAddressService networkAddressService;
109 protected @NonNullByDefault({}) MetadataRegistry metadataRegistry;
112 protected @NonNullByDefault({}) EventAdmin eventAdmin;
114 //// objects, set within activate()
115 private Set<InetAddress> discoveryIps = Collections.emptySet();
116 protected volatile @NonNullByDefault({}) HueEmulationConfig config;
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();
123 private int highestAssignedHueID = 1;
125 private String hueIDPrefix = "";
127 public ConfigStore() {
128 scheduler = ThreadPoolManager.getScheduledPool(ThreadPoolManager.THREAD_POOL_NAME_COMMON);
132 * For test dependency injection
134 * @param networkAddressService The network address service
135 * @param configAdmin The configuration admin service
136 * @param metadataRegistry The metadataRegistry service
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;
147 public void activate(Map<String, Object> properties) {
148 this.config = new Configuration(properties).as(HueEmulationConfig.class);
150 determineHighestAssignedHueID();
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);
160 modified(properties);
164 private @Nullable InetAddress byName(@Nullable String address) {
165 if (address == null) {
169 return InetAddress.getByName(address);
170 } catch (UnknownHostException e) {
171 logger.warn("Given IP address could not be resolved: {}", address, e);
177 public void modified(Map<String, Object> properties) {
178 this.config = new Configuration(properties).as(HueEmulationConfig.class);
180 switchFilter = Collections.unmodifiableSet(
181 Stream.of(config.restrictToTagsSwitches.split(",")).map(String::trim).collect(Collectors.toSet()));
183 colorFilter = Collections.unmodifiableSet(
184 Stream.of(config.restrictToTagsColorLights.split(",")).map(String::trim).collect(Collectors.toSet()));
186 whiteFilter = Collections.unmodifiableSet(
187 Stream.of(config.restrictToTagsWhiteLights.split(",")).map(String::trim).collect(Collectors.toSet()));
189 ignoreItemsFilter = Collections.unmodifiableSet(
190 Stream.of(config.ignoreItemsWithTags.split(",")).map(String::trim).collect(Collectors.toSet()));
192 // Use either the user configured
193 InetAddress configuredAddress = null;
194 int networkPrefixLength = 24; // Default for most networks: 255.255.255.0
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()));
200 discoveryIps = new LinkedHashSet<>();
201 configuredAddress = byName(networkAddressService.getPrimaryIpv4HostAddress());
202 if (configuredAddress != null) {
203 discoveryIps.add(configuredAddress);
205 for (CidrAddress a : NetUtil.getAllInterfaceAddresses()) {
206 if (a.getAddress().equals(configuredAddress)) {
207 networkPrefixLength = a.getPrefix();
209 discoveryIps.add(a.getAddress());
214 if (discoveryIps.isEmpty()) {
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();
223 configuredAddress = discoveryIps.iterator().next();
226 logger.info("Using discovery ip {}", configuredAddress.getHostAddress());
228 // Get and apply configurations
229 ds.config.createNewUserOnEveryEndpoint = config.createNewUserOnEveryEndpoint;
230 ds.config.networkopenduration = config.pairingTimeout;
231 ds.config.devicename = config.devicename;
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);
239 hueIDPrefix = getHueIDPrefixFromUUID(config.uuid);
241 if (config.permanentV1bridge) {
242 ds.config.makeV1bridge();
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)
251 if (eventAdmin != null) {
252 eventAdmin.postEvent(new Event(EVENT_ADDRESS_CHANGED, Collections.emptyMap()));
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);
267 * Get the prefix used to create a unique id
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.
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;
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();
284 } catch (final IllegalArgumentException e) {
285 // uuid is not a valid UUID
292 public void deactive(int reason) {
293 ScheduledFuture<?> future = pairingOffFuture;
294 if (future != null) {
295 future.cancel(false);
297 future = writeUUIDFuture;
298 if (future != null) {
299 future.cancel(false);
303 protected void determineHighestAssignedHueID() {
304 for (Metadata metadata : metadataRegistry.getAll()) {
305 if (!metadata.getUID().getNamespace().equals(METAKEY)) {
309 int hueId = Integer.parseInt(metadata.getValue());
310 if (hueId > highestAssignedHueID) {
311 highestAssignedHueID = hueId;
313 } catch (NumberFormatException e) {
314 logger.warn("A non numeric hue ID '{}' was assigned. Ignoring!", metadata.getValue());
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.
323 * @param item The item to map
324 * @return A stringified integer number
326 public String mapItemUIDtoHueID(Item item) {
327 MetadataKey key = new MetadataKey(METAKEY, item.getUID());
328 Metadata metadata = metadataRegistry.get(key);
330 if (metadata != null) {
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());
339 ++highestAssignedHueID;
340 hueId = highestAssignedHueID;
341 metadataRegistry.add(new Metadata(key, String.valueOf(hueId), null));
344 return String.valueOf(hueId);
350 * @param hueId The item hueID
351 * @return The unique id
353 public String getHueUniqueId(final String hueId) {
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,
361 unique = sb.toString();
362 } catch (final NumberFormatException | IllegalFormatException e) {
363 // Use the hueId as is
364 unique = hueIDPrefix + "-" + hueId;
370 public boolean isReady() {
371 return !discoveryIps.isEmpty();
374 public HueEmulationConfig getConfig() {
378 public int getHighestAssignedHueID() {
379 return highestAssignedHueID;
383 * Sets the link button state.
385 * Starts a pairing timeout thread if set to true.
386 * Stops any already running timers.
388 * @param linkbutton New link button state
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();
399 ScheduledFuture<?> future = pairingOffFuture;
400 if (future != null) {
401 future.cancel(false);
404 logger.info("Hue Emulation pairing disabled");
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();
414 config.createNewUserOnEveryEndpoint = false;
415 config.temporarilyEmulateV1bridge = false;
416 WriteConfig.unsetPairingMode(configAdmin);
417 }, ds.config.networkopenduration * 1000, TimeUnit.MILLISECONDS);
420 public Set<InetAddress> getDiscoveryIps() {