]> git.basschouten.com Git - openhab-addons.git/blob
6e8f63e29203d985cede3f1d6e0bfb28961d4967
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2024 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.binding.insteon.internal.handler;
14
15 import static org.openhab.binding.insteon.internal.InsteonBindingConstants.*;
16
17 import java.util.Optional;
18 import java.util.concurrent.ScheduledFuture;
19 import java.util.concurrent.TimeUnit;
20 import java.util.stream.Stream;
21
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;
47
48 /**
49  * The {@link InsteonBridgeHandler} represents an insteon bridge handler.
50  *
51  * @author Rob Nielsen - Initial contribution
52  * @author Jeremy Setton - Rewrite insteon binding
53  */
54 @NonNullByDefault
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
59
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;
69
70     public InsteonBridgeHandler(Bridge bridge, SerialPortManager serialPortManager, StorageService storageService,
71             ThingRegistry thingRegistry) {
72         super(bridge);
73         this.serialPortManager = serialPortManager;
74         this.storage = storageService.getStorage(bridge.getUID().toString(), DeviceCache.class.getClassLoader());
75         this.thingRegistry = thingRegistry;
76     }
77
78     @Override
79     public Bridge getThing() {
80         return (Bridge) super.getThing();
81     }
82
83     @Override
84     public @Nullable InsteonModem getDevice() {
85         return getModem();
86     }
87
88     @Override
89     public @Nullable InsteonModem getModem() {
90         return modem;
91     }
92
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));
96     }
97
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);
106         } else {
107             throw new UnsupportedOperationException("Unsupported bridge configuration");
108         }
109     }
110
111     public int getDevicePollInterval() {
112         return getBridgeConfig().getDevicePollInterval();
113     }
114
115     public boolean isDeviceDiscoveryEnabled() {
116         return getBridgeConfig().isDeviceDiscoveryEnabled();
117     }
118
119     public boolean isSceneDiscoveryEnabled() {
120         return getBridgeConfig().isSceneDiscoveryEnabled();
121     }
122
123     public boolean isDeviceSyncEnabled() {
124         return getBridgeConfig().isDeviceSyncEnabled();
125     }
126
127     protected @Nullable InsteonDiscoveryService getDiscoveryService() {
128         return discoveryService;
129     }
130
131     public void setDiscoveryService(InsteonDiscoveryService discoveryService) {
132         this.discoveryService = discoveryService;
133     }
134
135     public @Nullable DeviceCache getDeviceCache(DeviceAddress address) {
136         return storage.get(address.toString());
137     }
138
139     public void loadDeviceCache(Device device) {
140         DeviceCache cache = getDeviceCache(device.getAddress());
141         if (cache != null) {
142             cache.load(device);
143         }
144     }
145
146     public void storeDeviceCache(DeviceAddress address, DeviceCache cache) {
147         storage.put(address.toString(), cache);
148     }
149
150     @Override
151     public void initialize() {
152         logger.debug("starting bridge {}", getThing().getUID());
153
154         InsteonBridgeConfiguration config = getBridgeConfig();
155         if (isDuplicateBridge(config)) {
156             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Duplicate bridge.");
157             return;
158         }
159
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();
165         }
166
167         InsteonModem modem = InsteonModem.makeModem(this, config, scheduler, serialPortManager);
168         this.modem = modem;
169
170         if (isInitialized()) {
171             getChildHandlers().forEach(handler -> handler.bridgeThingUpdated(config, modem));
172         }
173
174         updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.NONE, "Connecting to modem.");
175
176         scheduler.execute(() -> {
177             connectJob = scheduler.scheduleWithFixedDelay(() -> {
178                 if (!modem.connect()) {
179                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
180                             "Unable to connect to modem.");
181                     return;
182                 }
183
184                 statisticsJob = scheduler.scheduleWithFixedDelay(() -> modem.logDeviceStatistics(), 0,
185                         DEVICE_STATISTICS_INTERVAL, TimeUnit.SECONDS);
186
187                 cancelJob(connectJob, false);
188             }, START_DELAY, RETRY_INTERVAL, TimeUnit.SECONDS);
189         });
190     }
191
192     @Override
193     public void dispose() {
194         logger.debug("shutting down bridge {}", getThing().getUID());
195
196         cancelJob(connectJob, true);
197         cancelJob(reconnectJob, true);
198         cancelJob(resetJob, true);
199         cancelJob(statisticsJob, true);
200
201         getChildHandlers().forEach(InsteonThingHandler::bridgeThingDisposed);
202
203         InsteonModem modem = getModem();
204         if (modem != null) {
205             if (modem.isInitialized()) {
206                 storeDeviceCache(modem.getAddress(), DeviceCache.builder().withProductData(modem.getProductData())
207                         .withDatabase(modem.getDB()).withFeatures(modem.getFeatures()).build());
208             }
209             modem.stopPolling();
210             modem.disconnect();
211         }
212         this.modem = null;
213
214         super.dispose();
215     }
216
217     @Override
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());
222     }
223
224     @Override
225     public void childHandlerInitialized(ThingHandler childHandler, Thing childThing) {
226         logger.debug("added thing {}", childThing.getUID());
227     }
228
229     @Override
230     public void childHandlerDisposed(ThingHandler childHandler, Thing childThing) {
231         logger.debug("removed thing {}", childThing.getUID());
232     }
233
234     @Override
235     protected String getConfigInfo() {
236         return getBridgeConfig().toString();
237     }
238
239     @Override
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.");
244             return;
245         }
246
247         if (!modem.getDB().isComplete()) {
248             updateStatus(ThingStatus.ONLINE, ThingStatusDetail.CONFIGURATION_PENDING, "Loading modem database.");
249             return;
250         }
251
252         updateStatus(ThingStatus.ONLINE);
253     }
254
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);
258     }
259
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);
264     }
265
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()));
270     }
271
272     private void cleanUpStorage() {
273         storage.getKeys().stream().filter(InsteonAddress::isValid).map(InsteonAddress::new)
274                 .forEach(this::cleanUpStorage);
275     }
276
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());
282         }
283     }
284
285     protected void discoverInsteonDevice(InsteonAddress address, @Nullable ProductData productData) {
286         InsteonDiscoveryService discoveryService = getDiscoveryService();
287         if (discoveryService != null) {
288             scheduler.execute(() -> discoveryService.discoverInsteonDevice(address, productData));
289         }
290     }
291
292     protected void discoverInsteonScene(int group) {
293         InsteonDiscoveryService discoveryService = getDiscoveryService();
294         if (discoveryService != null) {
295             scheduler.execute(() -> discoveryService.discoverInsteonScene(group));
296         }
297     }
298
299     protected void discoverMissingThings() {
300         InsteonDiscoveryService discoveryService = getDiscoveryService();
301         if (discoveryService != null) {
302             scheduler.execute(() -> discoveryService.discoverMissingThings());
303         }
304     }
305
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.");
311                 return;
312             }
313
314             cancelJob(reconnectJob, false);
315             updateStatus();
316         }, 0, RETRY_INTERVAL, TimeUnit.SECONDS);
317     }
318
319     public void reset(long delay) {
320         scheduler.execute(() -> {
321             logger.trace("resetting bridge {}", getThing().getUID());
322
323             dispose();
324
325             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.DUTY_CYCLE, "Resetting bridge.");
326
327             resetJob = scheduler.schedule(() -> {
328                 initialize();
329                 cancelJob(resetJob, false);
330             }, delay, TimeUnit.SECONDS);
331         });
332     }
333
334     /**
335      * Notifies that the modem has been discovered
336      *
337      * @param modem the discovered modem
338      */
339     public void modemDiscovered(InsteonModem modem) {
340         modem.setPollInterval(getDevicePollInterval());
341
342         initializeChannels(modem);
343         updateProperties(modem);
344         loadDeviceCache(modem);
345
346         if (!modem.getDB().isComplete()) {
347             modem.getDB().load();
348         }
349
350         updateStatus();
351     }
352
353     /**
354      * Notifies that the modem database has completed
355      */
356     public void modemDBCompleted() {
357         discoverMissingThings();
358         cleanUpStorage();
359     }
360
361     /**
362      * Notifies that a modem database link has been updated
363      *
364      * @param address the link address
365      * @param group the link group
366      */
367     public void modemDBLinkUpdated(InsteonAddress address, int group) {
368         discoverInsteonDevice(address, getProductData(address));
369         discoverInsteonScene(group);
370         cleanUpStorage(address);
371     }
372
373     /**
374      * Notifies that a modem database product data has been updated
375      *
376      * @param address the device address
377      * @param productData the product data
378      */
379     public void modemDBProductDataUpdated(InsteonAddress address, ProductData productData) {
380         discoverInsteonDevice(address, productData);
381     }
382 }