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 handler) {
174 synchronized (handlers) {
175 // Start the BPUP update service after the first child device is added
176 startUDPListenerJob();
177 if (!handlers.contains(handler)) {
178 handlers.add(handler);
185 public void childHandlerDisposed(ThingHandler childHandler, Thing childThing) {
186 if (childHandler instanceof BondDeviceHandler handler) {
187 synchronized (handlers) {
188 handlers.remove(handler);
189 if (handlers.isEmpty()) {
190 // Stop the update service when the last child is removed
191 stopUDPListenerJob();
195 super.childHandlerDisposed(childHandler, childThing);
199 * Forwards a push update to a device
201 * @param pushUpdate the {@link BPUPUpdate object}
203 public void forwardUpdateToThing(BPUPUpdate pushUpdate) {
204 updateStatus(ThingStatus.ONLINE);
206 BondDeviceState updateState = pushUpdate.deviceState;
207 String topic = pushUpdate.topic;
208 String deviceId = null;
209 String topicType = null;
211 String[] parts = topic.split("/");
213 topicType = parts[2];
215 // We can't use getThingByUID because we don't know the type of device and thus
216 // don't know the full uid (that is we cannot tell a fan from a fireplace, etc,
217 // from the contents of the update)
218 if (deviceId != null) {
219 if (topicType != null && "state".equals(topicType)) {
220 synchronized (handlers) {
221 for (BondDeviceHandler handler : handlers) {
222 String handlerDeviceId = handler.getDeviceId();
223 if (handlerDeviceId.equalsIgnoreCase(deviceId)) {
224 handler.updateChannelsFromState(updateState);
230 logger.trace("could not read topic type from push update or type was not state.");
233 logger.warn("Can not read device Id from push update.");
238 * Returns the Id of the bridge associated with the handler
240 public String getBridgeId() {
241 return config.serialNumber;
245 * Returns the Ip Address of the bridge associated with the handler as a string
247 public String getBridgeIpAddress() {
248 return config.ipAddress;
252 * Returns the local token of the bridge associated with the handler as a string
254 public String getBridgeToken() {
255 return config.localToken;
259 * Returns the api instance
261 public BondHttpApi getBridgeAPI() {
266 * Set the bridge status offline.
268 * Called by the dependents to set the bridge offline when repeated requests
271 * NOTE: This does NOT stop the UDP listener job, which will keep pinging the
272 * bridge's IP once a minute. The listener job will set the bridge back online
273 * if it receives a proper response from the bridge.
275 public void setBridgeOffline(ThingStatusDetail detail, String description) {
276 updateStatus(ThingStatus.OFFLINE, detail, description);
280 * Set the bridge status back online.
282 * Called by the UDP listener when it gets a proper response.
284 public void setBridgeOnline(String bridgeAddress) {
285 if (!config.isValid()) {
286 logger.warn("Configuration error, cannot set the bridghe online without configuration");
288 } else if (!config.ipAddress.equals(bridgeAddress)) {
289 logger.debug("IP address of Bond {} has changed to {}", config.serialNumber, bridgeAddress);
290 Configuration c = editConfiguration();
291 c.put(CONFIG_IP_ADDRESS, bridgeAddress);
292 updateConfiguration(c);
293 updateBridgeProperties();
296 // don't bother updating on every keepalive packet
297 if (getThing().getStatus() != ThingStatus.ONLINE) {
298 updateBridgeProperties();
302 private void updateProperty(Map<String, String> thingProperties, String key, @Nullable String value) {
306 thingProperties.put(key, value);
309 private void updateBridgeProperties() {
310 BondSysVersion myVersion;
312 myVersion = api.getBridgeVersion();
313 } catch (BondException e) {
314 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
317 // Update all the thing properties based on the result
318 Map<String, String> thingProperties = editProperties();
319 updateProperty(thingProperties, PROPERTY_VENDOR, myVersion.make);
320 updateProperty(thingProperties, PROPERTY_MODEL_ID, myVersion.model);
321 updateProperty(thingProperties, PROPERTY_SERIAL_NUMBER, myVersion.bondid);
322 updateProperty(thingProperties, PROPERTY_FIRMWARE_VERSION, myVersion.firmwareVersion);
323 updateProperties(thingProperties);
324 updateStatus(ThingStatus.ONLINE);
325 BondDiscoveryService localDiscoveryService = discoveryService;
326 if (localDiscoveryService != null) {
327 localDiscoveryService.discoverNow();
332 public Collection<Class<? extends ThingHandlerService>> getServices() {
333 return Set.of(BondDiscoveryService.class);
336 public void setDiscoveryService(BondDiscoveryService discoveryService) {
337 this.discoveryService = discoveryService;