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.List;
19 import java.util.concurrent.ScheduledFuture;
20 import java.util.concurrent.TimeUnit;
21 import java.util.stream.Collectors;
22 import java.util.stream.Stream;
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;
50 * The {@link InsteonDeviceHandler} represents an Insteon device handler.
52 * @author Jeremy Setton - Initial contribution
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
60 private @Nullable InsteonDevice device;
61 private @Nullable ScheduledFuture<?> heartbeatJob;
62 private InsteonStateDescriptionProvider stateDescriptionProvider;
64 public InsteonDeviceHandler(Thing thing, InsteonStateDescriptionProvider stateDescriptionProvider) {
66 this.stateDescriptionProvider = stateDescriptionProvider;
70 public @Nullable InsteonDevice getDevice() {
75 public void initialize() {
76 InsteonDeviceConfiguration config = getConfigAs(InsteonDeviceConfiguration.class);
78 scheduler.execute(() -> {
79 Bridge bridge = getBridge();
81 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "No bridge selected.");
85 if (bridge.getThingTypeUID().equals(THING_TYPE_LEGACY_NETWORK)) {
86 changeThingType(THING_TYPE_LEGACY_DEVICE, bridge.getHandler());
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'.");
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.");
103 InsteonDevice device = createDevice(address, modem);
104 this.device = device;
107 modem.addDevice(device);
110 initializeChannels(device);
111 updateProperties(device);
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));
121 legacyNetworkHandler.addChannelConfigs(channelConfigs);
124 changeThingType(thingTypeUID, getConfig());
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);
136 device = InsteonDevice.makeDevice(address, modem, null);
138 device.setHandler(this);
144 protected void initializeChannels(Device device) {
145 DeviceType deviceType = device.getType();
146 if (deviceType == null) {
150 super.initializeChannels(device);
152 getThing().getChannels().forEach(channel -> setChannelCustomSettings(channel, deviceType.getName()));
155 private void setChannelCustomSettings(Channel channel, String deviceTypeName) {
156 ChannelUID channelUID = channel.getUID();
157 ChannelTypeUID channelTypeUID = channel.getChannelTypeUID();
158 if (channelTypeUID == null) {
162 String key = deviceTypeName + ":" + channelIdToFeatureName(channelTypeUID.getId());
163 String[] stateDescriptionOptions = CUSTOM_STATE_DESCRIPTION_OPTIONS.get(key);
164 if (stateDescriptionOptions == null) {
168 List<StateOption> options = Stream.of(stateDescriptionOptions).map(value -> new StateOption(value,
169 StringUtils.capitalizeByWhitespace(value.replace("_", " ").toLowerCase()))).toList();
171 logger.trace("setting state options for {} to {}", channelUID, options);
173 stateDescriptionProvider.setStateOptions(channelUID, options);
177 public void dispose() {
178 InsteonDevice device = getDevice();
179 if (device != null) {
180 device.stopPolling();
182 InsteonModem modem = getModem();
184 modem.deleteSceneEntries(device);
185 modem.removeDevice(device);
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());
198 stopHeartbeatMonitor();
204 public void refresh() {
205 resetHeartbeatMonitor();
211 public void bridgeThingDisposed() {
212 InsteonDevice device = getDevice();
213 if (device != null) {
214 device.stopPolling();
215 device.setModem(null);
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);
227 modem.addDevice(device);
231 public void deviceLinkDBUpdated(InsteonDevice device) {
232 if (device.getLinkDB().isComplete()) {
233 resetHeartbeatMonitor();
235 InsteonModem modem = getModem();
237 modem.updateSceneEntries(device);
244 protected String getConfigInfo() {
245 return getConfigAs(InsteonDeviceConfiguration.class).toString();
249 public void updateStatus() {
250 Bridge bridge = getBridge();
251 if (bridge == null) {
252 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "No bridge selected.");
256 if (bridge.getStatus() == ThingStatus.OFFLINE) {
257 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
261 InsteonModem modem = getModem();
262 if (modem == null || !modem.getDB().isComplete()) {
263 updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.NONE, "Waiting for modem database.");
267 InsteonDevice device = getDevice();
268 if (device == null) {
269 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Unable to determine device.");
273 if (!device.hasModemDBEntry()) {
274 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
275 "Device not found in modem database.");
279 if (!device.isResponding() && !device.isBatteryPowered()) {
280 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Device not responding.");
284 if (device.getProductData() == null) {
285 updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.NONE, "Waiting for product data.");
289 if (device.getType() == null) {
290 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Unsupported device.");
294 if (!device.getLinkDB().isComplete()) {
295 updateStatus(ThingStatus.ONLINE, ThingStatusDetail.CONFIGURATION_PENDING, "Waiting for link database.");
299 updateStatus(ThingStatus.ONLINE);
302 public void updateProperties(InsteonDevice device) {
303 InsteonEngine engine = device.getInsteonEngine();
304 if (engine != InsteonEngine.UNKNOWN) {
305 updateProperty(PROPERTY_ENGINE_VERSION, engine.name());
308 super.updateProperties(device);
311 public void reset(InsteonDevice oldDevice) {
312 scheduler.schedule(() -> {
313 logger.debug("resetting thing {}", getThing().getUID());
318 scheduler.schedule(() -> {
319 InsteonDevice device = getDevice();
320 if (device != null) {
321 device.replayMessages(oldDevice.getStoredMessages());
323 }, INIT_DELAY, TimeUnit.MILLISECONDS);
325 }, RESET_DELAY, TimeUnit.MILLISECONDS);
328 public void resetHeartbeatMonitor() {
329 if (stopHeartbeatMonitor()) {
333 InsteonDevice device = getDevice();
334 if (device == null || !device.hasModemDBEntry() || !device.hasHeartbeat()) {
338 if (device.getMissingLinks().contains(FEATURE_HEARTBEAT)) {
339 logger.warn("heartbeat link missing, timeout monitor disabled for {}", getThing().getUID());
343 int timeout = device.getHeartbeatTimeout();
345 logger.debug("setting heartbeat timeout monitor to {} min for {}", timeout, getThing().getUID());
347 heartbeatJob = scheduler.schedule(() -> {
348 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Heartbeat timed out.");
349 }, timeout + HEARTBEAT_TIMEOUT_BUFFER, TimeUnit.MINUTES);
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;