2 * Copyright (c) 2010-2023 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.io.imperihome.internal.processor;
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;
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;
65 * Processor of openHAB Items. Parses ISS tags and creates and registers {@link AbstractDevice} implementations where
68 * @author Pepijn de Geus - Initial contribution
70 public class ItemProcessor implements ItemRegistryChangeListener {
72 private static final String PREFIX_ISS = "iss:";
74 private final Logger logger = LoggerFactory.getLogger(ItemProcessor.class);
76 private final ItemRegistry itemRegistry;
77 private final DeviceRegistry deviceRegistry;
78 private final ActionRegistry actionRegistry;
79 private final ImperiHomeConfig config;
81 public ItemProcessor(ItemRegistry itemRegistry, DeviceRegistry deviceRegistry, ActionRegistry actionRegistry,
82 ImperiHomeConfig config) {
83 this.itemRegistry = itemRegistry;
84 this.deviceRegistry = deviceRegistry;
85 this.actionRegistry = actionRegistry;
88 allItemsChanged(Collections.emptyList());
89 itemRegistry.addRegistryChangeListener(this);
92 public void destroy() {
93 itemRegistry.removeRegistryChangeListener(this);
95 // Destroy all Devices (unregisters state listeners)
96 synchronized (deviceRegistry) {
97 for (AbstractDevice device : deviceRegistry) {
100 deviceRegistry.clear();
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);
109 DeviceType deviceType = getDeviceType(item, issTags);
110 if (deviceType == null) {
111 logger.warn("Unrecognized device type for item: {}", item);
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);
119 setIcon(device, issTags);
120 setDeviceRoom(device, issTags);
121 setDeviceLinks(device, item, issTags);
122 setMapping(device, item, issTags);
123 setUnit(device, issTags);
125 device.processCustomTags(issTags);
128 logger.debug("Setting initial state of {} to {}", device, item.getState());
129 device.stateUpdated(item, item.getState());
131 logger.debug("Item parsed to device: {}", device);
132 synchronized (deviceRegistry) {
133 deviceRegistry.add(device);
139 private void setIcon(AbstractDevice device, Map<TagType, List<String>> issTags) {
140 if (!issTags.containsKey(TagType.ICON)) {
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");
151 icon = rootUrl + "icon/" + icon;
154 device.addParam(new DeviceParam(ParamType.DEFAULT_ICON, icon));
157 private AbstractDevice getDeviceInstance(DeviceType deviceType, Item item) {
158 switch (deviceType) {
160 return new SwitchDevice(item);
162 return new DimmerDevice(item);
164 return new RgbLightDevice(item);
166 return new TemperatureDevice(item);
168 return new TempHygroDevice(item);
170 return new LuminosityDevice(item);
172 return new HygrometryDevice(item);
174 return new Co2SensorDevice(item);
176 return new ElectricityDevice(item);
178 return new SceneDevice(item);
180 return new MultiSwitchDevice(item);
182 return new GenericSensorDevice(item);
184 return new PressureDevice(item);
186 return new UvDevice(item);
188 return new NoiseDevice(item);
190 return new RainDevice(item);
192 return new WindDevice(item);
194 return new LockDevice(item);
196 return new ShutterDevice(item);
198 return new ThermostatDevice(item);
204 return new TrippableDevice(deviceType, item);
209 throw new IllegalArgumentException("Unknown device type: " + deviceType);
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);
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('['));
226 return item.getName();
229 private boolean isInverted(Map<TagType, List<String>> issTags) {
230 return issTags.containsKey(TagType.INVERT) && BooleanUtils.toBoolean(issTags.get(TagType.INVERT).get(0));
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);
239 device.setRoom(DigestUtil.sha1(roomName));
240 device.setRoomName(roomName);
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);
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());
254 logger.error("Item has incorrect link format (should be 'iss:link:<type>:<item>'): {}", item);
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);
269 * Parses a mapping tag, if it exists. Format: "iss:mapping:1=Foo,2=Bar,3=Foobar".
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);
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);
282 mapping.put(keyVal[0].trim(), keyVal[1].trim());
285 device.setMapping(mapping);
290 * Parses the unit tag, if it exists. Format: "iss:unit:°C".
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);
299 ((AbstractNumericValueDevice) device).setUnit(issTags.get(TagType.UNIT).get(0));
304 * Determines the Device type for the given Item. Uses the 'type' tag first, tries to auto-detect the type if no
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));
312 List<Class<? extends State>> acceptedDataTypes = item.getAcceptedDataTypes();
313 String name = item.getName().toLowerCase();
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;
325 return DeviceType.GENERIC_SENSOR;
329 if (acceptedDataTypes.contains(HSBType.class)) {
330 return DeviceType.RGB_LIGHT;
333 if (acceptedDataTypes.contains(OpenClosedType.class)) {
334 return DeviceType.DOOR;
336 if (acceptedDataTypes.contains(OnOffType.class)) {
337 return DeviceType.SWITCH;
343 private Map<TagType, List<String>> getIssTags(Item item) {
344 Map<TagType, List<String>> tags = new EnumMap<>(TagType.class);
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());
358 tags.get(tagType).add(tagValue);
369 * Removes the given item for the device list.
371 * @param item Item to remove.
373 private void removeItem(Item item) {
374 removeItem(item.getName());
378 * Removes the given item for the device list.
380 * @param itemName Name of the Item to remove.
382 private void removeItem(String itemName) {
383 String deviceId = getDeviceId(itemName);
385 AbstractDevice device;
386 synchronized (deviceRegistry) {
387 device = deviceRegistry.remove(deviceId);
390 if (device != null) {
391 logger.debug("Removing Device from ISS registry for Item: {}", itemName);
397 * Generates an unique device ID for the given item.
399 * @param item Item to get device ID for.
402 public static String getDeviceId(Item item) {
403 return getDeviceId(item.getName());
407 * Generates an unique device ID for the given item name.
409 * @param itemName Item name.
412 public static String getDeviceId(String itemName) {
413 return DigestUtil.sha1(itemName);
417 public void added(Item item) {
418 logger.debug("Processing item added event");
423 public void removed(Item item) {
424 logger.debug("Processing item removed event");
429 public void updated(Item oldItem, Item newItem) {
430 logger.debug("Processing item updated event");
436 public void allItemsChanged(Collection<String> oldItems) {
437 synchronized (deviceRegistry) {
438 logger.debug("Processing allItemsChanged event");
440 for (String oldItem : oldItems) {
444 if (deviceRegistry.hasDevices()) {
445 logger.warn("There are still Devices left after processing all Items from allItemsChanged(): {}",
446 deviceRegistry.getDevices());
447 deviceRegistry.clear();
450 for (Item item : itemRegistry.getItems()) {