]> git.basschouten.com Git - openhab-addons.git/blob
04b352913fd8deb37d70f9032f976f778d34b0f6
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2021 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.io.imperihome.internal.processor;
14
15 import java.util.Collection;
16 import java.util.Collections;
17 import java.util.EnumMap;
18 import java.util.HashMap;
19 import java.util.LinkedList;
20 import java.util.List;
21 import java.util.Map;
22
23 import org.apache.commons.lang.BooleanUtils;
24 import org.apache.commons.lang.StringUtils;
25 import org.openhab.core.items.Item;
26 import org.openhab.core.items.ItemRegistry;
27 import org.openhab.core.items.ItemRegistryChangeListener;
28 import org.openhab.core.library.types.DecimalType;
29 import org.openhab.core.library.types.HSBType;
30 import org.openhab.core.library.types.OnOffType;
31 import org.openhab.core.library.types.OpenClosedType;
32 import org.openhab.core.types.State;
33 import org.openhab.io.imperihome.internal.ImperiHomeConfig;
34 import org.openhab.io.imperihome.internal.action.ActionRegistry;
35 import org.openhab.io.imperihome.internal.model.device.AbstractDevice;
36 import org.openhab.io.imperihome.internal.model.device.AbstractNumericValueDevice;
37 import org.openhab.io.imperihome.internal.model.device.Co2SensorDevice;
38 import org.openhab.io.imperihome.internal.model.device.DeviceType;
39 import org.openhab.io.imperihome.internal.model.device.DimmerDevice;
40 import org.openhab.io.imperihome.internal.model.device.ElectricityDevice;
41 import org.openhab.io.imperihome.internal.model.device.GenericSensorDevice;
42 import org.openhab.io.imperihome.internal.model.device.HygrometryDevice;
43 import org.openhab.io.imperihome.internal.model.device.LockDevice;
44 import org.openhab.io.imperihome.internal.model.device.LuminosityDevice;
45 import org.openhab.io.imperihome.internal.model.device.MultiSwitchDevice;
46 import org.openhab.io.imperihome.internal.model.device.NoiseDevice;
47 import org.openhab.io.imperihome.internal.model.device.PressureDevice;
48 import org.openhab.io.imperihome.internal.model.device.RainDevice;
49 import org.openhab.io.imperihome.internal.model.device.RgbLightDevice;
50 import org.openhab.io.imperihome.internal.model.device.SceneDevice;
51 import org.openhab.io.imperihome.internal.model.device.ShutterDevice;
52 import org.openhab.io.imperihome.internal.model.device.SwitchDevice;
53 import org.openhab.io.imperihome.internal.model.device.TempHygroDevice;
54 import org.openhab.io.imperihome.internal.model.device.TemperatureDevice;
55 import org.openhab.io.imperihome.internal.model.device.ThermostatDevice;
56 import org.openhab.io.imperihome.internal.model.device.TrippableDevice;
57 import org.openhab.io.imperihome.internal.model.device.UvDevice;
58 import org.openhab.io.imperihome.internal.model.device.WindDevice;
59 import org.openhab.io.imperihome.internal.model.param.DeviceParam;
60 import org.openhab.io.imperihome.internal.model.param.ParamType;
61 import org.openhab.io.imperihome.internal.util.DigestUtil;
62 import org.slf4j.Logger;
63 import org.slf4j.LoggerFactory;
64
65 /**
66  * Processor of openHAB Items. Parses ISS tags and creates and registers {@link AbstractDevice} implementations where
67  * applicable.
68  *
69  * @author Pepijn de Geus - Initial contribution
70  */
71 public class ItemProcessor implements ItemRegistryChangeListener {
72
73     private static final String PREFIX_ISS = "iss:";
74
75     private final Logger logger = LoggerFactory.getLogger(ItemProcessor.class);
76
77     private final ItemRegistry itemRegistry;
78     private final DeviceRegistry deviceRegistry;
79     private final ActionRegistry actionRegistry;
80     private final ImperiHomeConfig config;
81
82     public ItemProcessor(ItemRegistry itemRegistry, DeviceRegistry deviceRegistry, ActionRegistry actionRegistry,
83             ImperiHomeConfig config) {
84         this.itemRegistry = itemRegistry;
85         this.deviceRegistry = deviceRegistry;
86         this.actionRegistry = actionRegistry;
87         this.config = config;
88
89         allItemsChanged(Collections.emptyList());
90         itemRegistry.addRegistryChangeListener(this);
91     }
92
93     public void destroy() {
94         itemRegistry.removeRegistryChangeListener(this);
95
96         // Destroy all Devices (unregisters state listeners)
97         synchronized (deviceRegistry) {
98             for (AbstractDevice device : deviceRegistry) {
99                 device.destroy();
100             }
101             deviceRegistry.clear();
102         }
103     }
104
105     private void parseItem(Item item) {
106         Map<TagType, List<String>> issTags = getIssTags(item);
107         if (!issTags.isEmpty()) {
108             logger.debug("Found item {} with ISS tags: {}", item, issTags);
109
110             DeviceType deviceType = getDeviceType(item, issTags);
111             if (deviceType == null) {
112                 logger.warn("Unrecognized device type for item: {}", item);
113             } else {
114                 AbstractDevice device = getDeviceInstance(deviceType, item);
115                 device.setId(getDeviceId(item));
116                 device.setName(getLabel(item, issTags));
117                 device.setInverted(isInverted(issTags));
118                 device.setActionRegistry(actionRegistry);
119
120                 setIcon(device, issTags);
121                 setDeviceRoom(device, issTags);
122                 setDeviceLinks(device, item, issTags);
123                 setMapping(device, item, issTags);
124                 setUnit(device, issTags);
125
126                 device.processCustomTags(issTags);
127
128                 // Set initial state
129                 logger.debug("Setting initial state of {} to {}", device, item.getState());
130                 device.stateUpdated(item, item.getState());
131
132                 logger.debug("Item parsed to device: {}", device);
133                 synchronized (deviceRegistry) {
134                     deviceRegistry.add(device);
135                 }
136             }
137         }
138     }
139
140     private void setIcon(AbstractDevice device, Map<TagType, List<String>> issTags) {
141         if (!issTags.containsKey(TagType.ICON)) {
142             return;
143         }
144
145         String icon = issTags.get(TagType.ICON).get(0);
146         if (!icon.toLowerCase().startsWith("http")) {
147             if (StringUtils.isEmpty(config.getRootUrl())) {
148                 logger.error("Can't set icon; 'openhab.rootUrl' not set in configuration");
149                 return;
150             }
151             icon = config.getRootUrl() + "icon/" + icon;
152         }
153
154         device.addParam(new DeviceParam(ParamType.DEFAULT_ICON, icon));
155     }
156
157     private AbstractDevice getDeviceInstance(DeviceType deviceType, Item item) {
158         switch (deviceType) {
159             case SWITCH:
160                 return new SwitchDevice(item);
161             case DIMMER:
162                 return new DimmerDevice(item);
163             case RGB_LIGHT:
164                 return new RgbLightDevice(item);
165             case TEMPERATURE:
166                 return new TemperatureDevice(item);
167             case TEMP_HYGRO:
168                 return new TempHygroDevice(item);
169             case LUMINOSITY:
170                 return new LuminosityDevice(item);
171             case HYGROMETRY:
172                 return new HygrometryDevice(item);
173             case CO2:
174                 return new Co2SensorDevice(item);
175             case ELECTRICITY:
176                 return new ElectricityDevice(item);
177             case SCENE:
178                 return new SceneDevice(item);
179             case MULTI_SWITCH:
180                 return new MultiSwitchDevice(item);
181             case GENERIC_SENSOR:
182                 return new GenericSensorDevice(item);
183             case PRESSURE:
184                 return new PressureDevice(item);
185             case UV:
186                 return new UvDevice(item);
187             case NOISE:
188                 return new NoiseDevice(item);
189             case RAIN:
190                 return new RainDevice(item);
191             case WIND:
192                 return new WindDevice(item);
193             case LOCK:
194                 return new LockDevice(item);
195             case SHUTTER:
196                 return new ShutterDevice(item);
197             case THERMOSTAT:
198                 return new ThermostatDevice(item);
199             case CO2_ALERT:
200             case SMOKE:
201             case DOOR:
202             case MOTION:
203             case FLOOD:
204                 return new TrippableDevice(deviceType, item);
205             default:
206                 break;
207         }
208
209         throw new IllegalArgumentException("Unknown device type: " + deviceType);
210     }
211
212     private String getLabel(Item item, Map<TagType, List<String>> issTags) {
213         if (issTags.containsKey(TagType.LABEL)) {
214             return issTags.get(TagType.LABEL).get(0);
215         }
216
217         if (StringUtils.isNotBlank(item.getLabel())) {
218             String label = item.getLabel().trim();
219             if (label.matches("\\[.*\\]$")) {
220                 label = label.substring(0, label.indexOf('['));
221             }
222             return label;
223         }
224
225         return item.getName();
226     }
227
228     private boolean isInverted(Map<TagType, List<String>> issTags) {
229         return issTags.containsKey(TagType.INVERT) && BooleanUtils.toBoolean(issTags.get(TagType.INVERT).get(0));
230     }
231
232     private void setDeviceRoom(AbstractDevice device, Map<TagType, List<String>> issTags) {
233         String roomName = "No room";
234         if (issTags.containsKey(TagType.ROOM)) {
235             roomName = issTags.get(TagType.ROOM).get(0);
236         }
237
238         device.setRoom(DigestUtil.sha1(roomName));
239         device.setRoomName(roomName);
240     }
241
242     private void setDeviceLinks(AbstractDevice device, Item item, Map<TagType, List<String>> issTags) {
243         if (issTags.containsKey(TagType.LINK)) {
244             // Pass device registry to device for linked device lookup
245             device.setDeviceRegistry(deviceRegistry);
246
247             // Parse link tags
248             for (String link : issTags.get(TagType.LINK)) {
249                 String[] parts = link.split(":");
250                 if (parts.length == 2) {
251                     device.addLink(parts[0].toLowerCase().trim(), parts[1].trim());
252                 } else {
253                     logger.error("Item has incorrect link format (should be 'iss:link:<type>:<item>'): {}", item);
254                 }
255             }
256
257             // Check required links
258             for (String requiredLink : device.getType().getRequiredLinks()) {
259                 if (!device.getLinks().containsKey(requiredLink)) {
260                     logger.error("Item doesn't contain required link {} for {}: {}", requiredLink,
261                             device.getType().getApiString(), item);
262                 }
263             }
264         }
265     }
266
267     /**
268      * Parses a mapping tag, if it exists. Format: "iss:mapping:1=Foo,2=Bar,3=Foobar".
269      */
270     private void setMapping(AbstractDevice device, Item item, Map<TagType, List<String>> issTags) {
271         if (issTags.containsKey(TagType.MAPPING)) {
272             String mapItems = issTags.get(TagType.MAPPING).get(0);
273
274             Map<String, String> mapping = new HashMap<>();
275             for (String mapItem : mapItems.split(",")) {
276                 String[] keyVal = mapItem.split("=", 2);
277                 if (keyVal.length != 2) {
278                     logger.error("Invalid mapping syntax for Item {}", item);
279                     return;
280                 }
281                 mapping.put(keyVal[0].trim(), keyVal[1].trim());
282             }
283
284             device.setMapping(mapping);
285         }
286     }
287
288     /**
289      * Parses the unit tag, if it exists. Format: "iss:unit:°C".
290      */
291     private void setUnit(AbstractDevice device, Map<TagType, List<String>> issTags) {
292         if (issTags.containsKey(TagType.UNIT)) {
293             if (!(device instanceof AbstractNumericValueDevice)) {
294                 logger.warn("Unit tag is not supported for device {}", device);
295                 return;
296             }
297
298             ((AbstractNumericValueDevice) device).setUnit(issTags.get(TagType.UNIT).get(0));
299         }
300     }
301
302     /**
303      * Determines the Device type for the given Item. Uses the 'type' tag first, tries to auto-detect the type if no
304      * such tag exists.
305      */
306     private DeviceType getDeviceType(Item item, Map<TagType, List<String>> issTags) {
307         if (issTags.containsKey(TagType.TYPE)) {
308             return DeviceType.forApiString(issTags.get(TagType.TYPE).get(0));
309         }
310
311         List<Class<? extends State>> acceptedDataTypes = item.getAcceptedDataTypes();
312         String name = item.getName().toLowerCase();
313
314         if (acceptedDataTypes.contains(DecimalType.class)) {
315             if (name.contains("tempe")) {
316                 return DeviceType.TEMPERATURE;
317             } else if (name.contains("lumi")) {
318                 return DeviceType.LUMINOSITY;
319             } else if (name.contains("hygro")) {
320                 return DeviceType.HYGROMETRY;
321             } else if (name.contains("wind")) {
322                 return DeviceType.WIND;
323             } else {
324                 return DeviceType.GENERIC_SENSOR;
325             }
326         }
327
328         if (acceptedDataTypes.contains(HSBType.class)) {
329             return DeviceType.RGB_LIGHT;
330         }
331
332         if (acceptedDataTypes.contains(OpenClosedType.class)) {
333             return DeviceType.DOOR;
334         }
335         if (acceptedDataTypes.contains(OnOffType.class)) {
336             return DeviceType.SWITCH;
337         }
338
339         return null;
340     }
341
342     private Map<TagType, List<String>> getIssTags(Item item) {
343         Map<TagType, List<String>> tags = new EnumMap<>(TagType.class);
344
345         for (String tag : item.getTags()) {
346             if (tag.startsWith(PREFIX_ISS)) {
347                 String issTag = tag.substring(PREFIX_ISS.length());
348                 for (TagType tagType : TagType.values()) {
349                     if (issTag.startsWith(tagType.getPrefix() + ':')) {
350                         String tagValue = issTag.substring(tagType.getPrefix().length() + 1);
351                         if (!tags.containsKey(tagType)) {
352                             tags.put(tagType, new LinkedList<>());
353                         } else if (!tagType.isMultiValue()) {
354                             logger.error("Found multiple values for tag {} - only first value is used",
355                                     tagType.getPrefix());
356                         }
357                         tags.get(tagType).add(tagValue);
358                         break;
359                     }
360                 }
361             }
362         }
363
364         return tags;
365     }
366
367     /**
368      * Removes the given item for the device list.
369      *
370      * @param item Item to remove.
371      */
372     private void removeItem(Item item) {
373         removeItem(item.getName());
374     }
375
376     /**
377      * Removes the given item for the device list.
378      *
379      * @param itemName Name of the Item to remove.
380      */
381     private void removeItem(String itemName) {
382         String deviceId = getDeviceId(itemName);
383
384         AbstractDevice device;
385         synchronized (deviceRegistry) {
386             device = deviceRegistry.remove(deviceId);
387         }
388
389         if (device != null) {
390             logger.debug("Removing Device from ISS registry for Item: {}", itemName);
391             device.destroy();
392         }
393     }
394
395     /**
396      * Generates an unique device ID for the given item.
397      *
398      * @param item Item to get device ID for.
399      * @return Device ID.
400      */
401     public static String getDeviceId(Item item) {
402         return getDeviceId(item.getName());
403     }
404
405     /**
406      * Generates an unique device ID for the given item name.
407      *
408      * @param itemName Item name.
409      * @return Device ID.
410      */
411     public static String getDeviceId(String itemName) {
412         return DigestUtil.sha1(itemName);
413     }
414
415     @Override
416     public void added(Item item) {
417         logger.debug("Processing item added event");
418         parseItem(item);
419     }
420
421     @Override
422     public void removed(Item item) {
423         logger.debug("Processing item removed event");
424         removeItem(item);
425     }
426
427     @Override
428     public void updated(Item oldItem, Item newItem) {
429         logger.debug("Processing item updated event");
430         removeItem(oldItem);
431         parseItem(newItem);
432     }
433
434     @Override
435     public void allItemsChanged(Collection<String> oldItems) {
436         synchronized (deviceRegistry) {
437             logger.debug("Processing allItemsChanged event");
438
439             for (String oldItem : oldItems) {
440                 removeItem(oldItem);
441             }
442
443             if (deviceRegistry.hasDevices()) {
444                 logger.warn("There are still Devices left after processing all Items from allItemsChanged(): {}",
445                         deviceRegistry.getDevices());
446                 deviceRegistry.clear();
447             }
448
449             for (Item item : itemRegistry.getItems()) {
450                 parseItem(item);
451             }
452         }
453     }
454 }