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