]> git.basschouten.com Git - openhab-addons.git/blob
6c483ed8d1546078c1043a778262fa912ada5789
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2020 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.Set;
24 import java.util.stream.Collectors;
25 import java.util.stream.Stream;
26
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;
47
48 import com.google.gson.Gson;
49 import com.google.gson.JsonParseException;
50 import com.google.gson.reflect.TypeToken;
51
52 /**
53  * The {@link InsteonDeviceHandler} is responsible for handling commands, which are
54  * sent to one of the channels.
55  *
56  * @author Rob Nielsen - Initial contribution
57  */
58 @NonNullByDefault
59 @SuppressWarnings("null")
60 public class InsteonDeviceHandler extends BaseThingHandler {
61
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()));
100
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";
117
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";
123
124     private final Logger logger = LoggerFactory.getLogger(InsteonDeviceHandler.class);
125
126     private @Nullable InsteonDeviceConfiguration config;
127
128     public InsteonDeviceHandler(Thing thing) {
129         super(thing);
130     }
131
132     @Override
133     public void initialize() {
134         config = getConfigAs(InsteonDeviceConfiguration.class);
135
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);
140
141                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, msg);
142                 return;
143             }
144
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);
150
151                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, msg);
152                 return;
153             }
154
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);
159
160                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, msg);
161                 return;
162             }
163
164             String deviceConfig = config.getDeviceConfig();
165             Map<String, @Nullable Object> deviceConfigMap;
166             if (deviceConfig != null) {
167                 Type mapType = new TypeToken<Map<String, Object>>() {
168                 }.getType();
169                 try {
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);
174
175                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, msg);
176                     return;
177                 }
178             } else {
179                 deviceConfigMap = Collections.emptyMap();
180             }
181
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);
187
188                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, msg);
189                 return;
190             }
191
192             InsteonDevice device = insteonBinding.makeNewDevice(insteonAddress, productKey, deviceConfigMap);
193
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();
199
200                 if (productKey.equals(HIDDEN_DOOR_SENSOR_PRODUCT_KEY)) {
201                     if (feature.equalsIgnoreCase(InsteonBindingConstants.BATTERY_LEVEL)
202                             || feature.equalsIgnoreCase(InsteonBindingConstants.BATTERY_WATERMARK_LEVEL)) {
203                         feature = DATA;
204                     }
205                 } else if (productKey.equals(MOTION_SENSOR_PRODUCT_KEY)) {
206                     if (feature.equalsIgnoreCase(InsteonBindingConstants.BATTERY_LEVEL)
207                             || feature.equalsIgnoreCase(InsteonBindingConstants.LIGHT_LEVEL)) {
208                         feature = DATA;
209                     }
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)) {
215                         feature = DATA;
216                     }
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;
222                     }
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)) {
228                         feature = METER;
229                     }
230                 }
231
232                 DeviceFeature f = device.getFeature(feature);
233                 if (f != null) {
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);
242                                 }
243                             }
244
245                             Object groups = deviceConfigMap.get(BROADCAST_GROUPS);
246                             if (groups != null) {
247                                 boolean valid = false;
248                                 if (groups instanceof List<?>) {
249                                     valid = true;
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)
261                                                         .build();
262
263                                                 addChannel(channel, id, channels, channelList);
264                                                 broadcastChannels.add(id);
265                                             }
266                                         } else {
267                                             valid = false;
268                                             break;
269                                         }
270                                     }
271                                 }
272
273                                 if (!valid) {
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);
277
278                                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, msg);
279                                     return;
280                                 }
281                             }
282                         } else {
283                             ChannelUID channelUID = new ChannelUID(thing.getUID(), channelId);
284                             ChannelTypeUID channelTypeUID = new ChannelTypeUID(InsteonBindingConstants.BINDING_ID,
285                                     channelId);
286                             Channel channel = thing.getChannel(channelUID);
287                             if (channel == null) {
288                                 channel = getCallback().createChannelBuilder(channelUID, channelTypeUID).build();
289                             }
290
291                             addChannel(channel, channelId, channels, channelList);
292                         }
293                     } else {
294                         logger.debug("{} is a feature group for {}. It will not be added as a channel.", feature,
295                                 productKey);
296                     }
297                 }
298             }
299
300             if (!channels.isEmpty() || device.isModem()) {
301                 if (!channels.isEmpty()) {
302                     updateThing(editThing().withChannels(channels).build());
303                 }
304
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);
314
315                 getInsteonNetworkHandler().initialized(getThing().getUID(), msg);
316
317                 updateStatus(ThingStatus.ONLINE);
318             } else {
319                 String msg = "Product key '" + productKey
320                         + "' does not have any features that match existing channels.";
321
322                 logger.warn("{} {}", thing.getUID().getAsString(), msg);
323
324                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, msg);
325             }
326         });
327     }
328
329     private void addChannel(Channel channel, String channelId, List<Channel> channels, StringBuilder channelList) {
330         channels.add(channel);
331
332         if (channelList.length() > 0) {
333             channelList.append(", ");
334         }
335         channelList.append(channelId);
336     }
337
338     @Override
339     public void dispose() {
340         String address = config.getAddress();
341         if (getBridge() != null && InsteonAddress.isValid(address)) {
342             getInsteonBinding().removeDevice(new InsteonAddress(address));
343
344             logger.debug("removed {} address = {}", getThing().getUID().getAsString(), address);
345         }
346
347         getInsteonNetworkHandler().disposed(getThing().getUID());
348
349         super.dispose();
350     }
351
352     @Override
353     public void handleCommand(ChannelUID channelUID, Command command) {
354         logger.debug("channel {} was triggered with the command {}", channelUID.getAsString(), command);
355
356         getInsteonBinding().sendCommand(channelUID.getAsString(), command);
357     }
358
359     @Override
360     public void channelLinked(ChannelUID channelUID) {
361         Map<String, @Nullable String> params = new HashMap<>();
362         Channel channel = getThing().getChannel(channelUID.getId());
363
364         Map<String, Object> channelProperties = channel.getConfiguration().getProperties();
365         for (String key : channelProperties.keySet()) {
366             Object value = channelProperties.get(key);
367             if (value instanceof String) {
368                 params.put(key, (String) value);
369             } else if (value instanceof BigDecimal) {
370                 String s = ((BigDecimal) value).toPlainString();
371                 params.put(key, s);
372             } else {
373                 logger.warn("not a string or big decimal value key '{}' value '{}' {}", key, value,
374                         value.getClass().getName());
375             }
376         }
377
378         String feature = channelUID.getId().toLowerCase();
379         String productKey = config.getProductKey();
380         if (productKey.equals(HIDDEN_DOOR_SENSOR_PRODUCT_KEY)) {
381             if (feature.equalsIgnoreCase(InsteonBindingConstants.BATTERY_LEVEL)) {
382                 params.put(FIELD, FIELD_BATTERY_LEVEL);
383                 feature = DATA;
384             } else if (feature.equalsIgnoreCase(InsteonBindingConstants.BATTERY_WATERMARK_LEVEL)) {
385                 params.put(FIELD, FIELD_BATTERY_WATERMARK_LEVEL);
386                 feature = DATA;
387             }
388         } else if (productKey.equals(MOTION_SENSOR_PRODUCT_KEY)) {
389             if (feature.equalsIgnoreCase(InsteonBindingConstants.BATTERY_LEVEL)) {
390                 params.put(FIELD, FIELD_BATTERY_LEVEL);
391                 feature = DATA;
392             } else if (feature.equalsIgnoreCase(InsteonBindingConstants.LIGHT_LEVEL)) {
393                 params.put(FIELD, FIELD_LIGHT_LEVEL);
394                 feature = DATA;
395             }
396         } else if (productKey.equals(MOTION_SENSOR_II_PRODUCT_KEY)) {
397             if (feature.equalsIgnoreCase(InsteonBindingConstants.BATTERY_LEVEL)) {
398                 params.put(FIELD, FIELD_BATTERY_LEVEL);
399                 feature = DATA;
400             } else if (feature.equalsIgnoreCase(InsteonBindingConstants.BATTERY_PERCENT)) {
401                 params.put(FIELD, FIELD_BATTERY_PERCENTAGE);
402                 feature = DATA;
403             } else if (feature.equalsIgnoreCase(InsteonBindingConstants.LIGHT_LEVEL)) {
404                 params.put(FIELD, FIELD_LIGHT_LEVEL);
405                 feature = DATA;
406             } else if (feature.equalsIgnoreCase(InsteonBindingConstants.TEMPERATURE_LEVEL)) {
407                 params.put(FIELD, FIELD_TEMPERATURE_LEVEL);
408                 feature = DATA;
409             }
410         } else if (productKey.equals(PLM_PRODUCT_KEY)) {
411             String parts[] = feature.split("#");
412             if (parts.length == 2 && parts[0].equalsIgnoreCase(InsteonBindingConstants.BROADCAST_ON_OFF)
413                     && parts[1].matches("^\\d+$")) {
414                 params.put(GROUP, parts[1]);
415                 feature = BROADCAST_ON_OFF;
416             }
417         } else if (productKey.equals(POWER_METER_PRODUCT_KEY)) {
418             if (feature.equalsIgnoreCase(InsteonBindingConstants.KWH)) {
419                 params.put(FIELD, FIELD_KWH);
420             } else if (feature.equalsIgnoreCase(InsteonBindingConstants.WATTS)) {
421                 params.put(FIELD, FIELD_WATTS);
422             } else if (feature.equalsIgnoreCase(InsteonBindingConstants.RESET)) {
423                 params.put(CMD, CMD_RESET);
424             } else if (feature.equalsIgnoreCase(InsteonBindingConstants.UPDATE)) {
425                 params.put(CMD, CMD_UPDATE);
426             }
427
428             feature = METER;
429         }
430
431         InsteonChannelConfiguration bindingConfig = new InsteonChannelConfiguration(channelUID, feature,
432                 new InsteonAddress(config.getAddress()), productKey, params);
433         getInsteonBinding().addFeatureListener(bindingConfig);
434
435         StringBuilder builder = new StringBuilder(channelUID.getAsString());
436         builder.append(" feature = ");
437         builder.append(feature);
438         builder.append(" parameters = ");
439         builder.append(params);
440         String msg = builder.toString();
441         logger.debug("{}", msg);
442
443         getInsteonNetworkHandler().linked(channelUID, msg);
444     }
445
446     @Override
447     public void channelUnlinked(ChannelUID channelUID) {
448         getInsteonBinding().removeFeatureListener(channelUID);
449         getInsteonNetworkHandler().unlinked(channelUID);
450
451         logger.debug("channel {} unlinked ", channelUID.getAsString());
452     }
453
454     private @Nullable InsteonNetworkHandler getInsteonNetworkHandler() {
455         return (InsteonNetworkHandler) getBridge().getHandler();
456     }
457
458     private @Nullable InsteonBinding getInsteonBinding() {
459         return getInsteonNetworkHandler().getInsteonBinding();
460     }
461 }