2 * Copyright (c) 2010-2020 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 java.lang.reflect.Type;
16 import java.math.BigDecimal;
17 import java.util.ArrayList;
18 import java.util.Collections;
19 import java.util.HashMap;
20 import java.util.HashSet;
21 import java.util.List;
24 import java.util.stream.Collectors;
25 import java.util.stream.Stream;
27 import org.eclipse.jdt.annotation.NonNullByDefault;
28 import org.eclipse.jdt.annotation.Nullable;
29 import org.openhab.binding.insteon.internal.InsteonBinding;
30 import org.openhab.binding.insteon.internal.InsteonBindingConstants;
31 import org.openhab.binding.insteon.internal.config.InsteonChannelConfiguration;
32 import org.openhab.binding.insteon.internal.config.InsteonDeviceConfiguration;
33 import org.openhab.binding.insteon.internal.device.DeviceFeature;
34 import org.openhab.binding.insteon.internal.device.DeviceTypeLoader;
35 import org.openhab.binding.insteon.internal.device.InsteonAddress;
36 import org.openhab.binding.insteon.internal.device.InsteonDevice;
37 import org.openhab.core.thing.Channel;
38 import org.openhab.core.thing.ChannelUID;
39 import org.openhab.core.thing.Thing;
40 import org.openhab.core.thing.ThingStatus;
41 import org.openhab.core.thing.ThingStatusDetail;
42 import org.openhab.core.thing.binding.BaseThingHandler;
43 import org.openhab.core.thing.type.ChannelTypeUID;
44 import org.openhab.core.types.Command;
45 import org.slf4j.Logger;
46 import org.slf4j.LoggerFactory;
48 import com.google.gson.Gson;
49 import com.google.gson.JsonParseException;
50 import com.google.gson.reflect.TypeToken;
53 * The {@link InsteonDeviceHandler} is responsible for handling commands, which are
54 * sent to one of the channels.
56 * @author Rob Nielsen - Initial contribution
59 @SuppressWarnings("null")
60 public class InsteonDeviceHandler extends BaseThingHandler {
62 private static final Set<String> ALL_CHANNEL_IDS = Collections.unmodifiableSet(Stream.of(
63 InsteonBindingConstants.AC_DELAY, InsteonBindingConstants.BACKLIGHT_DURATION,
64 InsteonBindingConstants.BATTERY_LEVEL, InsteonBindingConstants.BATTERY_PERCENT,
65 InsteonBindingConstants.BATTERY_WATERMARK_LEVEL, InsteonBindingConstants.BEEP,
66 InsteonBindingConstants.BOTTOM_OUTLET, InsteonBindingConstants.BUTTON_A, InsteonBindingConstants.BUTTON_B,
67 InsteonBindingConstants.BUTTON_C, InsteonBindingConstants.BUTTON_D, InsteonBindingConstants.BUTTON_E,
68 InsteonBindingConstants.BUTTON_F, InsteonBindingConstants.BUTTON_G, InsteonBindingConstants.BUTTON_H,
69 InsteonBindingConstants.BROADCAST_ON_OFF, InsteonBindingConstants.CONTACT,
70 InsteonBindingConstants.COOL_SET_POINT, InsteonBindingConstants.DIMMER, InsteonBindingConstants.FAN,
71 InsteonBindingConstants.FAN_MODE, InsteonBindingConstants.FAST_ON_OFF,
72 InsteonBindingConstants.FAST_ON_OFF_BUTTON_A, InsteonBindingConstants.FAST_ON_OFF_BUTTON_B,
73 InsteonBindingConstants.FAST_ON_OFF_BUTTON_C, InsteonBindingConstants.FAST_ON_OFF_BUTTON_D,
74 InsteonBindingConstants.FAST_ON_OFF_BUTTON_E, InsteonBindingConstants.FAST_ON_OFF_BUTTON_F,
75 InsteonBindingConstants.FAST_ON_OFF_BUTTON_G, InsteonBindingConstants.FAST_ON_OFF_BUTTON_H,
76 InsteonBindingConstants.HEAT_SET_POINT, InsteonBindingConstants.HUMIDITY,
77 InsteonBindingConstants.HUMIDITY_HIGH, InsteonBindingConstants.HUMIDITY_LOW,
78 InsteonBindingConstants.IS_COOLING, InsteonBindingConstants.IS_HEATING,
79 InsteonBindingConstants.KEYPAD_BUTTON_A, InsteonBindingConstants.KEYPAD_BUTTON_B,
80 InsteonBindingConstants.KEYPAD_BUTTON_C, InsteonBindingConstants.KEYPAD_BUTTON_D,
81 InsteonBindingConstants.KEYPAD_BUTTON_E, InsteonBindingConstants.KEYPAD_BUTTON_F,
82 InsteonBindingConstants.KEYPAD_BUTTON_G, InsteonBindingConstants.KEYPAD_BUTTON_H,
83 InsteonBindingConstants.KWH, InsteonBindingConstants.LAST_HEARD_FROM,
84 InsteonBindingConstants.LED_BRIGHTNESS, InsteonBindingConstants.LED_ONOFF,
85 InsteonBindingConstants.LIGHT_DIMMER, InsteonBindingConstants.LIGHT_LEVEL,
86 InsteonBindingConstants.LIGHT_LEVEL_ABOVE_THRESHOLD, InsteonBindingConstants.LOAD_DIMMER,
87 InsteonBindingConstants.LOAD_SWITCH, InsteonBindingConstants.LOAD_SWITCH_FAST_ON_OFF,
88 InsteonBindingConstants.LOAD_SWITCH_MANUAL_CHANGE, InsteonBindingConstants.LOWBATTERY,
89 InsteonBindingConstants.MANUAL_CHANGE, InsteonBindingConstants.MANUAL_CHANGE_BUTTON_A,
90 InsteonBindingConstants.MANUAL_CHANGE_BUTTON_B, InsteonBindingConstants.MANUAL_CHANGE_BUTTON_C,
91 InsteonBindingConstants.MANUAL_CHANGE_BUTTON_D, InsteonBindingConstants.MANUAL_CHANGE_BUTTON_E,
92 InsteonBindingConstants.MANUAL_CHANGE_BUTTON_F, InsteonBindingConstants.MANUAL_CHANGE_BUTTON_G,
93 InsteonBindingConstants.MANUAL_CHANGE_BUTTON_H, InsteonBindingConstants.NOTIFICATION,
94 InsteonBindingConstants.ON_LEVEL, InsteonBindingConstants.RAMP_DIMMER, InsteonBindingConstants.RAMP_RATE,
95 InsteonBindingConstants.RESET, InsteonBindingConstants.STAGE1_DURATION, InsteonBindingConstants.SWITCH,
96 InsteonBindingConstants.SYSTEM_MODE, InsteonBindingConstants.TAMPER_SWITCH,
97 InsteonBindingConstants.TEMPERATURE, InsteonBindingConstants.TEMPERATURE_LEVEL,
98 InsteonBindingConstants.TOP_OUTLET, InsteonBindingConstants.UPDATE, InsteonBindingConstants.WATTS)
99 .collect(Collectors.toSet()));
101 public static final String BROADCAST_GROUPS = "broadcastGroups";
102 public static final String BROADCAST_ON_OFF = "broadcastonoff";
103 public static final String CMD = "cmd";
104 public static final String CMD_RESET = "reset";
105 public static final String CMD_UPDATE = "update";
106 public static final String DATA = "data";
107 public static final String FIELD = "field";
108 public static final String FIELD_BATTERY_LEVEL = "battery_level";
109 public static final String FIELD_BATTERY_PERCENTAGE = "battery_percentage";
110 public static final String FIELD_BATTERY_WATERMARK_LEVEL = "battery_watermark_level";
111 public static final String FIELD_KWH = "kwh";
112 public static final String FIELD_LIGHT_LEVEL = "light_level";
113 public static final String FIELD_TEMPERATURE_LEVEL = "temperature_level";
114 public static final String FIELD_WATTS = "watts";
115 public static final String GROUP = "group";
116 public static final String METER = "meter";
118 public static final String HIDDEN_DOOR_SENSOR_PRODUCT_KEY = "F00.00.03";
119 public static final String MOTION_SENSOR_II_PRODUCT_KEY = "F00.00.24";
120 public static final String MOTION_SENSOR_PRODUCT_KEY = "0x00004A";
121 public static final String PLM_PRODUCT_KEY = "0x000045";
122 public static final String POWER_METER_PRODUCT_KEY = "F00.00.17";
124 private final Logger logger = LoggerFactory.getLogger(InsteonDeviceHandler.class);
126 private @Nullable InsteonDeviceConfiguration config;
128 public InsteonDeviceHandler(Thing thing) {
133 public void initialize() {
134 config = getConfigAs(InsteonDeviceConfiguration.class);
136 scheduler.execute(() -> {
137 if (getBridge() == null) {
138 String msg = "An Insteon network bridge has not been selected for this device.";
139 logger.warn("{} {}", thing.getUID().getAsString(), msg);
141 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, msg);
145 String address = config.getAddress();
146 if (!InsteonAddress.isValid(address)) {
147 String msg = "Unable to start Insteon device, the insteon or X10 address '" + address
148 + "' is invalid. It must be in the format 'AB.CD.EF' or 'H.U' (X10).";
149 logger.warn("{} {}", thing.getUID().getAsString(), msg);
151 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, msg);
155 String productKey = config.getProductKey();
156 if (DeviceTypeLoader.instance().getDeviceType(productKey) == null) {
157 String msg = "Unable to start Insteon device, invalid product key '" + productKey + "'.";
158 logger.warn("{} {}", thing.getUID().getAsString(), msg);
160 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, msg);
164 String deviceConfig = config.getDeviceConfig();
165 Map<String, Object> deviceConfigMap;
166 if (deviceConfig != null) {
167 Type mapType = new TypeToken<Map<String, Object>>() {
170 deviceConfigMap = new Gson().fromJson(deviceConfig, mapType);
171 } catch (JsonParseException e) {
172 String msg = "The device configuration parameter is not valid JSON.";
173 logger.warn("{} {}", thing.getUID().getAsString(), msg);
175 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, msg);
179 deviceConfigMap = Collections.emptyMap();
182 InsteonBinding insteonBinding = getInsteonBinding();
183 InsteonAddress insteonAddress = new InsteonAddress(address);
184 if (insteonBinding.getDevice(insteonAddress) != null) {
185 String msg = "A device already exists with the address '" + address + "'.";
186 logger.warn("{} {}", thing.getUID().getAsString(), msg);
188 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, msg);
192 InsteonDevice device = insteonBinding.makeNewDevice(insteonAddress, productKey, deviceConfigMap);
194 StringBuilder channelList = new StringBuilder();
195 List<Channel> channels = new ArrayList<>();
196 String thingId = getThing().getUID().getAsString();
197 for (String channelId : ALL_CHANNEL_IDS) {
198 String feature = channelId.toLowerCase();
200 if (productKey.equals(HIDDEN_DOOR_SENSOR_PRODUCT_KEY)) {
201 if (feature.equalsIgnoreCase(InsteonBindingConstants.BATTERY_LEVEL)
202 || feature.equalsIgnoreCase(InsteonBindingConstants.BATTERY_WATERMARK_LEVEL)) {
205 } else if (productKey.equals(MOTION_SENSOR_PRODUCT_KEY)) {
206 if (feature.equalsIgnoreCase(InsteonBindingConstants.BATTERY_LEVEL)
207 || feature.equalsIgnoreCase(InsteonBindingConstants.LIGHT_LEVEL)) {
210 } else if (productKey.equals(MOTION_SENSOR_II_PRODUCT_KEY)) {
211 if (feature.equalsIgnoreCase(InsteonBindingConstants.BATTERY_LEVEL)
212 || feature.equalsIgnoreCase(InsteonBindingConstants.BATTERY_PERCENT)
213 || feature.equalsIgnoreCase(InsteonBindingConstants.LIGHT_LEVEL)
214 || feature.equalsIgnoreCase(InsteonBindingConstants.TEMPERATURE_LEVEL)) {
217 } else if (productKey.equals(PLM_PRODUCT_KEY)) {
218 String parts[] = feature.split("#");
219 if (parts.length == 2 && parts[0].equalsIgnoreCase(InsteonBindingConstants.BROADCAST_ON_OFF)
220 && parts[1].matches("^\\d+$")) {
221 feature = BROADCAST_ON_OFF;
223 } else if (productKey.equals(POWER_METER_PRODUCT_KEY)) {
224 if (feature.equalsIgnoreCase(InsteonBindingConstants.KWH)
225 || feature.equalsIgnoreCase(InsteonBindingConstants.RESET)
226 || feature.equalsIgnoreCase(InsteonBindingConstants.UPDATE)
227 || feature.equalsIgnoreCase(InsteonBindingConstants.WATTS)) {
232 DeviceFeature f = device.getFeature(feature);
234 if (!f.isFeatureGroup()) {
235 if (channelId.equals(InsteonBindingConstants.BROADCAST_ON_OFF)) {
236 Set<String> broadcastChannels = new HashSet<>();
237 for (Channel channel : thing.getChannels()) {
238 String id = channel.getUID().getId();
239 if (id.startsWith(InsteonBindingConstants.BROADCAST_ON_OFF)) {
240 addChannel(channel, id, channels, channelList);
241 broadcastChannels.add(id);
245 Object groups = deviceConfigMap.get(BROADCAST_GROUPS);
246 if (groups != null) {
247 boolean valid = false;
248 if (groups instanceof List<?>) {
250 for (Object o : (List<?>) groups) {
251 if (o instanceof Double && (Double) o % 1 == 0) {
252 String id = InsteonBindingConstants.BROADCAST_ON_OFF + "#"
253 + ((Double) o).intValue();
254 if (!broadcastChannels.contains(id)) {
255 ChannelUID channelUID = new ChannelUID(thing.getUID(), id);
256 ChannelTypeUID channelTypeUID = new ChannelTypeUID(
257 InsteonBindingConstants.BINDING_ID,
258 InsteonBindingConstants.SWITCH);
259 Channel channel = getCallback()
260 .createChannelBuilder(channelUID, channelTypeUID).withLabel(id)
263 addChannel(channel, id, channels, channelList);
264 broadcastChannels.add(id);
274 String msg = "The value for key " + BROADCAST_GROUPS
275 + " must be an array of integers in the device configuration parameter.";
276 logger.warn("{} {}", thing.getUID().getAsString(), msg);
278 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, msg);
283 ChannelUID channelUID = new ChannelUID(thing.getUID(), channelId);
284 ChannelTypeUID channelTypeUID = new ChannelTypeUID(InsteonBindingConstants.BINDING_ID,
286 Channel channel = thing.getChannel(channelUID);
287 if (channel == null) {
288 channel = getCallback().createChannelBuilder(channelUID, channelTypeUID).build();
291 addChannel(channel, channelId, channels, channelList);
294 logger.debug("{} is a feature group for {}. It will not be added as a channel.", feature,
300 if (!channels.isEmpty() || device.isModem()) {
301 if (!channels.isEmpty()) {
302 updateThing(editThing().withChannels(channels).build());
305 StringBuilder builder = new StringBuilder(thingId);
306 builder.append(" address = ");
307 builder.append(address);
308 builder.append(" productKey = ");
309 builder.append(productKey);
310 builder.append(" channels = ");
311 builder.append(channelList.toString());
312 String msg = builder.toString();
313 logger.debug("{}", msg);
315 getInsteonNetworkHandler().initialized(getThing().getUID(), msg);
317 channels.forEach(channel -> {
318 if (isLinked(channel.getUID())) {
319 channelLinked(channel.getUID());
323 updateStatus(ThingStatus.ONLINE);
325 String msg = "Product key '" + productKey
326 + "' does not have any features that match existing channels.";
328 logger.warn("{} {}", thing.getUID().getAsString(), msg);
330 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, msg);
335 private void addChannel(Channel channel, String channelId, List<Channel> channels, StringBuilder channelList) {
336 channels.add(channel);
338 if (channelList.length() > 0) {
339 channelList.append(", ");
341 channelList.append(channelId);
345 public void dispose() {
346 String address = config.getAddress();
347 if (getBridge() != null && InsteonAddress.isValid(address)) {
348 getInsteonBinding().removeDevice(new InsteonAddress(address));
350 logger.debug("removed {} address = {}", getThing().getUID().getAsString(), address);
353 getInsteonNetworkHandler().disposed(getThing().getUID());
359 public void handleCommand(ChannelUID channelUID, Command command) {
360 logger.debug("channel {} was triggered with the command {}", channelUID.getAsString(), command);
362 getInsteonBinding().sendCommand(channelUID.getAsString(), command);
366 public void channelLinked(ChannelUID channelUID) {
367 if (getInsteonNetworkHandler().isChannelLinked(channelUID)) {
371 Map<String, String> params = new HashMap<>();
372 Channel channel = getThing().getChannel(channelUID.getId());
374 Map<String, Object> channelProperties = channel.getConfiguration().getProperties();
375 for (String key : channelProperties.keySet()) {
376 Object value = channelProperties.get(key);
377 if (value instanceof String) {
378 params.put(key, (String) value);
379 } else if (value instanceof BigDecimal) {
380 String s = ((BigDecimal) value).toPlainString();
383 logger.warn("not a string or big decimal value key '{}' value '{}' {}", key, value,
384 value.getClass().getName());
388 String feature = channelUID.getId().toLowerCase();
389 String productKey = config.getProductKey();
390 if (productKey.equals(HIDDEN_DOOR_SENSOR_PRODUCT_KEY)) {
391 if (feature.equalsIgnoreCase(InsteonBindingConstants.BATTERY_LEVEL)) {
392 params.put(FIELD, FIELD_BATTERY_LEVEL);
394 } else if (feature.equalsIgnoreCase(InsteonBindingConstants.BATTERY_WATERMARK_LEVEL)) {
395 params.put(FIELD, FIELD_BATTERY_WATERMARK_LEVEL);
398 } else if (productKey.equals(MOTION_SENSOR_PRODUCT_KEY)) {
399 if (feature.equalsIgnoreCase(InsteonBindingConstants.BATTERY_LEVEL)) {
400 params.put(FIELD, FIELD_BATTERY_LEVEL);
402 } else if (feature.equalsIgnoreCase(InsteonBindingConstants.LIGHT_LEVEL)) {
403 params.put(FIELD, FIELD_LIGHT_LEVEL);
406 } else if (productKey.equals(MOTION_SENSOR_II_PRODUCT_KEY)) {
407 if (feature.equalsIgnoreCase(InsteonBindingConstants.BATTERY_LEVEL)) {
408 params.put(FIELD, FIELD_BATTERY_LEVEL);
410 } else if (feature.equalsIgnoreCase(InsteonBindingConstants.BATTERY_PERCENT)) {
411 params.put(FIELD, FIELD_BATTERY_PERCENTAGE);
413 } else if (feature.equalsIgnoreCase(InsteonBindingConstants.LIGHT_LEVEL)) {
414 params.put(FIELD, FIELD_LIGHT_LEVEL);
416 } else if (feature.equalsIgnoreCase(InsteonBindingConstants.TEMPERATURE_LEVEL)) {
417 params.put(FIELD, FIELD_TEMPERATURE_LEVEL);
420 } else if (productKey.equals(PLM_PRODUCT_KEY)) {
421 String parts[] = feature.split("#");
422 if (parts.length == 2 && parts[0].equalsIgnoreCase(InsteonBindingConstants.BROADCAST_ON_OFF)
423 && parts[1].matches("^\\d+$")) {
424 params.put(GROUP, parts[1]);
425 feature = BROADCAST_ON_OFF;
427 } else if (productKey.equals(POWER_METER_PRODUCT_KEY)) {
428 if (feature.equalsIgnoreCase(InsteonBindingConstants.KWH)) {
429 params.put(FIELD, FIELD_KWH);
430 } else if (feature.equalsIgnoreCase(InsteonBindingConstants.WATTS)) {
431 params.put(FIELD, FIELD_WATTS);
432 } else if (feature.equalsIgnoreCase(InsteonBindingConstants.RESET)) {
433 params.put(CMD, CMD_RESET);
434 } else if (feature.equalsIgnoreCase(InsteonBindingConstants.UPDATE)) {
435 params.put(CMD, CMD_UPDATE);
441 InsteonChannelConfiguration bindingConfig = new InsteonChannelConfiguration(channelUID, feature,
442 new InsteonAddress(config.getAddress()), productKey, params);
443 getInsteonBinding().addFeatureListener(bindingConfig);
445 StringBuilder builder = new StringBuilder(channelUID.getAsString());
446 builder.append(" feature = ");
447 builder.append(feature);
448 builder.append(" parameters = ");
449 builder.append(params);
450 String msg = builder.toString();
451 logger.debug("{}", msg);
453 getInsteonNetworkHandler().linked(channelUID, msg);
457 public void channelUnlinked(ChannelUID channelUID) {
458 getInsteonBinding().removeFeatureListener(channelUID);
459 getInsteonNetworkHandler().unlinked(channelUID);
461 logger.debug("channel {} unlinked ", channelUID.getAsString());
464 private @Nullable InsteonNetworkHandler getInsteonNetworkHandler() {
465 return (InsteonNetworkHandler) getBridge().getHandler();
468 private @Nullable InsteonBinding getInsteonBinding() {
469 return getInsteonNetworkHandler().getInsteonBinding();