]> git.basschouten.com Git - openhab-addons.git/blob
45f80a2f7d98be3173e967efe64df14c813d0fae
[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 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;
22 import java.util.Map;
23 import java.util.Objects;
24 import java.util.Set;
25 import java.util.stream.Collectors;
26 import java.util.stream.Stream;
27
28 import org.eclipse.jdt.annotation.NonNullByDefault;
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.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.binding.BaseThingHandler;
44 import org.openhab.core.thing.binding.ThingHandlerCallback;
45 import org.openhab.core.thing.type.ChannelTypeUID;
46 import org.openhab.core.types.Command;
47 import org.slf4j.Logger;
48 import org.slf4j.LoggerFactory;
49
50 import com.google.gson.Gson;
51 import com.google.gson.JsonParseException;
52 import com.google.gson.reflect.TypeToken;
53
54 /**
55  * The {@link InsteonDeviceHandler} is responsible for handling commands, which are
56  * sent to one of the channels.
57  *
58  * @author Rob Nielsen - Initial contribution
59  */
60 @NonNullByDefault
61 public class InsteonDeviceHandler extends BaseThingHandler {
62
63     private static final Set<String> ALL_CHANNEL_IDS = Collections.unmodifiableSet(Stream.of(
64             InsteonBindingConstants.AC_DELAY, InsteonBindingConstants.BACKLIGHT_DURATION,
65             InsteonBindingConstants.BATTERY_LEVEL, InsteonBindingConstants.BATTERY_PERCENT,
66             InsteonBindingConstants.BATTERY_WATERMARK_LEVEL, InsteonBindingConstants.BEEP,
67             InsteonBindingConstants.BOTTOM_OUTLET, InsteonBindingConstants.BUTTON_A, InsteonBindingConstants.BUTTON_B,
68             InsteonBindingConstants.BUTTON_C, InsteonBindingConstants.BUTTON_D, InsteonBindingConstants.BUTTON_E,
69             InsteonBindingConstants.BUTTON_F, InsteonBindingConstants.BUTTON_G, InsteonBindingConstants.BUTTON_H,
70             InsteonBindingConstants.BROADCAST_ON_OFF, InsteonBindingConstants.CONTACT,
71             InsteonBindingConstants.COOL_SET_POINT, InsteonBindingConstants.DIMMER, InsteonBindingConstants.FAN,
72             InsteonBindingConstants.FAN_MODE, InsteonBindingConstants.FAST_ON_OFF,
73             InsteonBindingConstants.FAST_ON_OFF_BUTTON_A, InsteonBindingConstants.FAST_ON_OFF_BUTTON_B,
74             InsteonBindingConstants.FAST_ON_OFF_BUTTON_C, InsteonBindingConstants.FAST_ON_OFF_BUTTON_D,
75             InsteonBindingConstants.FAST_ON_OFF_BUTTON_E, InsteonBindingConstants.FAST_ON_OFF_BUTTON_F,
76             InsteonBindingConstants.FAST_ON_OFF_BUTTON_G, InsteonBindingConstants.FAST_ON_OFF_BUTTON_H,
77             InsteonBindingConstants.HEAT_SET_POINT, InsteonBindingConstants.HUMIDITY,
78             InsteonBindingConstants.HUMIDITY_HIGH, InsteonBindingConstants.HUMIDITY_LOW,
79             InsteonBindingConstants.IS_COOLING, InsteonBindingConstants.IS_HEATING,
80             InsteonBindingConstants.KEYPAD_BUTTON_A, InsteonBindingConstants.KEYPAD_BUTTON_B,
81             InsteonBindingConstants.KEYPAD_BUTTON_C, InsteonBindingConstants.KEYPAD_BUTTON_D,
82             InsteonBindingConstants.KEYPAD_BUTTON_E, InsteonBindingConstants.KEYPAD_BUTTON_F,
83             InsteonBindingConstants.KEYPAD_BUTTON_G, InsteonBindingConstants.KEYPAD_BUTTON_H,
84             InsteonBindingConstants.KWH, InsteonBindingConstants.LAST_HEARD_FROM,
85             InsteonBindingConstants.LED_BRIGHTNESS, InsteonBindingConstants.LED_ONOFF,
86             InsteonBindingConstants.LIGHT_DIMMER, InsteonBindingConstants.LIGHT_LEVEL,
87             InsteonBindingConstants.LIGHT_LEVEL_ABOVE_THRESHOLD, InsteonBindingConstants.LOAD_DIMMER,
88             InsteonBindingConstants.LOAD_SWITCH, InsteonBindingConstants.LOAD_SWITCH_FAST_ON_OFF,
89             InsteonBindingConstants.LOAD_SWITCH_MANUAL_CHANGE, InsteonBindingConstants.LOWBATTERY,
90             InsteonBindingConstants.MANUAL_CHANGE, InsteonBindingConstants.MANUAL_CHANGE_BUTTON_A,
91             InsteonBindingConstants.MANUAL_CHANGE_BUTTON_B, InsteonBindingConstants.MANUAL_CHANGE_BUTTON_C,
92             InsteonBindingConstants.MANUAL_CHANGE_BUTTON_D, InsteonBindingConstants.MANUAL_CHANGE_BUTTON_E,
93             InsteonBindingConstants.MANUAL_CHANGE_BUTTON_F, InsteonBindingConstants.MANUAL_CHANGE_BUTTON_G,
94             InsteonBindingConstants.MANUAL_CHANGE_BUTTON_H, InsteonBindingConstants.NOTIFICATION,
95             InsteonBindingConstants.ON_LEVEL, InsteonBindingConstants.RAMP_DIMMER, InsteonBindingConstants.RAMP_RATE,
96             InsteonBindingConstants.RESET, InsteonBindingConstants.STAGE1_DURATION, InsteonBindingConstants.SWITCH,
97             InsteonBindingConstants.SYSTEM_MODE, InsteonBindingConstants.TAMPER_SWITCH,
98             InsteonBindingConstants.TEMPERATURE, InsteonBindingConstants.TEMPERATURE_LEVEL,
99             InsteonBindingConstants.TOP_OUTLET, InsteonBindingConstants.UPDATE, InsteonBindingConstants.WATTS)
100             .collect(Collectors.toSet()));
101
102     public static final String BROADCAST_GROUPS = "broadcastGroups";
103     public static final String BROADCAST_ON_OFF = "broadcastonoff";
104     public static final String CMD = "cmd";
105     public static final String CMD_RESET = "reset";
106     public static final String CMD_UPDATE = "update";
107     public static final String DATA = "data";
108     public static final String FIELD = "field";
109     public static final String FIELD_BATTERY_LEVEL = "battery_level";
110     public static final String FIELD_BATTERY_PERCENTAGE = "battery_percentage";
111     public static final String FIELD_BATTERY_WATERMARK_LEVEL = "battery_watermark_level";
112     public static final String FIELD_KWH = "kwh";
113     public static final String FIELD_LIGHT_LEVEL = "light_level";
114     public static final String FIELD_TEMPERATURE_LEVEL = "temperature_level";
115     public static final String FIELD_WATTS = "watts";
116     public static final String GROUP = "group";
117     public static final String METER = "meter";
118
119     public static final String HIDDEN_DOOR_SENSOR_PRODUCT_KEY = "F00.00.03";
120     public static final String MOTION_SENSOR_II_PRODUCT_KEY = "F00.00.24";
121     public static final String MOTION_SENSOR_PRODUCT_KEY = "0x00004A";
122     public static final String PLM_PRODUCT_KEY = "0x000045";
123     public static final String POWER_METER_PRODUCT_KEY = "F00.00.17";
124
125     private final Logger logger = LoggerFactory.getLogger(InsteonDeviceHandler.class);
126
127     private @NonNullByDefault({}) InsteonDeviceConfiguration config;
128     private boolean deviceLinked = true;
129
130     public InsteonDeviceHandler(Thing thing) {
131         super(thing);
132     }
133
134     @Override
135     public void initialize() {
136         config = getConfigAs(InsteonDeviceConfiguration.class);
137         deviceLinked = true;
138
139         scheduler.execute(() -> {
140             final Bridge bridge = getBridge();
141             if (bridge == null) {
142                 String msg = "An Insteon network bridge has not been selected for this device.";
143                 logger.warn("{} {}", thing.getUID().getAsString(), msg);
144
145                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, msg);
146                 return;
147             }
148
149             InsteonDeviceConfiguration config = this.config;
150             if (config == null) {
151                 String msg = "Insteon device configuration is null.";
152                 logger.warn("{} {}", thing.getUID().getAsString(), msg);
153
154                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, msg);
155                 return;
156             }
157             String address = config.getAddress();
158             if (!InsteonAddress.isValid(address)) {
159                 String msg = "Unable to start Insteon device, the insteon or X10 address '" + address
160                         + "' is invalid. It must be in the format 'AB.CD.EF' or 'H.U' (X10).";
161                 logger.warn("{} {}", thing.getUID().getAsString(), msg);
162
163                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, msg);
164                 return;
165             }
166
167             DeviceTypeLoader instance = DeviceTypeLoader.instance();
168             if (instance == null) {
169                 String msg = "Device type loader is null.";
170                 logger.warn("{} {}", thing.getUID().getAsString(), msg);
171
172                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, msg);
173                 return;
174             }
175
176             String productKey = config.getProductKey();
177             if (instance.getDeviceType(productKey) == null) {
178                 String msg = "Unable to start Insteon device, invalid product key '" + productKey + "'.";
179                 logger.warn("{} {}", thing.getUID().getAsString(), msg);
180
181                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, msg);
182                 return;
183             }
184
185             String deviceConfig = config.getDeviceConfig();
186             Map<String, Object> deviceConfigMap;
187             if (deviceConfig != null) {
188                 Type mapType = new TypeToken<Map<String, Object>>() {
189                 }.getType();
190                 try {
191                     deviceConfigMap = Objects.requireNonNull(new Gson().fromJson(deviceConfig, mapType));
192                 } catch (JsonParseException e) {
193                     String msg = "The device configuration parameter is not valid JSON.";
194                     logger.warn("{} {}", thing.getUID().getAsString(), msg);
195
196                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, msg);
197                     return;
198                 }
199             } else {
200                 deviceConfigMap = Collections.emptyMap();
201             }
202
203             InsteonBinding insteonBinding = getInsteonBinding();
204             InsteonAddress insteonAddress = new InsteonAddress(address);
205             if (insteonBinding.getDevice(insteonAddress) != null) {
206                 String msg = "A device already exists with the address '" + address + "'.";
207                 logger.warn("{} {}", thing.getUID().getAsString(), msg);
208
209                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, msg);
210                 return;
211             }
212
213             InsteonDevice device = insteonBinding.makeNewDevice(insteonAddress, productKey, deviceConfigMap);
214             if (device == null) {
215                 String msg = "Unable to create a device with the product key '" + productKey + "' with the address'"
216                         + address + "'.";
217                 logger.warn("{} {}", thing.getUID().getAsString(), msg);
218
219                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, msg);
220                 return;
221             }
222
223             ThingHandlerCallback callback = getCallback();
224             if (callback == null) {
225                 String msg = "Unable to get thing handler callback.";
226                 logger.warn("{} {}", thing.getUID().getAsString(), msg);
227
228                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, msg);
229                 return;
230             }
231
232             Map<String, Channel> channelMap = new HashMap<>();
233             String thingId = getThing().getUID().getAsString();
234             for (String channelId : ALL_CHANNEL_IDS) {
235                 String feature = channelId.toLowerCase();
236
237                 if (productKey.equals(HIDDEN_DOOR_SENSOR_PRODUCT_KEY)) {
238                     if (feature.equalsIgnoreCase(InsteonBindingConstants.BATTERY_LEVEL)
239                             || feature.equalsIgnoreCase(InsteonBindingConstants.BATTERY_WATERMARK_LEVEL)) {
240                         feature = DATA;
241                     }
242                 } else if (productKey.equals(MOTION_SENSOR_PRODUCT_KEY)) {
243                     if (feature.equalsIgnoreCase(InsteonBindingConstants.BATTERY_LEVEL)
244                             || feature.equalsIgnoreCase(InsteonBindingConstants.LIGHT_LEVEL)) {
245                         feature = DATA;
246                     }
247                 } else if (productKey.equals(MOTION_SENSOR_II_PRODUCT_KEY)) {
248                     if (feature.equalsIgnoreCase(InsteonBindingConstants.BATTERY_LEVEL)
249                             || feature.equalsIgnoreCase(InsteonBindingConstants.BATTERY_PERCENT)
250                             || feature.equalsIgnoreCase(InsteonBindingConstants.LIGHT_LEVEL)
251                             || feature.equalsIgnoreCase(InsteonBindingConstants.TEMPERATURE_LEVEL)) {
252                         feature = DATA;
253                     }
254                 } else if (productKey.equals(PLM_PRODUCT_KEY)) {
255                     String[] parts = feature.split("#");
256                     if (parts.length == 2 && parts[0].equalsIgnoreCase(InsteonBindingConstants.BROADCAST_ON_OFF)
257                             && parts[1].matches("^\\d+$")) {
258                         feature = BROADCAST_ON_OFF;
259                     }
260                 } else if (productKey.equals(POWER_METER_PRODUCT_KEY)) {
261                     if (feature.equalsIgnoreCase(InsteonBindingConstants.KWH)
262                             || feature.equalsIgnoreCase(InsteonBindingConstants.RESET)
263                             || feature.equalsIgnoreCase(InsteonBindingConstants.UPDATE)
264                             || feature.equalsIgnoreCase(InsteonBindingConstants.WATTS)) {
265                         feature = METER;
266                     }
267                 }
268
269                 DeviceFeature f = device.getFeature(feature);
270                 if (f != null) {
271                     if (!f.isFeatureGroup()) {
272                         if (channelId.equals(InsteonBindingConstants.BROADCAST_ON_OFF)) {
273                             Set<String> broadcastChannels = new HashSet<>();
274                             for (Channel channel : thing.getChannels()) {
275                                 String id = channel.getUID().getId();
276                                 if (id.startsWith(InsteonBindingConstants.BROADCAST_ON_OFF)) {
277                                     channelMap.put(id, channel);
278                                     broadcastChannels.add(id);
279                                 }
280                             }
281
282                             Object groups = deviceConfigMap.get(BROADCAST_GROUPS);
283                             if (groups != null) {
284                                 boolean valid = false;
285                                 if (groups instanceof List<?> list) {
286                                     valid = true;
287                                     for (Object o : list) {
288                                         if (o instanceof Double && (Double) o % 1 == 0) {
289                                             String id = InsteonBindingConstants.BROADCAST_ON_OFF + "#"
290                                                     + ((Double) o).intValue();
291                                             if (!broadcastChannels.contains(id)) {
292                                                 ChannelUID channelUID = new ChannelUID(thing.getUID(), id);
293                                                 ChannelTypeUID channelTypeUID = new ChannelTypeUID(
294                                                         InsteonBindingConstants.BINDING_ID,
295                                                         InsteonBindingConstants.SWITCH);
296                                                 Channel channel = callback
297                                                         .createChannelBuilder(channelUID, channelTypeUID).withLabel(id)
298                                                         .build();
299
300                                                 channelMap.put(id, channel);
301                                                 broadcastChannels.add(id);
302                                             }
303                                         } else {
304                                             valid = false;
305                                             break;
306                                         }
307                                     }
308                                 }
309
310                                 if (!valid) {
311                                     String msg = "The value for key " + BROADCAST_GROUPS
312                                             + " must be an array of integers in the device configuration parameter.";
313                                     logger.warn("{} {}", thing.getUID().getAsString(), msg);
314
315                                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, msg);
316                                     return;
317                                 }
318                             }
319                         } else {
320                             ChannelUID channelUID = new ChannelUID(thing.getUID(), channelId);
321                             ChannelTypeUID channelTypeUID = new ChannelTypeUID(InsteonBindingConstants.BINDING_ID,
322                                     channelId);
323                             Channel channel = thing.getChannel(channelUID);
324                             if (channel == null) {
325                                 channel = callback.createChannelBuilder(channelUID, channelTypeUID).build();
326                             }
327
328                             channelMap.put(channelId, channel);
329                         }
330                     } else {
331                         logger.debug("{} is a feature group for {}. It will not be added as a channel.", feature,
332                                 productKey);
333                     }
334                 }
335             }
336
337             if (!channelMap.isEmpty() || device.isModem()) {
338                 List<Channel> channels = new ArrayList<>();
339                 StringBuilder channelList = new StringBuilder();
340                 if (!channelMap.isEmpty()) {
341                     List<String> channelIds = new ArrayList<>(channelMap.keySet());
342                     Collections.sort(channelIds);
343                     channelIds.forEach(channelId -> {
344                         Channel channel = channelMap.get(channelId);
345                         if (channel != null) {
346                             channels.add(channel);
347
348                             if (channelList.length() > 0) {
349                                 channelList.append(", ");
350                             }
351                             channelList.append(channelId);
352                         }
353                     });
354
355                     updateThing(editThing().withChannels(channels).build());
356                 }
357
358                 StringBuilder builder = new StringBuilder(thingId);
359                 builder.append(" address = ");
360                 builder.append(address);
361                 builder.append(" productKey = ");
362                 builder.append(productKey);
363                 builder.append(" channels = ");
364                 builder.append(channelList.toString());
365                 String msg = builder.toString();
366                 logger.debug("{}", msg);
367
368                 getInsteonNetworkHandler().initialized(getThing().getUID(), msg);
369
370                 channels.forEach(channel -> {
371                     if (isLinked(channel.getUID())) {
372                         channelLinked(channel.getUID());
373                     }
374                 });
375
376                 if (ThingStatus.ONLINE == bridge.getStatus()) {
377                     if (deviceLinked) {
378                         updateStatus(ThingStatus.ONLINE);
379                     }
380                 } else {
381                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
382                 }
383             } else {
384                 String msg = "Product key '" + productKey
385                         + "' does not have any features that match existing channels.";
386
387                 logger.warn("{} {}", thing.getUID().getAsString(), msg);
388
389                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, msg);
390             }
391         });
392     }
393
394     @Override
395     public void dispose() {
396         InsteonDeviceConfiguration config = this.config;
397         if (config != null) {
398             String address = config.getAddress();
399             if (getBridge() != null && InsteonAddress.isValid(address)) {
400                 getInsteonBinding().removeDevice(new InsteonAddress(address));
401
402                 logger.debug("removed {} address = {}", getThing().getUID().getAsString(), address);
403             }
404
405             InsteonNetworkHandler handler = null;
406             try {
407                 handler = getInsteonNetworkHandler();
408             } catch (IllegalArgumentException e) {
409             }
410
411             if (handler != null) {
412                 handler.disposed(getThing().getUID());
413             }
414         }
415
416         super.dispose();
417     }
418
419     @Override
420     public void handleCommand(ChannelUID channelUID, Command command) {
421         if (ThingStatus.ONLINE == getThing().getStatus()) {
422             logger.debug("channel {} was triggered with the command {}", channelUID.getAsString(), command);
423
424             getInsteonBinding().sendCommand(channelUID.getAsString(), command);
425         } else {
426             logger.debug("the command {} for channel {} was ignored because the thing is not ONLINE", command,
427                     channelUID.getAsString());
428         }
429     }
430
431     @Override
432     public void channelLinked(ChannelUID channelUID) {
433         if (getInsteonNetworkHandler().isChannelLinked(channelUID)) {
434             return;
435         }
436
437         Map<String, String> params = new HashMap<>();
438         Channel channel = getThing().getChannel(channelUID.getId());
439         if (channel == null) {
440             logger.warn("channel is null");
441             return;
442         }
443
444         Map<String, Object> channelProperties = channel.getConfiguration().getProperties();
445         for (String key : channelProperties.keySet()) {
446             Object value = channelProperties.get(key);
447             if (value instanceof String stringValue) {
448                 params.put(key, stringValue);
449             } else if (value instanceof BigDecimal decimalValue) {
450                 String s = decimalValue.toPlainString();
451                 params.put(key, s);
452             } else {
453                 logger.warn("not a string or big decimal value key '{}' value '{}' {}", key, value,
454                         value != null ? value.getClass().getName() : "unknown");
455             }
456         }
457
458         String feature = channelUID.getId().toLowerCase();
459         InsteonDeviceConfiguration config = this.config;
460         if (config == null) {
461             logger.warn("insteon device config is null");
462             return;
463         }
464         String productKey = config.getProductKey();
465         if (productKey.equals(HIDDEN_DOOR_SENSOR_PRODUCT_KEY)) {
466             if (feature.equalsIgnoreCase(InsteonBindingConstants.BATTERY_LEVEL)) {
467                 params.put(FIELD, FIELD_BATTERY_LEVEL);
468                 feature = DATA;
469             } else if (feature.equalsIgnoreCase(InsteonBindingConstants.BATTERY_WATERMARK_LEVEL)) {
470                 params.put(FIELD, FIELD_BATTERY_WATERMARK_LEVEL);
471                 feature = DATA;
472             }
473         } else if (productKey.equals(MOTION_SENSOR_PRODUCT_KEY)) {
474             if (feature.equalsIgnoreCase(InsteonBindingConstants.BATTERY_LEVEL)) {
475                 params.put(FIELD, FIELD_BATTERY_LEVEL);
476                 feature = DATA;
477             } else if (feature.equalsIgnoreCase(InsteonBindingConstants.LIGHT_LEVEL)) {
478                 params.put(FIELD, FIELD_LIGHT_LEVEL);
479                 feature = DATA;
480             }
481         } else if (productKey.equals(MOTION_SENSOR_II_PRODUCT_KEY)) {
482             if (feature.equalsIgnoreCase(InsteonBindingConstants.BATTERY_LEVEL)) {
483                 params.put(FIELD, FIELD_BATTERY_LEVEL);
484                 feature = DATA;
485             } else if (feature.equalsIgnoreCase(InsteonBindingConstants.BATTERY_PERCENT)) {
486                 params.put(FIELD, FIELD_BATTERY_PERCENTAGE);
487                 feature = DATA;
488             } else if (feature.equalsIgnoreCase(InsteonBindingConstants.LIGHT_LEVEL)) {
489                 params.put(FIELD, FIELD_LIGHT_LEVEL);
490                 feature = DATA;
491             } else if (feature.equalsIgnoreCase(InsteonBindingConstants.TEMPERATURE_LEVEL)) {
492                 params.put(FIELD, FIELD_TEMPERATURE_LEVEL);
493                 feature = DATA;
494             }
495         } else if (productKey.equals(PLM_PRODUCT_KEY)) {
496             String[] parts = feature.split("#");
497             if (parts.length == 2 && parts[0].equalsIgnoreCase(InsteonBindingConstants.BROADCAST_ON_OFF)
498                     && parts[1].matches("^\\d+$")) {
499                 params.put(GROUP, parts[1]);
500                 feature = BROADCAST_ON_OFF;
501             }
502         } else if (productKey.equals(POWER_METER_PRODUCT_KEY)) {
503             if (feature.equalsIgnoreCase(InsteonBindingConstants.KWH)) {
504                 params.put(FIELD, FIELD_KWH);
505             } else if (feature.equalsIgnoreCase(InsteonBindingConstants.WATTS)) {
506                 params.put(FIELD, FIELD_WATTS);
507             } else if (feature.equalsIgnoreCase(InsteonBindingConstants.RESET)) {
508                 params.put(CMD, CMD_RESET);
509             } else if (feature.equalsIgnoreCase(InsteonBindingConstants.UPDATE)) {
510                 params.put(CMD, CMD_UPDATE);
511             }
512
513             feature = METER;
514         }
515
516         InsteonChannelConfiguration bindingConfig = new InsteonChannelConfiguration(channelUID, feature,
517                 new InsteonAddress(config.getAddress()), productKey, params);
518         getInsteonBinding().addFeatureListener(bindingConfig);
519
520         StringBuilder builder = new StringBuilder(channelUID.getAsString());
521         builder.append(" feature = ");
522         builder.append(feature);
523         builder.append(" parameters = ");
524         builder.append(params);
525         String msg = builder.toString();
526         logger.debug("{}", msg);
527
528         getInsteonNetworkHandler().linked(channelUID, msg);
529     }
530
531     @Override
532     public void channelUnlinked(ChannelUID channelUID) {
533         getInsteonBinding().removeFeatureListener(channelUID);
534         getInsteonNetworkHandler().unlinked(channelUID);
535
536         logger.debug("channel {} unlinked ", channelUID.getAsString());
537     }
538
539     public InsteonAddress getInsteonAddress() {
540         return new InsteonAddress(config.getAddress());
541     }
542
543     public void deviceNotLinked() {
544         String msg = "device with the address '" + config.getAddress()
545                 + "' was not found in the modem database. Did you forget to link?";
546         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, msg);
547
548         deviceLinked = false;
549     }
550
551     private InsteonNetworkHandler getInsteonNetworkHandler() {
552         Bridge bridge = getBridge();
553         if (bridge == null) {
554             throw new IllegalArgumentException("insteon network bridge is null");
555         }
556         InsteonNetworkHandler handler = (InsteonNetworkHandler) bridge.getHandler();
557         if (handler == null) {
558             throw new IllegalArgumentException("insteon network handler is null");
559         }
560         return handler;
561     }
562
563     private InsteonBinding getInsteonBinding() {
564         return getInsteonNetworkHandler().getInsteonBinding();
565     }
566 }