2 * Copyright (c) 2010-2024 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.insteon.internal.handler;
15 import static org.openhab.binding.insteon.internal.InsteonBindingConstants.*;
17 import java.util.Optional;
18 import java.util.concurrent.ScheduledFuture;
19 import java.util.concurrent.TimeUnit;
20 import java.util.stream.Stream;
22 import org.eclipse.jdt.annotation.NonNullByDefault;
23 import org.eclipse.jdt.annotation.Nullable;
24 import org.openhab.binding.insteon.internal.config.InsteonBridgeConfiguration;
25 import org.openhab.binding.insteon.internal.config.InsteonHub1Configuration;
26 import org.openhab.binding.insteon.internal.config.InsteonHub2Configuration;
27 import org.openhab.binding.insteon.internal.config.InsteonPLMConfiguration;
28 import org.openhab.binding.insteon.internal.device.Device;
29 import org.openhab.binding.insteon.internal.device.DeviceAddress;
30 import org.openhab.binding.insteon.internal.device.DeviceCache;
31 import org.openhab.binding.insteon.internal.device.InsteonAddress;
32 import org.openhab.binding.insteon.internal.device.InsteonModem;
33 import org.openhab.binding.insteon.internal.device.ProductData;
34 import org.openhab.binding.insteon.internal.discovery.InsteonDiscoveryService;
35 import org.openhab.core.io.transport.serial.SerialPortManager;
36 import org.openhab.core.storage.Storage;
37 import org.openhab.core.storage.StorageService;
38 import org.openhab.core.thing.Bridge;
39 import org.openhab.core.thing.Thing;
40 import org.openhab.core.thing.ThingRegistry;
41 import org.openhab.core.thing.ThingStatus;
42 import org.openhab.core.thing.ThingStatusDetail;
43 import org.openhab.core.thing.ThingTypeUID;
44 import org.openhab.core.thing.binding.BridgeHandler;
45 import org.openhab.core.thing.binding.ThingHandler;
46 import org.openhab.core.thing.binding.builder.BridgeBuilder;
49 * The {@link InsteonBridgeHandler} represents an insteon bridge handler.
51 * @author Rob Nielsen - Initial contribution
52 * @author Jeremy Setton - Rewrite insteon binding
55 public class InsteonBridgeHandler extends InsteonBaseThingHandler implements BridgeHandler {
56 private static final int DEVICE_STATISTICS_INTERVAL = 600; // seconds
57 private static final int RETRY_INTERVAL = 30; // seconds
58 private static final int START_DELAY = 5; // seconds
60 private @Nullable InsteonModem modem;
61 private @Nullable InsteonDiscoveryService discoveryService;
62 private @Nullable ScheduledFuture<?> connectJob;
63 private @Nullable ScheduledFuture<?> reconnectJob;
64 private @Nullable ScheduledFuture<?> resetJob;
65 private @Nullable ScheduledFuture<?> statisticsJob;
66 private SerialPortManager serialPortManager;
67 private Storage<DeviceCache> storage;
68 private ThingRegistry thingRegistry;
70 public InsteonBridgeHandler(Bridge bridge, SerialPortManager serialPortManager, StorageService storageService,
71 ThingRegistry thingRegistry) {
73 this.serialPortManager = serialPortManager;
74 this.storage = storageService.getStorage(bridge.getUID().toString(), DeviceCache.class.getClassLoader());
75 this.thingRegistry = thingRegistry;
79 public Bridge getThing() {
80 return (Bridge) super.getThing();
84 public @Nullable InsteonModem getDevice() {
89 public @Nullable InsteonModem getModem() {
93 public @Nullable ProductData getProductData(DeviceAddress address) {
94 return Optional.ofNullable(getDeviceCache(address)).map(DeviceCache::getProductData)
95 .orElse(Optional.ofNullable(modem).map(modem -> modem.getProductData(address)).orElse(null));
98 protected InsteonBridgeConfiguration getBridgeConfig() {
99 ThingTypeUID thingTypeUID = getThing().getThingTypeUID();
100 if (THING_TYPE_HUB1.equals(thingTypeUID)) {
101 return getConfigAs(InsteonHub1Configuration.class);
102 } else if (THING_TYPE_HUB2.equals(thingTypeUID)) {
103 return getConfigAs(InsteonHub2Configuration.class);
104 } else if (THING_TYPE_PLM.equals(thingTypeUID)) {
105 return getConfigAs(InsteonPLMConfiguration.class);
107 throw new UnsupportedOperationException("Unsupported bridge configuration");
111 public int getDevicePollInterval() {
112 return getBridgeConfig().getDevicePollInterval();
115 public boolean isDeviceDiscoveryEnabled() {
116 return getBridgeConfig().isDeviceDiscoveryEnabled();
119 public boolean isSceneDiscoveryEnabled() {
120 return getBridgeConfig().isSceneDiscoveryEnabled();
123 public boolean isDeviceSyncEnabled() {
124 return getBridgeConfig().isDeviceSyncEnabled();
127 protected @Nullable InsteonDiscoveryService getDiscoveryService() {
128 return discoveryService;
131 public void setDiscoveryService(InsteonDiscoveryService discoveryService) {
132 this.discoveryService = discoveryService;
135 public @Nullable DeviceCache getDeviceCache(DeviceAddress address) {
136 return storage.get(address.toString());
139 public void loadDeviceCache(Device device) {
140 DeviceCache cache = getDeviceCache(device.getAddress());
146 public void storeDeviceCache(DeviceAddress address, DeviceCache cache) {
147 storage.put(address.toString(), cache);
151 public void initialize() {
152 logger.debug("starting bridge {}", getThing().getUID());
154 InsteonBridgeConfiguration config = getBridgeConfig();
155 if (isDuplicateBridge(config)) {
156 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Duplicate bridge.");
160 InsteonLegacyNetworkHandler legacyHandler = getLegacyNetworkHandler(config);
161 if (legacyHandler != null) {
162 logger.info("Disabling Insteon legacy network bridge {} in favor of bridge {}",
163 legacyHandler.getThing().getUID(), getThing().getUID());
164 legacyHandler.disable();
167 InsteonModem modem = InsteonModem.makeModem(this, config, scheduler, serialPortManager);
170 if (isInitialized()) {
171 getChildHandlers().forEach(handler -> handler.bridgeThingUpdated(config, modem));
174 updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.NONE, "Connecting to modem.");
176 scheduler.execute(() -> {
177 connectJob = scheduler.scheduleWithFixedDelay(() -> {
178 if (!modem.connect()) {
179 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
180 "Unable to connect to modem.");
184 statisticsJob = scheduler.scheduleWithFixedDelay(() -> modem.logDeviceStatistics(), 0,
185 DEVICE_STATISTICS_INTERVAL, TimeUnit.SECONDS);
187 cancelJob(connectJob, false);
188 }, START_DELAY, RETRY_INTERVAL, TimeUnit.SECONDS);
193 public void dispose() {
194 logger.debug("shutting down bridge {}", getThing().getUID());
196 cancelJob(connectJob, true);
197 cancelJob(reconnectJob, true);
198 cancelJob(resetJob, true);
199 cancelJob(statisticsJob, true);
201 getChildHandlers().forEach(InsteonThingHandler::bridgeThingDisposed);
203 InsteonModem modem = getModem();
205 if (modem.isInitialized()) {
206 storeDeviceCache(modem.getAddress(), DeviceCache.builder().withProductData(modem.getProductData())
207 .withDatabase(modem.getDB()).withFeatures(modem.getFeatures()).build());
218 protected BridgeBuilder editThing() {
219 return BridgeBuilder.create(thing.getThingTypeUID(), thing.getUID()).withBridge(thing.getBridgeUID())
220 .withChannels(thing.getChannels()).withConfiguration(thing.getConfiguration())
221 .withLabel(thing.getLabel()).withLocation(thing.getLocation()).withProperties(thing.getProperties());
225 public void childHandlerInitialized(ThingHandler childHandler, Thing childThing) {
226 logger.debug("added thing {}", childThing.getUID());
230 public void childHandlerDisposed(ThingHandler childHandler, Thing childThing) {
231 logger.debug("removed thing {}", childThing.getUID());
235 protected String getConfigInfo() {
236 return getBridgeConfig().toString();
240 public void updateStatus() {
241 InsteonModem modem = getModem();
242 if (modem == null || !modem.isInitialized()) {
243 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Unable to determine modem.");
247 if (!modem.getDB().isComplete()) {
248 updateStatus(ThingStatus.ONLINE, ThingStatusDetail.CONFIGURATION_PENDING, "Loading modem database.");
252 updateStatus(ThingStatus.ONLINE);
255 public Stream<InsteonThingHandler> getChildHandlers() {
256 return getThing().getThings().stream().filter(Thing::isEnabled).map(Thing::getHandler)
257 .filter(InsteonThingHandler.class::isInstance).map(InsteonThingHandler.class::cast);
260 private @Nullable InsteonLegacyNetworkHandler getLegacyNetworkHandler(InsteonBridgeConfiguration config) {
261 return thingRegistry.getAll().stream().filter(Thing::isEnabled).map(Thing::getHandler)
262 .filter(InsteonLegacyNetworkHandler.class::isInstance).map(InsteonLegacyNetworkHandler.class::cast)
263 .filter(handler -> config.equals(handler.getBridgeConfig())).findFirst().orElse(null);
266 private boolean isDuplicateBridge(InsteonBridgeConfiguration config) {
267 return thingRegistry.getAll().stream().filter(Thing::isEnabled).map(Thing::getHandler)
268 .filter(InsteonBridgeHandler.class::isInstance).map(InsteonBridgeHandler.class::cast)
269 .anyMatch(handler -> !this.equals(handler) && config.equals(handler.getBridgeConfig()));
272 private void cleanUpStorage() {
273 storage.getKeys().stream().filter(InsteonAddress::isValid).map(InsteonAddress::new)
274 .forEach(this::cleanUpStorage);
277 private void cleanUpStorage(InsteonAddress address) {
278 InsteonModem modem = getModem();
279 if (modem != null && modem.getDB().isComplete() && !modem.getDB().hasEntry(address)
280 && !modem.getAddress().equals(address)) {
281 storage.remove(address.toString());
285 protected void discoverInsteonDevice(InsteonAddress address, @Nullable ProductData productData) {
286 InsteonDiscoveryService discoveryService = getDiscoveryService();
287 if (discoveryService != null) {
288 scheduler.execute(() -> discoveryService.discoverInsteonDevice(address, productData));
292 protected void discoverInsteonScene(int group) {
293 InsteonDiscoveryService discoveryService = getDiscoveryService();
294 if (discoveryService != null) {
295 scheduler.execute(() -> discoveryService.discoverInsteonScene(group));
299 protected void discoverMissingThings() {
300 InsteonDiscoveryService discoveryService = getDiscoveryService();
301 if (discoveryService != null) {
302 scheduler.execute(() -> discoveryService.discoverMissingThings());
306 public void reconnect(InsteonModem modem) {
307 reconnectJob = scheduler.scheduleWithFixedDelay(() -> {
308 if (!modem.reconnect()) {
309 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
310 "Unable to reconnect to modem.");
314 cancelJob(reconnectJob, false);
316 }, 0, RETRY_INTERVAL, TimeUnit.SECONDS);
319 public void reset(long delay) {
320 scheduler.execute(() -> {
321 logger.trace("resetting bridge {}", getThing().getUID());
325 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.DUTY_CYCLE, "Resetting bridge.");
327 resetJob = scheduler.schedule(() -> {
329 cancelJob(resetJob, false);
330 }, delay, TimeUnit.SECONDS);
335 * Notifies that the modem has been discovered
337 * @param modem the discovered modem
339 public void modemDiscovered(InsteonModem modem) {
340 modem.setPollInterval(getDevicePollInterval());
342 initializeChannels(modem);
343 updateProperties(modem);
344 loadDeviceCache(modem);
346 if (!modem.getDB().isComplete()) {
347 modem.getDB().load();
354 * Notifies that the modem database has completed
356 public void modemDBCompleted() {
357 discoverMissingThings();
362 * Notifies that a modem database link has been updated
364 * @param address the link address
365 * @param group the link group
367 public void modemDBLinkUpdated(InsteonAddress address, int group) {
368 discoverInsteonDevice(address, getProductData(address));
369 discoverInsteonScene(group);
370 cleanUpStorage(address);
374 * Notifies that a modem database product data has been updated
376 * @param address the device address
377 * @param productData the product data
379 public void modemDBProductDataUpdated(InsteonAddress address, ProductData productData) {
380 discoverInsteonDevice(address, productData);