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.binding.bondhome.internal.handler;
15 import static org.openhab.binding.bondhome.internal.BondHomeBindingConstants.*;
16 import static org.openhab.core.thing.Thing.*;
18 import java.net.InetAddress;
19 import java.net.UnknownHostException;
20 import java.util.Collection;
21 import java.util.Collections;
22 import java.util.HashSet;
25 import java.util.concurrent.ScheduledExecutorService;
26 import java.util.concurrent.ScheduledFuture;
27 import java.util.concurrent.TimeUnit;
29 import org.eclipse.jdt.annotation.NonNullByDefault;
30 import org.eclipse.jdt.annotation.Nullable;
31 import org.openhab.binding.bondhome.internal.BondException;
32 import org.openhab.binding.bondhome.internal.api.BPUPListener;
33 import org.openhab.binding.bondhome.internal.api.BPUPUpdate;
34 import org.openhab.binding.bondhome.internal.api.BondDeviceState;
35 import org.openhab.binding.bondhome.internal.api.BondHttpApi;
36 import org.openhab.binding.bondhome.internal.api.BondSysVersion;
37 import org.openhab.binding.bondhome.internal.config.BondBridgeConfiguration;
38 import org.openhab.binding.bondhome.internal.discovery.BondDiscoveryService;
39 import org.openhab.core.common.ThreadPoolManager;
40 import org.openhab.core.config.core.Configuration;
41 import org.openhab.core.io.net.http.HttpClientFactory;
42 import org.openhab.core.thing.Bridge;
43 import org.openhab.core.thing.ChannelUID;
44 import org.openhab.core.thing.Thing;
45 import org.openhab.core.thing.ThingStatus;
46 import org.openhab.core.thing.ThingStatusDetail;
47 import org.openhab.core.thing.binding.BaseBridgeHandler;
48 import org.openhab.core.thing.binding.ThingHandler;
49 import org.openhab.core.thing.binding.ThingHandlerService;
50 import org.openhab.core.types.Command;
51 import org.slf4j.Logger;
52 import org.slf4j.LoggerFactory;
55 * The {@link BondBridgeHandler} is responsible for handling commands, which are
56 * sent to one of the channels.
58 * @author Sara Geleskie Damiano - Initial contribution
61 public class BondBridgeHandler extends BaseBridgeHandler {
63 private final Logger logger = LoggerFactory.getLogger(BondBridgeHandler.class);
65 // Get a dedicated threadpool for the long-running listener thread.
66 // Intent is to not permanently tie up the common scheduler pool.
67 private final ScheduledExecutorService bondScheduler = ThreadPoolManager.getScheduledPool("bondBridgeHandler");
68 private final BPUPListener udpListener;
69 private final BondHttpApi api;
71 private BondBridgeConfiguration config = new BondBridgeConfiguration();
73 private @Nullable BondDiscoveryService discoveryService;
75 private final Set<BondDeviceHandler> handlers = Collections.synchronizedSet(new HashSet<>());
77 private @Nullable ScheduledFuture<?> initializer;
79 public BondBridgeHandler(Bridge bridge, final HttpClientFactory httpClientFactory) {
81 udpListener = new BPUPListener(this);
82 api = new BondHttpApi(this, httpClientFactory);
83 logger.debug("Created a BondBridgeHandler for thing '{}'", getThing().getUID());
87 public void handleCommand(ChannelUID channelUID, Command command) {
88 // Not needed, all commands are handled in the {@link BondDeviceHandler}
92 public void initialize() {
93 config = getConfigAs(BondBridgeConfiguration.class);
95 // set the thing status to UNKNOWN temporarily
96 updateStatus(ThingStatus.UNKNOWN);
98 this.initializer = scheduler.schedule(this::initializeThing, 0L, TimeUnit.MILLISECONDS);
101 private void initializeThing() {
102 if (config.localToken.isEmpty()) {
103 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
104 "@text/offline.conf-error.incorrect-local-token");
105 this.initializer = null;
108 if (config.ipAddress.isEmpty()) {
110 String lookupAddress = config.serialNumber + ".local";
111 logger.debug("Attempting to get IP address for Bond Bridge {}", lookupAddress);
112 InetAddress ia = InetAddress.getByName(lookupAddress);
113 String ip = ia.getHostAddress();
114 Configuration c = editConfiguration();
115 c.put(CONFIG_IP_ADDRESS, ip);
116 updateConfiguration(c);
117 config = getConfigAs(BondBridgeConfiguration.class);
118 } catch (UnknownHostException ignored) {
119 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
120 "@text/offline.conf-error.unknown-host");
121 this.initializer = null;
126 InetAddress.getByName(config.ipAddress);
127 } catch (UnknownHostException ignored) {
128 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
129 "@text/offline.conf-error.invalid-host");
130 this.initializer = null;
135 // Ask the bridge its current status and update the properties with the info
136 // This will also set the thing status to online/offline based on whether it
137 // succeeds in getting the properties from the bridge.
138 updateBridgeProperties();
139 this.initializer = null;
143 public void dispose() {
144 // The listener should already have been stopped when the last child was
146 // but we'll call the stop here for good measure.
147 stopUDPListenerJob();
148 ScheduledFuture<?> localInitializer = initializer;
149 if (localInitializer != null) {
150 localInitializer.cancel(true);
154 private synchronized void startUDPListenerJob() {
155 if (udpListener.isRunning()) {
158 logger.debug("Started listener job");
159 udpListener.start(bondScheduler);
162 private synchronized void stopUDPListenerJob() {
163 logger.trace("Stopping UDP listener job");
164 if (udpListener.isRunning()) {
165 udpListener.shutdown();
166 logger.debug("UDP listener job stopped");
171 public void childHandlerInitialized(ThingHandler childHandler, Thing childThing) {
172 super.childHandlerInitialized(childHandler, childThing);
173 if (childHandler instanceof BondDeviceHandler) {
174 BondDeviceHandler handler = (BondDeviceHandler) childHandler;
175 synchronized (handlers) {
176 // Start the BPUP update service after the first child device is added
177 startUDPListenerJob();
178 if (!handlers.contains(handler)) {
179 handlers.add(handler);
186 public void childHandlerDisposed(ThingHandler childHandler, Thing childThing) {
187 if (childHandler instanceof BondDeviceHandler) {
188 BondDeviceHandler handler = (BondDeviceHandler) childHandler;
189 synchronized (handlers) {
190 handlers.remove(handler);
191 if (handlers.isEmpty()) {
192 // Stop the update service when the last child is removed
193 stopUDPListenerJob();
197 super.childHandlerDisposed(childHandler, childThing);
201 * Forwards a push update to a device
203 * @param the {@link BPUPUpdate object}
205 public void forwardUpdateToThing(BPUPUpdate pushUpdate) {
206 updateStatus(ThingStatus.ONLINE);
208 BondDeviceState updateState = pushUpdate.deviceState;
209 String topic = pushUpdate.topic;
210 String deviceId = null;
211 String topicType = null;
213 String parts[] = topic.split("/");
215 topicType = parts[2];
217 // We can't use getThingByUID because we don't know the type of device and thus
218 // don't know the full uid (that is we cannot tell a fan from a fireplace, etc,
219 // from the contents of the update)
220 if (deviceId != null) {
221 if (topicType != null && "state".equals(topicType)) {
222 synchronized (handlers) {
223 for (BondDeviceHandler handler : handlers) {
224 String handlerDeviceId = handler.getDeviceId();
225 if (handlerDeviceId.equalsIgnoreCase(deviceId)) {
226 handler.updateChannelsFromState(updateState);
232 logger.trace("could not read topic type from push update or type was not state.");
235 logger.warn("Can not read device Id from push update.");
240 * Returns the Id of the bridge associated with the handler
242 public String getBridgeId() {
243 return config.serialNumber;
247 * Returns the Ip Address of the bridge associated with the handler as a string
249 public String getBridgeIpAddress() {
250 return config.ipAddress;
254 * Returns the local token of the bridge associated with the handler as a string
256 public String getBridgeToken() {
257 return config.localToken;
261 * Returns the api instance
263 public BondHttpApi getBridgeAPI() {
268 * Set the bridge status offline.
270 * Called by the dependents to set the bridge offline when repeated requests
273 * NOTE: This does NOT stop the UDP listener job, which will keep pinging the
274 * bridge's IP once a minute. The listener job will set the bridge back online
275 * if it receives a proper response from the bridge.
277 public void setBridgeOffline(ThingStatusDetail detail, String description) {
278 updateStatus(ThingStatus.OFFLINE, detail, description);
282 * Set the bridge status back online.
284 * Called by the UDP listener when it gets a proper response.
286 public void setBridgeOnline(String bridgeAddress) {
287 if (!config.isValid()) {
288 logger.warn("Configuration error, cannot set the bridghe online without configuration");
290 } else if (!config.ipAddress.equals(bridgeAddress)) {
291 logger.debug("IP address of Bond {} has changed to {}", config.serialNumber, bridgeAddress);
292 Configuration c = editConfiguration();
293 c.put(CONFIG_IP_ADDRESS, bridgeAddress);
294 updateConfiguration(c);
295 updateBridgeProperties();
298 // don't bother updating on every keepalive packet
299 if (getThing().getStatus() != ThingStatus.ONLINE) {
300 updateBridgeProperties();
304 private void updateProperty(Map<String, String> thingProperties, String key, @Nullable String value) {
308 thingProperties.put(key, value);
311 private void updateBridgeProperties() {
312 BondSysVersion myVersion;
314 myVersion = api.getBridgeVersion();
315 } catch (BondException e) {
316 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
319 // Update all the thing properties based on the result
320 Map<String, String> thingProperties = editProperties();
321 updateProperty(thingProperties, PROPERTY_VENDOR, myVersion.make);
322 updateProperty(thingProperties, PROPERTY_MODEL_ID, myVersion.model);
323 updateProperty(thingProperties, PROPERTY_SERIAL_NUMBER, myVersion.bondid);
324 updateProperty(thingProperties, PROPERTY_FIRMWARE_VERSION, myVersion.firmwareVersion);
325 updateProperties(thingProperties);
326 updateStatus(ThingStatus.ONLINE);
327 BondDiscoveryService localDiscoveryService = discoveryService;
328 if (localDiscoveryService != null) {
329 localDiscoveryService.discoverNow();
334 public Collection<Class<? extends ThingHandlerService>> getServices() {
335 return Collections.singleton(BondDiscoveryService.class);
338 public void setDiscoveryService(BondDiscoveryService discoveryService) {
339 this.discoveryService = discoveryService;