]> git.basschouten.com Git - openhab-addons.git/blob
bd415cf4c5d1593b4158ed826f5528f0c15409bc
[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.List;
18 import java.util.Map;
19 import java.util.concurrent.ScheduledFuture;
20 import java.util.concurrent.TimeUnit;
21 import java.util.stream.Collectors;
22 import java.util.stream.Stream;
23
24 import org.eclipse.jdt.annotation.NonNullByDefault;
25 import org.eclipse.jdt.annotation.Nullable;
26 import org.openhab.binding.insteon.internal.InsteonStateDescriptionProvider;
27 import org.openhab.binding.insteon.internal.config.InsteonBridgeConfiguration;
28 import org.openhab.binding.insteon.internal.config.InsteonDeviceConfiguration;
29 import org.openhab.binding.insteon.internal.device.Device;
30 import org.openhab.binding.insteon.internal.device.DeviceCache;
31 import org.openhab.binding.insteon.internal.device.DeviceType;
32 import org.openhab.binding.insteon.internal.device.InsteonAddress;
33 import org.openhab.binding.insteon.internal.device.InsteonDevice;
34 import org.openhab.binding.insteon.internal.device.InsteonEngine;
35 import org.openhab.binding.insteon.internal.device.InsteonModem;
36 import org.openhab.core.config.core.Configuration;
37 import org.openhab.core.thing.Bridge;
38 import org.openhab.core.thing.Channel;
39 import org.openhab.core.thing.ChannelUID;
40 import org.openhab.core.thing.Thing;
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.type.ChannelTypeUID;
46 import org.openhab.core.types.StateOption;
47 import org.openhab.core.util.StringUtils;
48
49 /**
50  * The {@link InsteonDeviceHandler} represents an Insteon device handler.
51  *
52  * @author Jeremy Setton - Initial contribution
53  */
54 @NonNullByDefault
55 public class InsteonDeviceHandler extends InsteonBaseThingHandler {
56     private static final int HEARTBEAT_TIMEOUT_BUFFER = 5; // in minutes
57     private static final int INIT_DELAY = 100; // in milliseconds
58     private static final int RESET_DELAY = 1000; // in milliseconds
59
60     private @Nullable InsteonDevice device;
61     private @Nullable ScheduledFuture<?> heartbeatJob;
62     private InsteonStateDescriptionProvider stateDescriptionProvider;
63
64     public InsteonDeviceHandler(Thing thing, InsteonStateDescriptionProvider stateDescriptionProvider) {
65         super(thing);
66         this.stateDescriptionProvider = stateDescriptionProvider;
67     }
68
69     @Override
70     public @Nullable InsteonDevice getDevice() {
71         return device;
72     }
73
74     @Override
75     public void initialize() {
76         InsteonDeviceConfiguration config = getConfigAs(InsteonDeviceConfiguration.class);
77
78         scheduler.execute(() -> {
79             Bridge bridge = getBridge();
80             if (bridge == null) {
81                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "No bridge selected.");
82                 return;
83             }
84
85             if (bridge.getThingTypeUID().equals(THING_TYPE_LEGACY_NETWORK)) {
86                 changeThingType(THING_TYPE_LEGACY_DEVICE, bridge.getHandler());
87                 return;
88             }
89
90             if (!InsteonAddress.isValid(config.getAddress())) {
91                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
92                         "Invalid device address, it must be formatted as 'AB.CD.EF'.");
93                 return;
94             }
95
96             InsteonModem modem = getModem();
97             InsteonAddress address = new InsteonAddress(config.getAddress());
98             if (modem != null && modem.hasDevice(address)) {
99                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Duplicate device.");
100                 return;
101             }
102
103             InsteonDevice device = createDevice(address, modem);
104             this.device = device;
105
106             if (modem != null) {
107                 modem.addDevice(device);
108             }
109
110             initializeChannels(device);
111             updateProperties(device);
112             refresh();
113         });
114     }
115
116     private void changeThingType(ThingTypeUID thingTypeUID, @Nullable BridgeHandler bridgeHandler) {
117         if (bridgeHandler instanceof InsteonLegacyNetworkHandler legacyNetworkHandler) {
118             Map<ChannelUID, Configuration> channelConfigs = getThing().getChannels().stream()
119                     .collect(Collectors.toMap(Channel::getUID, Channel::getConfiguration));
120
121             legacyNetworkHandler.addChannelConfigs(channelConfigs);
122         }
123
124         changeThingType(thingTypeUID, getConfig());
125     }
126
127     private InsteonDevice createDevice(InsteonAddress address, @Nullable InsteonModem modem) {
128         InsteonDevice device;
129         InsteonBridgeHandler handler = getInsteonBridgeHandler();
130         if (handler != null) {
131             device = InsteonDevice.makeDevice(address, modem, handler.getProductData(address));
132             device.setPollInterval(handler.getDevicePollInterval());
133             device.setIsDeviceSyncEnabled(handler.isDeviceSyncEnabled());
134             handler.loadDeviceCache(device);
135         } else {
136             device = InsteonDevice.makeDevice(address, modem, null);
137         }
138         device.setHandler(this);
139         device.initialize();
140         return device;
141     }
142
143     @Override
144     protected void initializeChannels(Device device) {
145         DeviceType deviceType = device.getType();
146         if (deviceType == null) {
147             return;
148         }
149
150         super.initializeChannels(device);
151
152         getThing().getChannels().forEach(channel -> setChannelCustomSettings(channel, deviceType.getName()));
153     }
154
155     private void setChannelCustomSettings(Channel channel, String deviceTypeName) {
156         ChannelUID channelUID = channel.getUID();
157         ChannelTypeUID channelTypeUID = channel.getChannelTypeUID();
158         if (channelTypeUID == null) {
159             return;
160         }
161
162         String key = deviceTypeName + ":" + channelIdToFeatureName(channelTypeUID.getId());
163         String[] stateDescriptionOptions = CUSTOM_STATE_DESCRIPTION_OPTIONS.get(key);
164         if (stateDescriptionOptions == null) {
165             return;
166         }
167
168         List<StateOption> options = Stream.of(stateDescriptionOptions).map(value -> new StateOption(value,
169                 StringUtils.capitalizeByWhitespace(value.replace("_", " ").toLowerCase()))).toList();
170
171         logger.trace("setting state options for {} to {}", channelUID, options);
172
173         stateDescriptionProvider.setStateOptions(channelUID, options);
174     }
175
176     @Override
177     public void dispose() {
178         InsteonDevice device = getDevice();
179         if (device != null) {
180             device.stopPolling();
181
182             InsteonModem modem = getModem();
183             if (modem != null) {
184                 modem.deleteSceneEntries(device);
185                 modem.removeDevice(device);
186             }
187
188             InsteonBridgeHandler handler = getInsteonBridgeHandler();
189             if (handler != null && device.hasModemDBEntry()) {
190                 handler.storeDeviceCache(device.getAddress(),
191                         DeviceCache.builder().withProductData(device.getProductData())
192                                 .withInsteonEngine(device.getInsteonEngine()).withDatabase(device.getLinkDB())
193                                 .withFeatures(device.getFeatures()).build());
194             }
195         }
196         this.device = null;
197
198         stopHeartbeatMonitor();
199
200         super.dispose();
201     }
202
203     @Override
204     public void refresh() {
205         resetHeartbeatMonitor();
206
207         super.refresh();
208     }
209
210     @Override
211     public void bridgeThingDisposed() {
212         InsteonDevice device = getDevice();
213         if (device != null) {
214             device.stopPolling();
215             device.setModem(null);
216         }
217     }
218
219     @Override
220     public void bridgeThingUpdated(InsteonBridgeConfiguration config, InsteonModem modem) {
221         InsteonDevice device = getDevice();
222         if (device != null) {
223             device.setPollInterval(config.getDevicePollInterval());
224             device.setIsDeviceSyncEnabled(config.isDeviceSyncEnabled());
225             device.setModem(modem);
226
227             modem.addDevice(device);
228         }
229     }
230
231     public void deviceLinkDBUpdated(InsteonDevice device) {
232         if (device.getLinkDB().isComplete()) {
233             resetHeartbeatMonitor();
234
235             InsteonModem modem = getModem();
236             if (modem != null) {
237                 modem.updateSceneEntries(device);
238             }
239         }
240         updateStatus();
241     }
242
243     @Override
244     protected String getConfigInfo() {
245         return getConfigAs(InsteonDeviceConfiguration.class).toString();
246     }
247
248     @Override
249     public void updateStatus() {
250         Bridge bridge = getBridge();
251         if (bridge == null) {
252             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "No bridge selected.");
253             return;
254         }
255
256         if (bridge.getStatus() == ThingStatus.OFFLINE) {
257             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
258             return;
259         }
260
261         InsteonModem modem = getModem();
262         if (modem == null || !modem.getDB().isComplete()) {
263             updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.NONE, "Waiting for modem database.");
264             return;
265         }
266
267         InsteonDevice device = getDevice();
268         if (device == null) {
269             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Unable to determine device.");
270             return;
271         }
272
273         if (!device.hasModemDBEntry()) {
274             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
275                     "Device not found in modem database.");
276             return;
277         }
278
279         if (!device.isResponding() && !device.isBatteryPowered()) {
280             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Device not responding.");
281             return;
282         }
283
284         if (device.getProductData() == null) {
285             updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.NONE, "Waiting for product data.");
286             return;
287         }
288
289         if (device.getType() == null) {
290             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Unsupported device.");
291             return;
292         }
293
294         if (!device.getLinkDB().isComplete()) {
295             updateStatus(ThingStatus.ONLINE, ThingStatusDetail.CONFIGURATION_PENDING, "Waiting for link database.");
296             return;
297         }
298
299         updateStatus(ThingStatus.ONLINE);
300     }
301
302     public void updateProperties(InsteonDevice device) {
303         InsteonEngine engine = device.getInsteonEngine();
304         if (engine != InsteonEngine.UNKNOWN) {
305             updateProperty(PROPERTY_ENGINE_VERSION, engine.name());
306         }
307
308         super.updateProperties(device);
309     }
310
311     public void reset(InsteonDevice oldDevice) {
312         scheduler.schedule(() -> {
313             logger.debug("resetting thing {}", getThing().getUID());
314
315             dispose();
316             initialize();
317
318             scheduler.schedule(() -> {
319                 InsteonDevice device = getDevice();
320                 if (device != null) {
321                     device.replayMessages(oldDevice.getStoredMessages());
322                 }
323             }, INIT_DELAY, TimeUnit.MILLISECONDS);
324
325         }, RESET_DELAY, TimeUnit.MILLISECONDS);
326     }
327
328     public void resetHeartbeatMonitor() {
329         if (stopHeartbeatMonitor()) {
330             updateStatus();
331         }
332
333         InsteonDevice device = getDevice();
334         if (device == null || !device.hasModemDBEntry() || !device.hasHeartbeat()) {
335             return;
336         }
337
338         if (device.getMissingLinks().contains(FEATURE_HEARTBEAT)) {
339             logger.warn("heartbeat link missing, timeout monitor disabled for {}", getThing().getUID());
340             return;
341         }
342
343         int timeout = device.getHeartbeatTimeout();
344         if (timeout > 0) {
345             logger.debug("setting heartbeat timeout monitor to {} min for {}", timeout, getThing().getUID());
346
347             heartbeatJob = scheduler.schedule(() -> {
348                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Heartbeat timed out.");
349             }, timeout + HEARTBEAT_TIMEOUT_BUFFER, TimeUnit.MINUTES);
350         }
351     }
352
353     private boolean stopHeartbeatMonitor() {
354         boolean hasTimedOut = false;
355         ScheduledFuture<?> heartbeatJob = this.heartbeatJob;
356         if (heartbeatJob != null) {
357             hasTimedOut = heartbeatJob.isDone();
358             heartbeatJob.cancel(true);
359             this.heartbeatJob = null;
360         }
361         return hasTimedOut;
362     }
363 }