]> git.basschouten.com Git - openhab-addons.git/commitdiff
[systeminfo] dynamic channels (#13562)
authorMark Herwege <mherwege@users.noreply.github.com>
Fri, 4 Nov 2022 12:28:27 +0000 (13:28 +0100)
committerGitHub <noreply@github.com>
Fri, 4 Nov 2022 12:28:27 +0000 (13:28 +0100)
* Dynamic channels
* Status messages i8n
* Format fix
* Cache process load values
* Restore channel configs
* Fix test
* Stabilize tests
* Fix CpuLoad1-5-15 update
* Fix test bndrun
* String equals cleanup
* Fix potential null pointer in test

Signed-off-by: Mark Herwege <mark.herwege@telenet.be>
12 files changed:
bundles/org.openhab.binding.systeminfo/README.md
bundles/org.openhab.binding.systeminfo/src/main/java/org/openhab/binding/systeminfo/internal/SysteminfoBindingConstants.java
bundles/org.openhab.binding.systeminfo/src/main/java/org/openhab/binding/systeminfo/internal/SysteminfoHandlerFactory.java
bundles/org.openhab.binding.systeminfo/src/main/java/org/openhab/binding/systeminfo/internal/SysteminfoThingTypeProvider.java [new file with mode: 0644]
bundles/org.openhab.binding.systeminfo/src/main/java/org/openhab/binding/systeminfo/internal/handler/SysteminfoHandler.java
bundles/org.openhab.binding.systeminfo/src/main/java/org/openhab/binding/systeminfo/internal/model/OSHISysteminfo.java
bundles/org.openhab.binding.systeminfo/src/main/java/org/openhab/binding/systeminfo/internal/model/SysteminfoInterface.java
bundles/org.openhab.binding.systeminfo/src/main/resources/OH-INF/i18n/systeminfo.properties
bundles/org.openhab.binding.systeminfo/src/main/resources/OH-INF/thing/channels.xml
bundles/org.openhab.binding.systeminfo/src/main/resources/OH-INF/thing/computer.xml
itests/org.openhab.binding.systeminfo.tests/itest.bndrun
itests/org.openhab.binding.systeminfo.tests/src/main/java/org/openhab/binding/systeminfo/test/SysteminfoOSGiTest.java

index 197482b835e74a0992679ed64359f39bdaaaa034..500c631315753acc62c565fdba6b2e17936efab8 100644 (file)
@@ -36,9 +36,6 @@ The discovery service implementation tries to resolve the computer name.
 If the resolving process fails, the computer name is set to "Unknown".
 In both cases it creates a Discovery Result with thing type  **computer**.
 
-It will be possible to implement creation of dynamic channels (e.g. the binding will scan how many storage devices are present and create channel groups for them).
-At the moment this is not supported.
-
 ## Thing configuration
 
 The configuration of the Thing gives the user the possibility to update channels at different intervals.
@@ -82,23 +79,34 @@ In the list below, you can find, how are channel group and channels id`s related
   * **channel** `cpuTemp, cpuVoltage, fanSpeed`
 *   **group** `network` (deviceIndex)
   * **channel** `ip, mac, networkDisplayName, networkName, packetsSent, packetsReceived, dataSent, dataReceived`
+*   **group** `currentProcess`
+  * **channel** `load, used, name, threads, path`
 *   **group** `process` (pid)
   * **channel** `load, used, name, threads, path`
 
 The groups marked with "(deviceIndex)" may have device index attached to the Channel Group.
 
 -   channel ::= channel_group & (deviceIndex) & # channel_id
--   deviceIndex ::= number > 0
+-   deviceIndex ::= number >= 0
 -   (e.g. *storage1#available*)
 
+The `fanSpeed` channel in the `sensors` group may have a device index attached to the Channel.
+
+-   channel ::= channel_group & # channel_id & (deviceIndex)
+-   deviceIndex ::= number >= 0
+
+Channels or channel groups without a trailing index will show the data for the first device (index 0) if multiple exist.
+If only one device for a group exists, no channels or channel groups with indexes will be created.
+
 The group `process` is using a configuration parameter "pid" instead of "deviceIndex".
 This makes it possible to change the tracked process at runtime.
 
+The group `currentProcess` has the same channels as the `process` group without the "pid" configuration parameter.
+The PID is dynamically set to the PID of the process running openHAB.
+
 The binding uses this index to get information about a specific device from a list of devices (e.g on a single computer several local disks could be installed with names C:\, D:\, E:\ - the first will have deviceIndex=0, the second deviceIndex=1 etc).
 If device with this index is not existing, the binding will display an error message on the console.
 
-Unfortunately this feature can't be used at the moment without manually adding these new channel groups to the thing description (located in OH-INF/thing/computer.xml).
-
 The table shows more detailed information about each Channel type.
 The binding introduces the following channels:
 
@@ -244,6 +252,13 @@ Number Sensor_CPUTemp             "CPU Temperature"     <temperature>    { chann
 Number Sensor_CPUVoltage          "CPU Voltage"         <energy>         { channel="systeminfo:computer:work:sensors#cpuVoltage" }
 Number Sensor_FanSpeed            "Fan speed"           <fan>            { channel="systeminfo:computer:work:sensors#fanSpeed" }
 
+/* Current process information*/
+Number Current_process_load       "Load"                <none>           { channel="systeminfo:computer:work:currentProcess#load" }
+Number Current_process_used       "Used"                <none>           { channel="systeminfo:computer:work:currentProcess#used" }
+String Current_process_name       "Name"                <none>           { channel="systeminfo:computer:work:currentProcess#name" }
+Number Current_process_threads    "Threads"             <none>           { channel="systeminfo:computer:work:currentProcess#threads" }
+String Current_process_path       "Path"                <none>           { channel="systeminfo:computer:work:currentProcess#path" }
+
 /* Process information*/
 Number Process_load               "Load"                <none>           { channel="systeminfo:computer:work:process#load" }
 Number Process_used               "Used"                <none>           { channel="systeminfo:computer:work:process#used" }
@@ -313,6 +328,13 @@ sitemap systeminfo label="Systeminfo" {
         Default item=Sensor_CPUVoltage
         Default item=Sensor_FanSpeed
     }
+    Frame label="Current Process Information" {
+        Default item=Current_process_load
+        Default item=Current_process_used
+        Default item=Current_process_name
+        Default item=Current_process_threads
+        Default item=Current_process_path
+    }
     Frame label="Process Information" {
         Default item=Process_load
         Default item=Process_used
index d88327fe3afd08498d3b534c58bd375fe256bdfe..94127162524272463bc4b18872395ab40c40d49b 100644 (file)
@@ -20,13 +20,15 @@ import org.openhab.core.thing.ThingTypeUID;
  * used across the whole binding.
  *
  * @author Svilen Valkanov - Initial contribution
+ * @author Mark Herwege - Add dynamic creation of extra channels
  */
 @NonNullByDefault
 public class SysteminfoBindingConstants {
 
     public static final String BINDING_ID = "systeminfo";
 
-    public static final ThingTypeUID THING_TYPE_COMPUTER = new ThingTypeUID(BINDING_ID, "computer");
+    public static final String THING_TYPE_COMPUTER_ID = "computer";
+    public static final ThingTypeUID THING_TYPE_COMPUTER = new ThingTypeUID(BINDING_ID, THING_TYPE_COMPUTER_ID);
 
     // Thing properties
     /**
@@ -56,6 +58,16 @@ public class SysteminfoBindingConstants {
 
     // List of all Channel IDs
 
+    /**
+     * Name of the channel group type for memory information
+     */
+    public static final String CHANNEL_GROUP_TYPE_MEMORY = "memoryGroup";
+
+    /**
+     * Name of the channel group for memory information
+     */
+    public static final String CHANNEL_GROUP_MEMORY = "memory";
+
     /**
      * Size of the available memory
      */
@@ -91,6 +103,16 @@ public class SysteminfoBindingConstants {
      */
     public static final String CHANNEL_MEMORY_HEAP_AVAILABLE = "memory#availableHeap";
 
+    /**
+     * Name of the channel group type for swap information
+     */
+    public static final String CHANNEL_GROUP_TYPE_SWAP = "swapGroup";
+
+    /**
+     * Name of the channel group for swap information
+     */
+    public static final String CHANNEL_GROUP_SWAP = "swap";
+
     /**
      * Total size of swap memory
      */
@@ -116,6 +138,16 @@ public class SysteminfoBindingConstants {
      */
     public static final String CHANNEL_SWAP_USED_PERCENT = "swap#usedPercent";
 
+    /**
+     * Name of the channel group type for drive information
+     */
+    public static final String CHANNEL_GROUP_TYPE_DRIVE = "driveGroup";
+
+    /**
+     * Name of the channel group for drive information
+     */
+    public static final String CHANNEL_GROUP_DRIVE = "drive";
+
     /**
      * Physical storage drive name
      */
@@ -131,6 +163,16 @@ public class SysteminfoBindingConstants {
      */
     public static final String CHANNEL_DRIVE_SERIAL = "drive#serial";
 
+    /**
+     * Name of the channel group type for storage information
+     */
+    public static final String CHANNEL_GROUP_TYPE_STORAGE = "storageGroup";
+
+    /**
+     * Name of the channel group for storage information
+     */
+    public static final String CHANNEL_GROUP_STORAGE = "storage";
+
     /**
      * Name of the logical volume storage
      */
@@ -171,6 +213,16 @@ public class SysteminfoBindingConstants {
      */
     public static final String CHANNEL_STORAGE_USED_PERCENT = "storage#usedPercent";
 
+    /**
+     * Name of the channel group type for sensors information
+     */
+    public static final String CHANNEL_GROUP_TYPE_SENSORS = "sensorsGroup";
+
+    /**
+     * Name of the channel group for sensors information
+     */
+    public static final String CHANNEL_GROUP_SENSORS = "sensors";
+
     /**
      * Temperature of the CPU measured from the sensors.
      */
@@ -186,6 +238,16 @@ public class SysteminfoBindingConstants {
      */
     public static final String CHANNEL_SENSORS_FAN_SPEED = "sensors#fanSpeed";
 
+    /**
+     * Name of the channel group type for battery information
+     */
+    public static final String CHANNEL_GROUP_TYPE_BATTERY = "batteryGroup";
+
+    /**
+     * Name of the channel group for battery information
+     */
+    public static final String CHANNEL_GROUP_BATTERY = "battery";
+
     /**
      * Name of the battery
      */
@@ -201,6 +263,16 @@ public class SysteminfoBindingConstants {
      */
     public static final String CHANNEL_BATTERY_REMAINING_TIME = "battery#remainingTime";
 
+    /**
+     * Name of the channel group type for CPU information
+     */
+    public static final String CHANNEL_GROUP_TYPE_CPU = "cpuGroup";
+
+    /**
+     * Name of the channel group for CPU information
+     */
+    public static final String CHANNEL_GROUP_CPU = "cpu";
+
     /**
      * Detailed description about the CPU
      */
@@ -241,11 +313,31 @@ public class SysteminfoBindingConstants {
      */
     public static final String CHANNEL_CPU_THREADS = "cpu#threads";
 
+    /**
+     * Name of the channel group type for display information
+     */
+    public static final String CHANNEL_GROUP_TYPE_DISPLAY = "displayGroup";
+
+    /**
+     * Name of the channel group for display information
+     */
+    public static final String CHANNEL_GROUP_DISPLAY = "display";
+
     /**
      * Information about the display device
      */
     public static final String CHANNEL_DISPLAY_INFORMATION = "display#information";
 
+    /**
+     * Name of the channel group type for network information
+     */
+    public static final String CHANNEL_GROUP_TYPE_NETWORK = "networkGroup";
+
+    /**
+     * Name of the channel group for network information
+     */
+    public static final String CHANNEL_GROUP_NETWORK = "network";
+
     /**
      * Host IP address of the network
      */
@@ -286,6 +378,47 @@ public class SysteminfoBindingConstants {
      */
     public static final String CHANNEL_NETWORK_MAC = "network#mac";
 
+    /**
+     * Name of the channel group type for process information
+     */
+    public static final String CHANNEL_GROUP_TYPE_CURRENT_PROCESS = "currentProcessGroup";
+
+    /**
+     * Name of the channel group for process information
+     */
+    public static final String CHANNEL_GROUP_CURRENT_PROCESS = "currentProcess";
+
+    /**
+     * CPU load used from a process
+     */
+
+    public static final String CHANNEL_CURRENT_PROCESS_LOAD = "currentProcess#load";
+
+    /**
+     * Size of memory used from a process in MB
+     */
+    public static final String CHANNEL_CURRENT_PROCESS_MEMORY = "currentProcess#used";
+
+    /**
+     * Name of the process
+     */
+    public static final String CHANNEL_CURRENT_PROCESS_NAME = "currentProcess#name";
+
+    /**
+     * Number of threads, used form the process
+     */
+    public static final String CHANNEL_CURRENT_PROCESS_THREADS = "currentProcess#threads";
+
+    /**
+     * The full path of the process
+     */
+    public static final String CHANNEL_CURRENT_PROCESS_PATH = "currentProcess#path";
+
+    /**
+     * Name of the channel group type for process information
+     */
+    public static final String CHANNEL_GROUP_TYPE_PROCESS = "processGroup";
+
     /**
      * Name of the channel group for process information
      */
index efc75acc58e0a703344d19640073c549a3ecaffd..6b88bff8e2fedc7ce9a61ecccf14a54f401c51b6 100644 (file)
  */
 package org.openhab.binding.systeminfo.internal;
 
-import static org.openhab.binding.systeminfo.internal.SysteminfoBindingConstants.THING_TYPE_COMPUTER;
-
-import java.util.Collections;
-import java.util.Set;
+import static org.openhab.binding.systeminfo.internal.SysteminfoBindingConstants.*;
 
 import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.eclipse.jdt.annotation.Nullable;
@@ -36,28 +33,33 @@ import org.osgi.service.component.annotations.Reference;
  * @author Svilen Valkanov - Initial contribution
  * @author Lyubomir Papazov - Pass systeminfo service to the SysteminfoHandler constructor
  * @author Wouter Born - Add null annotations
+ * @author Mark Herwege - Add dynamic creation of extra channels
  */
 @NonNullByDefault
 @Component(service = ThingHandlerFactory.class, configurationPid = "binding.systeminfo")
 public class SysteminfoHandlerFactory extends BaseThingHandlerFactory {
-
-    private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections.singleton(THING_TYPE_COMPUTER);
-
     private @NonNullByDefault({}) SysteminfoInterface systeminfo;
+    private @NonNullByDefault({}) SysteminfoThingTypeProvider thingTypeProvider;
 
     @Override
     public boolean supportsThingType(ThingTypeUID thingTypeUID) {
-        return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
+        return BINDING_ID.equals(thingTypeUID.getBindingId())
+                && thingTypeUID.getId().startsWith(THING_TYPE_COMPUTER_ID);
     }
 
     @Override
     protected @Nullable ThingHandler createHandler(Thing thing) {
         ThingTypeUID thingTypeUID = thing.getThingTypeUID();
-
-        if (thingTypeUID.equals(THING_TYPE_COMPUTER)) {
-            return new SysteminfoHandler(thing, systeminfo);
+        if (supportsThingType(thingTypeUID)) {
+            String extString = "-" + thing.getUID().getId();
+            ThingTypeUID extThingTypeUID = new ThingTypeUID(BINDING_ID, THING_TYPE_COMPUTER_ID + extString);
+            if (thingTypeProvider.getThingType(extThingTypeUID, null) == null) {
+                thingTypeProvider.createThingType(extThingTypeUID);
+                thingTypeProvider.storeChannelsConfig(thing); // Save the current channels configs, will be restored
+                                                              // after thing type change.
+            }
+            return new SysteminfoHandler(thing, thingTypeProvider, systeminfo);
         }
-
         return null;
     }
 
@@ -69,4 +71,13 @@ public class SysteminfoHandlerFactory extends BaseThingHandlerFactory {
     public void unbindSystemInfo(SysteminfoInterface systeminfo) {
         this.systeminfo = null;
     }
+
+    @Reference
+    public void setSysteminfoThingTypeProvider(SysteminfoThingTypeProvider thingTypeProvider) {
+        this.thingTypeProvider = thingTypeProvider;
+    }
+
+    public void unsetSysteminfoThingTypeProvider(SysteminfoThingTypeProvider thingTypeProvider) {
+        this.thingTypeProvider = null;
+    }
 }
diff --git a/bundles/org.openhab.binding.systeminfo/src/main/java/org/openhab/binding/systeminfo/internal/SysteminfoThingTypeProvider.java b/bundles/org.openhab.binding.systeminfo/src/main/java/org/openhab/binding/systeminfo/internal/SysteminfoThingTypeProvider.java
new file mode 100644 (file)
index 0000000..61cd3aa
--- /dev/null
@@ -0,0 +1,279 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.systeminfo.internal;
+
+import static org.openhab.binding.systeminfo.internal.SysteminfoBindingConstants.*;
+
+import java.net.URI;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.core.config.core.Configuration;
+import org.openhab.core.thing.Channel;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingTypeUID;
+import org.openhab.core.thing.ThingUID;
+import org.openhab.core.thing.binding.BaseThingHandler;
+import org.openhab.core.thing.binding.ThingTypeProvider;
+import org.openhab.core.thing.binding.builder.ChannelBuilder;
+import org.openhab.core.thing.type.ChannelGroupDefinition;
+import org.openhab.core.thing.type.ChannelGroupType;
+import org.openhab.core.thing.type.ChannelGroupTypeRegistry;
+import org.openhab.core.thing.type.ChannelGroupTypeUID;
+import org.openhab.core.thing.type.ChannelType;
+import org.openhab.core.thing.type.ChannelTypeRegistry;
+import org.openhab.core.thing.type.ChannelTypeUID;
+import org.openhab.core.thing.type.ThingType;
+import org.openhab.core.thing.type.ThingTypeBuilder;
+import org.openhab.core.thing.type.ThingTypeRegistry;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Extended channels can be auto discovered and added to newly created groups in the {@link SystemInfoHandler}. The
+ * thing needs to be updated to add the groups. The `SysteminfoThingTypeProvider` OSGi service gives access to the
+ * `ThingTypeRegistry` and serves the updated `ThingType`.
+ *
+ * @author Mark Herwege - Initial contribution
+ *
+ */
+@NonNullByDefault
+@Component(service = { SysteminfoThingTypeProvider.class, ThingTypeProvider.class })
+public class SysteminfoThingTypeProvider implements ThingTypeProvider {
+    private final Logger logger = LoggerFactory.getLogger(SysteminfoThingTypeProvider.class);
+
+    private final ThingTypeRegistry thingTypeRegistry;
+    private final ChannelGroupTypeRegistry channelGroupTypeRegistry;
+    private final ChannelTypeRegistry channelTypeRegistry;
+
+    private final Map<ThingTypeUID, ThingType> thingTypes = new HashMap<>();
+
+    private final Map<ThingUID, Map<String, Configuration>> thingChannelsConfig = new HashMap<>();
+
+    @Activate
+    public SysteminfoThingTypeProvider(@Reference ThingTypeRegistry thingTypeRegistry,
+            @Reference ChannelGroupTypeRegistry channelGroupTypeRegistry,
+            @Reference ChannelTypeRegistry channelTypeRegistry) {
+        super();
+        this.thingTypeRegistry = thingTypeRegistry;
+        this.channelGroupTypeRegistry = channelGroupTypeRegistry;
+        this.channelTypeRegistry = channelTypeRegistry;
+    }
+
+    @Override
+    public Collection<ThingType> getThingTypes(@Nullable Locale locale) {
+        return thingTypes.values();
+    }
+
+    @Override
+    public @Nullable ThingType getThingType(ThingTypeUID thingTypeUID, @Nullable Locale locale) {
+        return thingTypes.get(thingTypeUID);
+    }
+
+    private void setThingType(ThingTypeUID uid, ThingType type) {
+        thingTypes.put(uid, type);
+    }
+
+    /**
+     * Create thing type with the provided typeUID and add it to the thing type registry.
+     *
+     * @param typeUID
+     * @return false if base type UID `systeminfo:computer` cannot be found in the thingTypeRegistry
+     */
+    public boolean createThingType(ThingTypeUID typeUID) {
+        logger.trace("Creating thing type {}", typeUID);
+        return updateThingType(typeUID, getChannelGroupDefinitions(typeUID));
+    }
+
+    /**
+     * Update `ThingType`with `typeUID`, replacing the channel group definitions with `groupDefs`.
+     *
+     * @param typeUID
+     * @param groupDefs
+     * @return false if `typeUID` or its base type UID `systeminfo:computer` cannot be found in the thingTypeRegistry
+     */
+    public boolean updateThingType(ThingTypeUID typeUID, List<ChannelGroupDefinition> groupDefs) {
+        ThingTypeUID baseTypeUID = THING_TYPE_COMPUTER;
+        if (thingTypes.containsKey(typeUID)) {
+            baseTypeUID = typeUID;
+        }
+        ThingType baseType = thingTypeRegistry.getThingType(baseTypeUID);
+        ThingTypeBuilder builder = createThingTypeBuilder(typeUID, baseTypeUID);
+        if (baseType != null && builder != null) {
+            logger.trace("Adding channel group definitions to thing type");
+            ThingType type = builder.withChannelGroupDefinitions(groupDefs).build();
+
+            setThingType(typeUID, type);
+            return true;
+        } else {
+            logger.debug("Error adding channel groups");
+            return false;
+        }
+    }
+
+    /**
+     * Return a {@link ThingTypeBuilder} that can create an exact copy of the `ThingType` with `baseTypeUID`.
+     * Further build steps can be performed on the returned object before recreating the `ThingType` from the builder.
+     *
+     * @param newTypeUID
+     * @param baseTypeUID
+     * @return the ThingTypeBuilder, null if `baseTypeUID` cannot be found in the thingTypeRegistry
+     */
+    private @Nullable ThingTypeBuilder createThingTypeBuilder(ThingTypeUID newTypeUID, ThingTypeUID baseTypeUID) {
+        ThingType type = thingTypeRegistry.getThingType(baseTypeUID);
+
+        if (type == null) {
+            return null;
+        }
+
+        ThingTypeBuilder result = ThingTypeBuilder.instance(newTypeUID, type.getLabel())
+                .withChannelGroupDefinitions(type.getChannelGroupDefinitions())
+                .withChannelDefinitions(type.getChannelDefinitions())
+                .withExtensibleChannelTypeIds(type.getExtensibleChannelTypeIds())
+                .withSupportedBridgeTypeUIDs(type.getSupportedBridgeTypeUIDs()).withProperties(type.getProperties())
+                .isListed(false);
+
+        String representationProperty = type.getRepresentationProperty();
+        if (representationProperty != null) {
+            result = result.withRepresentationProperty(representationProperty);
+        }
+        URI configDescriptionURI = type.getConfigDescriptionURI();
+        if (configDescriptionURI != null) {
+            result = result.withConfigDescriptionURI(configDescriptionURI);
+        }
+        String category = type.getCategory();
+        if (category != null) {
+            result = result.withCategory(category);
+        }
+        String description = type.getDescription();
+        if (description != null) {
+            result = result.withDescription(description);
+        }
+
+        return result;
+    }
+
+    /**
+     * Return List of {@link ChannelGroupDefinition} for `ThingType` with `typeUID`. If the `ThingType` does not exist
+     * in the thingTypeRegistry yet, retrieve list of `ChannelGroupDefinition` for base type systeminfo:computer.
+     *
+     * @param typeUID UID for ThingType
+     * @return list of channel group definitions, empty list if no channel group definitions
+     */
+    public List<ChannelGroupDefinition> getChannelGroupDefinitions(ThingTypeUID typeUID) {
+        ThingType type = thingTypeRegistry.getThingType(typeUID);
+        if (type == null) {
+            type = thingTypeRegistry.getThingType(THING_TYPE_COMPUTER);
+        }
+        if (type != null) {
+            return type.getChannelGroupDefinitions();
+        } else {
+            logger.debug("Cannot retrieve channel group definitions, no base thing type found");
+            return Collections.emptyList();
+        }
+    }
+
+    /**
+     * Create a new channel group definition with index appended to id and label.
+     *
+     * @param channelGroupID id of channel group without index
+     * @param channelGroupTypeID id ChannelGroupType for new channel group definition
+     * @param i index
+     * @return channel group definition, null if provided channelGroupTypeID cannot be found in ChannelGroupTypeRegistry
+     */
+    public @Nullable ChannelGroupDefinition createChannelGroupDefinitionWithIndex(String channelGroupID,
+            String channelGroupTypeID, int i) {
+        ChannelGroupTypeUID channelGroupTypeUID = new ChannelGroupTypeUID(BINDING_ID, channelGroupTypeID);
+        ChannelGroupType channelGroupType = channelGroupTypeRegistry.getChannelGroupType(channelGroupTypeUID);
+        if (channelGroupType == null) {
+            logger.debug("Cannot create channel group definition, group type {} invalid", channelGroupTypeID);
+            return null;
+        }
+        String index = String.valueOf(i);
+        return new ChannelGroupDefinition(channelGroupID + index, channelGroupTypeUID,
+                channelGroupType.getLabel() + " " + index, channelGroupType.getDescription());
+    }
+
+    /**
+     * Create a new channel with index appended to id and label of an existing channel.
+     *
+     * @param thing containing the existing channel
+     * @param channelID id of channel without index
+     * @param i index
+     * @return channel, null if provided channelID does not match a channel, or no type can be retrieved for the
+     *         provided channel
+     */
+    public @Nullable Channel createChannelWithIndex(Thing thing, String channelID, int i) {
+        Channel baseChannel = thing.getChannel(channelID);
+        if (baseChannel == null) {
+            logger.debug("Cannot create channel, ID {} invalid", channelID);
+            return null;
+        }
+        ChannelTypeUID channelTypeUID = baseChannel.getChannelTypeUID();
+        ChannelType channelType = channelTypeRegistry.getChannelType(channelTypeUID);
+        if (channelType == null) {
+            logger.debug("Cannot create channel, type {} invalid",
+                    channelTypeUID != null ? channelTypeUID.getId() : "null");
+            return null;
+        }
+        ThingUID thingUID = thing.getUID();
+        String index = String.valueOf(i);
+        ChannelUID channelUID = new ChannelUID(thingUID, channelID + index);
+        ChannelBuilder builder = ChannelBuilder.create(channelUID).withType(channelTypeUID)
+                .withConfiguration(baseChannel.getConfiguration());
+        builder.withLabel(channelType.getLabel() + " " + index);
+        String description = channelType.getDescription();
+        if (description != null) {
+            builder.withDescription(description);
+        }
+        return builder.build();
+    }
+
+    /**
+     * Store the channel configurations for a thing, to be able to restore them later when the thing handler for the
+     * same thing gets recreated with a new thing type. This is necessary because the
+     * {@link BaseThingHandler#changeThingType()} method reverts channel configurations to their defaults.
+     *
+     * @param thing
+     */
+    public void storeChannelsConfig(Thing thing) {
+        Map<String, Configuration> channelsConfig = thing.getChannels().stream()
+                .collect(Collectors.toMap(c -> c.getUID().getId(), c -> c.getConfiguration()));
+        thingChannelsConfig.put(thing.getUID(), channelsConfig);
+    }
+
+    /**
+     * Restore previous channel configurations of matching channels when the thing handler gets recreated with a new
+     * thing type. Return an empty map if no channel configurations where stored. Before returning previous channel
+     * configurations, clear the store, so they can only be retrieved ones, immediately after a thing type change. See
+     * also {@link #storeChannelsConfig(Thing)}.
+     *
+     * @param UID
+     * @return Map of ChannelId and Configuration for the channel
+     */
+    public Map<String, Configuration> restoreChannelsConfig(ThingUID UID) {
+        Map<String, Configuration> configs = thingChannelsConfig.remove(UID);
+        return configs != null ? configs : Collections.emptyMap();
+    }
+}
index 50be4a14ca5510b97780edf94b12c6802bdfc07b..3b8efb316e6e2506c2e26cd2daa755ef586cb372 100644 (file)
@@ -15,6 +15,8 @@ package org.openhab.binding.systeminfo.internal.handler;
 import static org.openhab.binding.systeminfo.internal.SysteminfoBindingConstants.*;
 
 import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.Collections;
 import java.util.HashSet;
 import java.util.Iterator;
 import java.util.List;
@@ -22,12 +24,17 @@ import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.ScheduledFuture;
 import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
 
 import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.systeminfo.internal.SysteminfoThingTypeProvider;
 import org.openhab.binding.systeminfo.internal.model.DeviceNotFoundException;
 import org.openhab.binding.systeminfo.internal.model.SysteminfoInterface;
+import org.openhab.core.cache.ExpiringCache;
+import org.openhab.core.cache.ExpiringCacheMap;
 import org.openhab.core.config.core.Configuration;
+import org.openhab.core.library.types.DecimalType;
 import org.openhab.core.library.types.PercentType;
 import org.openhab.core.library.types.QuantityType;
 import org.openhab.core.library.unit.Units;
@@ -36,7 +43,11 @@ import org.openhab.core.thing.ChannelUID;
 import org.openhab.core.thing.Thing;
 import org.openhab.core.thing.ThingStatus;
 import org.openhab.core.thing.ThingStatusDetail;
+import org.openhab.core.thing.ThingTypeUID;
+import org.openhab.core.thing.ThingUID;
 import org.openhab.core.thing.binding.BaseThingHandler;
+import org.openhab.core.thing.binding.builder.ThingBuilder;
+import org.openhab.core.thing.type.ChannelGroupDefinition;
 import org.openhab.core.types.Command;
 import org.openhab.core.types.RefreshType;
 import org.openhab.core.types.State;
@@ -51,6 +62,7 @@ import org.slf4j.LoggerFactory;
  * @author Svilen Valkanov - Initial contribution
  * @author Lyubomir Papzov - Separate the creation of the systeminfo object and its initialization
  * @author Wouter Born - Add null annotations
+ * @author Mark Herwege - Add dynamic creation of extra channels
  */
 @NonNullByDefault
 public class SysteminfoHandler extends BaseThingHandler {
@@ -91,31 +103,59 @@ public class SysteminfoHandler extends BaseThingHandler {
      */
     public static final int WAIT_TIME_CHANNEL_ITEM_LINK_INIT = 1;
 
+    /**
+     * String used to extend thingUID and channelGroupTypeUID for thing definition with added dynamic channels and
+     * extended channels. It is set in the constructor and unique to the thing.
+     */
+    public final String idExtString;
+
+    public final SysteminfoThingTypeProvider thingTypeProvider;
+
     private SysteminfoInterface systeminfo;
 
     private @Nullable ScheduledFuture<?> highPriorityTasks;
     private @Nullable ScheduledFuture<?> mediumPriorityTasks;
 
-    private Logger logger = LoggerFactory.getLogger(SysteminfoHandler.class);
+    /**
+     * Caches for cpu process load and process load for a given pid. Using this cache limits the process load refresh
+     * interval to the minimum interval. Too frequent refreshes leads to inaccurate results. This could happen when the
+     * same process is tracked as current process and as a channel with pid parameter, or when the task interval is set
+     * too low.
+     */
+    private static final int MIN_PROCESS_LOAD_REFRESH_INTERVAL_MS = 2000;
+    private ExpiringCache<PercentType> cpuLoadCache = new ExpiringCache<>(MIN_PROCESS_LOAD_REFRESH_INTERVAL_MS,
+            () -> getSystemCpuLoad());
+    private ExpiringCacheMap<Integer, @Nullable DecimalType> processLoadCache = new ExpiringCacheMap<>(
+            MIN_PROCESS_LOAD_REFRESH_INTERVAL_MS);
 
-    public SysteminfoHandler(Thing thing, @Nullable SysteminfoInterface systeminfo) {
+    private final Logger logger = LoggerFactory.getLogger(SysteminfoHandler.class);
+
+    public SysteminfoHandler(Thing thing, SysteminfoThingTypeProvider thingTypeProvider,
+            SysteminfoInterface systeminfo) {
         super(thing);
-        if (systeminfo != null) {
-            this.systeminfo = systeminfo;
-        } else {
-            throw new IllegalArgumentException("No systeminfo service was provided");
-        }
+        this.thingTypeProvider = thingTypeProvider;
+        this.systeminfo = systeminfo;
+
+        idExtString = "-" + thing.getUID().getId();
     }
 
     @Override
     public void initialize() {
+        logger.trace("Initializing thing {} with thing type {}", thing.getUID().getId(),
+                thing.getThingTypeUID().getId());
+        restoreChannelsConfig(); // After a thing type change, previous channel configs will have been stored, and will
+                                 // be restored here.
         if (instantiateSysteminfoLibrary() && isConfigurationValid() && updateProperties()) {
-            groupChannelsByPriority();
-            scheduleUpdates();
-            updateStatus(ThingStatus.ONLINE);
+            if (!addDynamicChannels()) { // If there are new channel groups, the thing will get recreated with a new
+                                         // thing type and this handler will be disposed. Therefore do not do anything
+                                         // further here.
+                groupChannelsByPriority();
+                scheduleUpdates();
+                updateStatus(ThingStatus.ONLINE);
+            }
         } else {
             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.HANDLER_INITIALIZING_ERROR,
-                    "Thing cannot be initialized!");
+                    "@text/offline.cannot-initialize");
         }
     }
 
@@ -169,15 +209,129 @@ public class SysteminfoHandler extends BaseThingHandler {
         }
     }
 
+    /**
+     * Retrieve info on available storages, drives, displays, batteries, network interfaces and fans in the system. If
+     * there is more than 1, create additional channel groups and channels representing each of the entities with an
+     * index added to the channel groups and channels. The base channel groups and channels will remain without index
+     * and are equal to the channel groups and channels with index 0. If there is only one entity in a group, do not add
+     * a channels group and channels with index 0.
+     * <p>
+     * If channel groups are added, the thing type will change to systeminfo:computer-Ext, with Ext equal to the thing
+     * id. A new handler will be created and initialization restarted. Therefore further initialization of the current
+     * handler can be aborted if the method returns true.
+     *
+     * @return true if channel groups where added
+     */
+    private boolean addDynamicChannels() {
+        ThingUID thingUID = thing.getUID();
+
+        List<ChannelGroupDefinition> newChannelGroups = new ArrayList<>();
+        newChannelGroups.addAll(createChannelGroups(thingUID, CHANNEL_GROUP_STORAGE, CHANNEL_GROUP_TYPE_STORAGE,
+                systeminfo.getFileOSStoreCount()));
+        newChannelGroups.addAll(createChannelGroups(thingUID, CHANNEL_GROUP_DRIVE, CHANNEL_GROUP_TYPE_DRIVE,
+                systeminfo.getDriveCount()));
+        newChannelGroups.addAll(createChannelGroups(thingUID, CHANNEL_GROUP_DISPLAY, CHANNEL_GROUP_TYPE_DISPLAY,
+                systeminfo.getDisplayCount()));
+        newChannelGroups.addAll(createChannelGroups(thingUID, CHANNEL_GROUP_BATTERY, CHANNEL_GROUP_TYPE_BATTERY,
+                systeminfo.getPowerSourceCount()));
+        newChannelGroups.addAll(createChannelGroups(thingUID, CHANNEL_GROUP_NETWORK, CHANNEL_GROUP_TYPE_NETWORK,
+                systeminfo.getNetworkIFCount()));
+        if (!newChannelGroups.isEmpty()) {
+            logger.debug("Creating additional channel groups");
+            newChannelGroups.addAll(0, thingTypeProvider.getChannelGroupDefinitions(thing.getThingTypeUID()));
+            ThingTypeUID thingTypeUID = new ThingTypeUID(BINDING_ID, THING_TYPE_COMPUTER_ID + idExtString);
+            if (thingTypeProvider.updateThingType(thingTypeUID, newChannelGroups)) {
+                logger.trace("Channel groups were added, changing the thing type");
+                changeThingType(thingTypeUID, thing.getConfiguration());
+            } else {
+                updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.HANDLER_INITIALIZING_ERROR,
+                        "@text/offline.cannot-initialize");
+            }
+            return true;
+        }
+
+        List<Channel> newChannels = new ArrayList<>();
+        newChannels.addAll(createChannels(thingUID, CHANNEL_SENSORS_FAN_SPEED, systeminfo.getFanCount()));
+        if (!newChannels.isEmpty()) {
+            logger.debug("Creating additional channels");
+            newChannels.addAll(0, thing.getChannels());
+            ThingBuilder thingBuilder = editThing();
+            thingBuilder.withChannels(newChannels);
+            updateThing(thingBuilder.build());
+        }
+
+        return false;
+    }
+
+    private List<ChannelGroupDefinition> createChannelGroups(ThingUID thingUID, String channelGroupID,
+            String channelGroupTypeID, int count) {
+        if (count <= 1) {
+            return Collections.emptyList();
+        }
+
+        List<String> channelGroups = thingTypeProvider.getChannelGroupDefinitions(thing.getThingTypeUID()).stream()
+                .map(ChannelGroupDefinition::getId).collect(Collectors.toList());
+
+        List<ChannelGroupDefinition> newChannelGroups = new ArrayList<>();
+        for (int i = 0; i < count; i++) {
+            String index = String.valueOf(i);
+            ChannelGroupDefinition channelGroupDef = thingTypeProvider
+                    .createChannelGroupDefinitionWithIndex(channelGroupID, channelGroupTypeID, i);
+            if (!(channelGroupDef == null || channelGroups.contains(channelGroupID + index))) {
+                logger.trace("Adding channel group {}", channelGroupID + index);
+                newChannelGroups.add(channelGroupDef);
+            }
+        }
+        return newChannelGroups;
+    }
+
+    private List<Channel> createChannels(ThingUID thingUID, String channelID, int count) {
+        if (count <= 1) {
+            return Collections.emptyList();
+        }
+
+        List<Channel> newChannels = new ArrayList<>();
+        for (int i = 0; i < count; i++) {
+            Channel channel = thingTypeProvider.createChannelWithIndex(thing, channelID, i);
+            if (channel != null && thing.getChannel(channel.getUID()) == null) {
+                logger.trace("Creating channel {}", channel.getUID().getId());
+                newChannels.add(channel);
+            }
+        }
+        return newChannels;
+    }
+
+    private void storeChannelsConfig() {
+        logger.trace("Storing channel configurations");
+        thingTypeProvider.storeChannelsConfig(thing);
+    }
+
+    private void restoreChannelsConfig() {
+        logger.trace("Restoring channel configurations");
+        Map<String, Configuration> channelsConfig = thingTypeProvider.restoreChannelsConfig(thing.getUID());
+        for (String channelId : channelsConfig.keySet()) {
+            Channel channel = thing.getChannel(channelId);
+            Configuration config = channelsConfig.get(channelId);
+            if (channel != null && config != null) {
+                Configuration currentConfig = channel.getConfiguration();
+                for (String param : config.keySet()) {
+                    if (isConfigurationKeyChanged(currentConfig, config, param)) {
+                        handleChannelConfigurationChange(channel, config, param);
+                    }
+                }
+            }
+        }
+    }
+
     private void groupChannelsByPriority() {
-        logger.trace("Grouping channels by priority.");
+        logger.trace("Grouping channels by priority");
         List<Channel> channels = this.thing.getChannels();
 
         for (Channel channel : channels) {
             Configuration properties = channel.getConfiguration();
             String priority = (String) properties.get(PRIOIRITY_PARAM);
             if (priority == null) {
-                logger.debug("Channel with UID {} will not be updated. The channel has no priority set !",
+                logger.debug("Channel with UID {} will not be updated. The channel has no priority set!",
                         channel.getUID());
                 break;
             }
@@ -220,23 +374,27 @@ public class SysteminfoHandler extends BaseThingHandler {
     }
 
     private void scheduleUpdates() {
-        logger.debug("Schedule high priority tasks at fixed rate {} s.", refreshIntervalHighPriority);
+        logger.debug("Schedule high priority tasks at fixed rate {} s", refreshIntervalHighPriority);
         highPriorityTasks = scheduler.scheduleWithFixedDelay(() -> {
             publishData(highPriorityChannels);
         }, WAIT_TIME_CHANNEL_ITEM_LINK_INIT, refreshIntervalHighPriority.intValue(), TimeUnit.SECONDS);
 
-        logger.debug("Schedule medium priority tasks at fixed rate {} s.", refreshIntervalMediumPriority);
+        logger.debug("Schedule medium priority tasks at fixed rate {} s", refreshIntervalMediumPriority);
         mediumPriorityTasks = scheduler.scheduleWithFixedDelay(() -> {
             publishData(mediumPriorityChannels);
         }, WAIT_TIME_CHANNEL_ITEM_LINK_INIT, refreshIntervalMediumPriority.intValue(), TimeUnit.SECONDS);
 
-        logger.debug("Schedule one time update for low priority tasks.");
+        logger.debug("Schedule one time update for low priority tasks");
         scheduler.schedule(() -> {
             publishData(lowPriorityChannels);
         }, WAIT_TIME_CHANNEL_ITEM_LINK_INIT, TimeUnit.SECONDS);
     }
 
     private void publishData(Set<ChannelUID> channels) {
+        // if handler disposed while waiting for the links, don't update the channel states
+        if (!ThingStatus.ONLINE.equals(thing.getStatus())) {
+            return;
+        }
         Iterator<ChannelUID> iter = channels.iterator();
         while (iter.hasNext()) {
             ChannelUID channeUID = iter.next();
@@ -276,16 +434,16 @@ public class SysteminfoHandler extends BaseThingHandler {
         State state = null;
 
         String channelID = channelUID.getId();
-        String channelIDWithoutGroup = channelUID.getIdWithoutGroup();
-        String channelGroupID = channelUID.getGroupId();
-
         int deviceIndex = getDeviceIndex(channelUID);
 
-        // The channelGroup may contain deviceIndex. It must be deleted from the channelID, because otherwise the
-        // switch will not find the correct method below.
-        // All digits are deleted from the ID
-        if (channelGroupID != null) {
-            channelID = channelGroupID.replaceAll("\\d+", "") + "#" + channelIDWithoutGroup;
+        logger.trace("Getting state for channel {} with device index {}", channelID, deviceIndex);
+
+        // The channelGroup or channel may contain deviceIndex. It must be deleted from the channelID, because otherwise
+        // the switch will not find the correct method below.
+        // All digits are deleted from the ID, except for CpuLoad channels.
+        if (!(CHANNEL_CPU_LOAD_1.equals(channelID) || CHANNEL_CPU_LOAD_5.equals(channelID)
+                || CHANNEL_CPU_LOAD_15.equals(channelID))) {
+            channelID = channelID.replaceAll("\\d+", "");
         }
 
         try {
@@ -319,7 +477,7 @@ public class SysteminfoHandler extends BaseThingHandler {
                     state = systeminfo.getSensorsFanSpeed(deviceIndex);
                     break;
                 case CHANNEL_CPU_LOAD:
-                    PercentType cpuLoad = systeminfo.getSystemCpuLoad();
+                    PercentType cpuLoad = cpuLoadCache.getValue();
                     state = (cpuLoad != null) ? new QuantityType<>(cpuLoad, Units.PERCENT) : null;
                     break;
                 case CHANNEL_CPU_LOAD_1:
@@ -431,34 +589,52 @@ public class SysteminfoHandler extends BaseThingHandler {
                     state = systeminfo.getNetworkPacketsSent(deviceIndex);
                     break;
                 case CHANNEL_PROCESS_LOAD:
-                    PercentType processLoad = systeminfo.getProcessCpuUsage(deviceIndex);
+                case CHANNEL_CURRENT_PROCESS_LOAD:
+                    DecimalType processLoad = processLoadCache.putIfAbsentAndGet(deviceIndex,
+                            () -> getProcessCpuUsage(deviceIndex));
                     state = (processLoad != null) ? new QuantityType<>(processLoad, Units.PERCENT) : null;
                     break;
                 case CHANNEL_PROCESS_MEMORY:
+                case CHANNEL_CURRENT_PROCESS_MEMORY:
                     state = systeminfo.getProcessMemoryUsage(deviceIndex);
                     break;
                 case CHANNEL_PROCESS_NAME:
+                case CHANNEL_CURRENT_PROCESS_NAME:
                     state = systeminfo.getProcessName(deviceIndex);
                     break;
                 case CHANNEL_PROCESS_PATH:
+                case CHANNEL_CURRENT_PROCESS_PATH:
                     state = systeminfo.getProcessPath(deviceIndex);
                     break;
                 case CHANNEL_PROCESS_THREADS:
+                case CHANNEL_CURRENT_PROCESS_THREADS:
                     state = systeminfo.getProcessThreads(deviceIndex);
                     break;
                 default:
                     logger.debug("Channel with unknown ID: {} !", channelID);
             }
         } catch (DeviceNotFoundException e) {
-            logger.warn("No information for channel {} with device index {} :", channelID, deviceIndex);
+            logger.warn("No information for channel {} with device index: {}", channelID, deviceIndex);
         } catch (Exception e) {
             logger.debug("Unexpected error occurred while getting system information!", e);
-            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
-                    "Cannot get system info as result of unexpected error. Please try to restart the binding (remove and re-add the thing)!");
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "@text/offline.unexpected-error");
         }
         return state != null ? state : UnDefType.UNDEF;
     }
 
+    private @Nullable PercentType getSystemCpuLoad() {
+        return systeminfo.getSystemCpuLoad();
+    }
+
+    private @Nullable DecimalType getProcessCpuUsage(int pid) {
+        try {
+            return systeminfo.getProcessCpuUsage(pid);
+        } catch (DeviceNotFoundException e) {
+            logger.warn("Process with pid {} does not exist", pid);
+            return null;
+        }
+    }
+
     /**
      * The device index is an optional part of the channelID - the last characters of the groupID. It is used to
      * identify unique device, when more than one devices are available (e.g. local disks with names C:\, D:\, E"\ - the
@@ -469,6 +645,7 @@ public class SysteminfoHandler extends BaseThingHandler {
      * @return natural number (number >=0)
      */
     private int getDeviceIndex(ChannelUID channelUID) {
+        String channelID = channelUID.getId();
         String channelGroupID = channelUID.getGroupId();
         if (channelGroupID == null) {
             return 0;
@@ -481,13 +658,23 @@ public class SysteminfoHandler extends BaseThingHandler {
             return pid;
         }
 
-        char lastChar = channelGroupID.charAt(channelGroupID.length() - 1);
-        if (Character.isDigit(lastChar)) {
-            // All non-digits are deleted from the ID
+        if (channelGroupID.contains(CHANNEL_GROUP_CURRENT_PROCESS)) {
+            int pid = systeminfo.getCurrentProcessID();
+            return pid;
+        }
+
+        // First try to get device index in group id, delete all non-digits from id
+        if (Character.isDigit(channelGroupID.charAt(channelGroupID.length() - 1))) {
             String deviceIndexPart = channelGroupID.replaceAll("\\D+", "");
             return Integer.parseInt(deviceIndexPart);
         }
 
+        // If not found, try to find it in channel id, delete all non-digits from id
+        if (Character.isDigit(channelID.charAt(channelID.length() - 1))) {
+            String deviceIndexPart = channelID.replaceAll("\\D+", "");
+            return Integer.parseInt(deviceIndexPart);
+        }
+
         return 0;
     }
 
@@ -510,10 +697,10 @@ public class SysteminfoHandler extends BaseThingHandler {
                     pid = pidValue.intValue();
                 }
             } else {
-                logger.debug("Channel does not exist ! Fall back to default value.");
+                logger.debug("Channel does not exist! Fall back to default value.");
             }
         } catch (ClassCastException e) {
-            logger.debug("Channel configuration cannot be read ! Fall back to default value.", e);
+            logger.debug("Channel configuration cannot be read! Fall back to default value.", e);
         } catch (IllegalArgumentException e) {
             logger.debug("PID (Process Identifier) must be positive number. Fall back to default value. ", e);
         }
@@ -524,10 +711,10 @@ public class SysteminfoHandler extends BaseThingHandler {
     public void handleCommand(ChannelUID channelUID, Command command) {
         if (thing.getStatus().equals(ThingStatus.ONLINE)) {
             if (command instanceof RefreshType) {
-                logger.debug("Refresh command received for channel {}!", channelUID);
+                logger.debug("Refresh command received for channel {} !", channelUID);
                 publishDataForChannel(channelUID);
             } else {
-                logger.debug("Unsupported command {}! Supported commands: REFRESH", command);
+                logger.debug("Unsupported command {} ! Supported commands: REFRESH", command);
             }
         } else {
             logger.debug("Cannot handle command. Thing is not ONLINE.");
@@ -546,9 +733,10 @@ public class SysteminfoHandler extends BaseThingHandler {
     }
 
     @Override
-    public void thingUpdated(Thing thing) {
-        logger.trace("About to update thing.");
+    public synchronized void thingUpdated(Thing thing) {
+        logger.trace("About to update thing");
         boolean isChannelConfigChanged = false;
+
         List<Channel> channels = thing.getChannels();
 
         for (Channel channel : channels) {
@@ -557,7 +745,7 @@ public class SysteminfoHandler extends BaseThingHandler {
             Channel oldChannel = this.thing.getChannel(channelUID.getId());
 
             if (oldChannel == null) {
-                logger.warn("Channel with UID {} cannot be updated, as it cannot be found !", channelUID);
+                logger.warn("Channel with UID {} cannot be updated, as it cannot be found!", channelUID);
                 continue;
             }
             Configuration currentChannelConfig = oldChannel.getConfiguration();
@@ -594,16 +782,22 @@ public class SysteminfoHandler extends BaseThingHandler {
         publishDataForChannel(channel.getUID());
     }
 
+    @Override
+    protected void changeThingType(ThingTypeUID thingTypeUID, Configuration configuration) {
+        storeChannelsConfig();
+        super.changeThingType(thingTypeUID, configuration);
+    }
+
     private void stopScheduledUpdates() {
         ScheduledFuture<?> localHighPriorityTasks = highPriorityTasks;
         if (localHighPriorityTasks != null) {
-            logger.debug("High prioriy tasks will not be run anymore !");
+            logger.debug("High prioriy tasks will not be run anymore!");
             localHighPriorityTasks.cancel(true);
         }
 
         ScheduledFuture<?> localMediumPriorityTasks = mediumPriorityTasks;
         if (localMediumPriorityTasks != null) {
-            logger.debug("Medium prioriy tasks will not be run anymore !");
+            logger.debug("Medium prioriy tasks will not be run anymore!");
             localMediumPriorityTasks.cancel(true);
         }
     }
index 5ab70b3adc764fad55bfb605c53c6063cd90cdaa..4c4b3903c7ad8887aed3399d997ad29b52422189 100644 (file)
@@ -52,6 +52,7 @@ import oshi.util.EdidUtil;
  * @author Christoph Weitkamp - Update to OSHI 3.13.0 - Replaced deprecated method
  *         CentralProcessor#getSystemSerialNumber()
  * @author Wouter Born - Update to OSHI 4.0.0 and add null annotations
+ * @author Mark Herwege - Add dynamic creation of extra channels
  *
  * @see <a href="https://github.com/oshi/oshi">OSHI GitHub repository</a>
  */
@@ -350,6 +351,8 @@ public class OSHISysteminfo implements SysteminfoInterface {
         int speed = 0; // 0 means unable to measure speed
         if (index < fanSpeeds.length) {
             speed = fanSpeeds[index];
+        } else {
+            throw new DeviceNotFoundException();
         }
         return speed > 0 ? new DecimalType(speed) : null;
     }
@@ -608,6 +611,11 @@ public class OSHISysteminfo implements SysteminfoInterface {
         return new DecimalType(getSizeInMB(bytesRecv));
     }
 
+    @Override
+    public int getCurrentProcessID() {
+        return operatingSystem.getProcessId();
+    }
+
     @Override
     public @Nullable StringType getProcessName(int pid) throws DeviceNotFoundException {
         if (pid > 0) {
@@ -620,11 +628,11 @@ public class OSHISysteminfo implements SysteminfoInterface {
     }
 
     @Override
-    public @Nullable PercentType getProcessCpuUsage(int pid) throws DeviceNotFoundException {
+    public @Nullable DecimalType getProcessCpuUsage(int pid) throws DeviceNotFoundException {
         if (pid > 0) {
             OSProcess process = getProcess(pid);
-            PercentType load = (processTicks.containsKey(pid))
-                    ? new PercentType(getPercentsValue(process.getProcessCpuLoadBetweenTicks(processTicks.get(pid))))
+            DecimalType load = (processTicks.containsKey(pid))
+                    ? new DecimalType(getPercentsValue(process.getProcessCpuLoadBetweenTicks(processTicks.get(pid))))
                     : null;
             processTicks.put(pid, process);
             return load;
@@ -666,4 +674,34 @@ public class OSHISysteminfo implements SysteminfoInterface {
             return null;
         }
     }
+
+    @Override
+    public int getNetworkIFCount() {
+        return networks.size();
+    }
+
+    @Override
+    public int getDisplayCount() {
+        return displays.size();
+    }
+
+    @Override
+    public int getFileOSStoreCount() {
+        return fileStores.size();
+    }
+
+    @Override
+    public int getPowerSourceCount() {
+        return powerSources.size();
+    }
+
+    @Override
+    public int getDriveCount() {
+        return drives.size();
+    }
+
+    @Override
+    public int getFanCount() {
+        return sensors.getFanSpeeds().length;
+    }
 }
index a606f7a26c81d252c68aa54c01b2f89aa9ea60cd..87a7c97610ac65d740fa89b54deddb157b1d2563 100644 (file)
@@ -23,6 +23,7 @@ import org.openhab.core.library.types.StringType;
  *
  * @author Svilen Valkanov - Initial contribution
  * @author Wouter Born - Add null annotations
+ * @author Mark Herwege - Add dynamic creation of extra channels
  */
 @NonNullByDefault
 public interface SysteminfoInterface {
@@ -404,6 +405,13 @@ public interface SysteminfoInterface {
      */
     public StringType getBatteryName(int deviceIndex) throws DeviceNotFoundException;
 
+    /**
+     * Get PID of process executing this code
+     *
+     * @return current process ID
+     */
+    int getCurrentProcessID();
+
     /**
      * Returns the name of the process
      *
@@ -416,10 +424,10 @@ public interface SysteminfoInterface {
      * Returns the CPU usage of the process
      *
      * @param pid - the PID of the process
-     * @return - percentage value /0-100/
+     * @return - percentage value, can be above 100% if process uses multiple cores
      * @throws DeviceNotFoundException - thrown if process with this PID can not be found
      */
-    public @Nullable PercentType getProcessCpuUsage(int pid) throws DeviceNotFoundException;
+    public @Nullable DecimalType getProcessCpuUsage(int pid) throws DeviceNotFoundException;
 
     /**
      * Returns the size of RAM memory only usage of the process
@@ -445,4 +453,46 @@ public interface SysteminfoInterface {
      * @throws DeviceNotFoundException - thrown if process with this PID can not be found
      */
     public @Nullable DecimalType getProcessThreads(int pid) throws DeviceNotFoundException;
+
+    /**
+     * Returns the number of network interfaces.
+     *
+     * @return network interface count
+     */
+    public int getNetworkIFCount();
+
+    /**
+     * Returns the number of displays.
+     *
+     * @return display count
+     */
+    public int getDisplayCount();
+
+    /**
+     * Returns the number of storages.
+     *
+     * @return storage count
+     */
+    public int getFileOSStoreCount();
+
+    /**
+     * Returns the number of power sources/batteries.
+     *
+     * @return power source count
+     */
+    public int getPowerSourceCount();
+
+    /**
+     * Returns the number of drives.
+     *
+     * @return drive count
+     */
+    public int getDriveCount();
+
+    /**
+     * Returns the number of fans.
+     *
+     * @return fan count
+     */
+    int getFanCount();
 }
index ebb3eded3dba0494e1ca3a5d2a45ce223eeeb9d0..289ef8f0d6e70935e64c0552febe1e3771eaa65a 100644 (file)
@@ -29,6 +29,8 @@ channel-group-type.systeminfo.memoryGroup.label = Physical Memory
 channel-group-type.systeminfo.memoryGroup.description = Physical memory information
 channel-group-type.systeminfo.networkGroup.label = Network
 channel-group-type.systeminfo.networkGroup.description = Network parameters
+channel-group-type.systeminfo.currentProcessGroup.label = Current Process
+channel-group-type.systeminfo.currentProcessGroup.description = Current process information
 channel-group-type.systeminfo.processGroup.label = Process
 channel-group-type.systeminfo.processGroup.description = System process information
 channel-group-type.systeminfo.sensorsGroup.label = Sensor
@@ -62,8 +64,8 @@ channel-type.systeminfo.information.label = Display Information
 channel-type.systeminfo.information.description = Product, manufacturer, SN, width and height of the display in cm
 channel-type.systeminfo.ip.label = IP Address
 channel-type.systeminfo.ip.description = Host IP address of the network
-channel-type.systeminfo.cpuLoad.label = CPU Load
-channel-type.systeminfo.cpuLoad.description = CPU load in percent
+channel-type.systeminfo.load.label = Load
+channel-type.systeminfo.load.description = Load in percent
 channel-type.systeminfo.loadAverage.label = Load Average
 channel-type.systeminfo.loadAverage.description = Load as a number of processes for the last 1,5 or 15 minutes
 channel-type.systeminfo.load_process.label = Load
@@ -84,6 +86,8 @@ channel-type.systeminfo.packetsReceived.label = Packets Received
 channel-type.systeminfo.packetsReceived.description = Number of packets received
 channel-type.systeminfo.packetsSent.label = Packets Sent
 channel-type.systeminfo.packetsSent.description = Number of packets sent
+channel-type.systeminfo.path.label = Path
+channel-type.systeminfo.path.description = The full path
 channel-type.systeminfo.path_process.label = Path
 channel-type.systeminfo.path_process.description = The full path
 channel-type.systeminfo.remainingCapacity.label = Remaining Capacity
@@ -151,3 +155,7 @@ channel-type.config.systeminfo.mediumpriority_process.priority.description = Ref
 channel-type.config.systeminfo.mediumpriority_process.priority.option.High = High
 channel-type.config.systeminfo.mediumpriority_process.priority.option.Medium = Medium
 channel-type.config.systeminfo.mediumpriority_process.priority.option.Low = Low
+
+# thing status messages
+offline.cannot-initialize = Thing cannot be initialized!
+offline.unexpected-error = Cannot get system info as result of unexpected error. Please try to restart the binding (remove and re-add the thing)!
index 16ddabce8ebc76aec5de3d7708f82994668c69f6..5add605847cf067b129f0739c904bf63473b249c 100644 (file)
                <channels>
                        <channel id="name" typeId="name"/>
                        <channel id="description" typeId="description"/>
-                       <channel id="load" typeId="cpuLoad"/>
+                       <channel id="load" typeId="load"/>
                        <channel id="load1" typeId="loadAverage"/>
                        <channel id="load5" typeId="loadAverage"/>
                        <channel id="load15" typeId="loadAverage"/>
                </channels>
        </channel-group-type>
 
+       <channel-group-type id="currentProcessGroup">
+               <label>Current Process</label>
+               <description>Current process information</description>
+               <channels>
+                       <channel id="load" typeId="load"/>
+                       <channel id="used" typeId="used"/>
+                       <channel id="name" typeId="name"/>
+                       <channel id="threads" typeId="threads"/>
+                       <channel id="path" typeId="path"/>
+               </channels>
+       </channel-group-type>
+
        <channel-group-type id="processGroup">
                <label>Process</label>
                <description>System process information</description>
                <config-description-ref uri="channel-type:systeminfo:mediumpriority"/>
        </channel-type>
 
+       <channel-type id="path">
+               <item-type>String</item-type>
+               <label>Path</label>
+               <description>The full path</description>
+               <state readOnly="true" pattern="%s"/>
+               <config-description-ref uri="channel-type:systeminfo:lowpriority"/>
+       </channel-type>
+
        <channel-type id="path_process">
                <item-type>String</item-type>
                <label>Path</label>
                <config-description-ref uri="channel-type:systeminfo:mediumpriority"/>
        </channel-type>
 
-       <channel-type id="load_process">
+       <channel-type id="load">
                <item-type>Number:Dimensionless</item-type>
                <label>Load</label>
                <description>Load in percent</description>
                <state readOnly="true" pattern="%.1f %%"/>
-               <config-description-ref uri="channel-type:systeminfo:highpriority_process"/>
+               <config-description-ref uri="channel-type:systeminfo:highpriority"/>
        </channel-type>
 
-       <channel-type id="cpuLoad">
+       <channel-type id="load_process">
                <item-type>Number:Dimensionless</item-type>
-               <label>CPU Load</label>
-               <description>CPU load in percent</description>
+               <label>Load</label>
+               <description>Load in percent</description>
                <state readOnly="true" pattern="%.1f %%"/>
-               <config-description-ref uri="channel-type:systeminfo:highpriority"/>
+               <config-description-ref uri="channel-type:systeminfo:highpriority_process"/>
        </channel-type>
 
        <channel-type id="loadAverage" advanced="true">
index ef680220dc7a4d100a88a6e9ce639afb95a76de1..31c543de77c9b5238580f8db79cac479120e53ef 100644 (file)
@@ -16,7 +16,7 @@
                        <channel-group id="storage" typeId="storageGroup"/>
                        <channel-group id="sensors" typeId="sensorsGroup"/>
                        <channel-group id="cpu" typeId="cpuGroup"/>
-                       <!-- This group types are not mandatory for every computer configuration -->
+                       <channel-group id="currentProcess" typeId="currentProcessGroup"/>
                        <channel-group id="process" typeId="processGroup"/>
                        <channel-group id="drive" typeId="driveGroup"/>
                        <channel-group id="swap" typeId="swapGroup"/>
index 146b3bc64202658435aadca6a6b2ffaf4f7cfcf8..022b2e258e37244d9bd24fb9110570396bcf2fdf 100644 (file)
@@ -74,4 +74,6 @@ Fragment-Host: org.openhab.binding.systeminfo
        org.openhab.core.io.console;version='[3.4.0,3.4.1)',\
        org.openhab.core.test;version='[3.4.0,3.4.1)',\
        org.openhab.core.thing;version='[3.4.0,3.4.1)',\
-       org.openhab.core.thing.xml;version='[3.4.0,3.4.1)'
+       org.openhab.core.thing.xml;version='[3.4.0,3.4.1)',\
+       org.mockito.junit-jupiter;version='[4.1.0,4.1.1)',\
+       com.github.oshi.oshi-core;version='[6.2.2,6.2.3)'
index 24d90803624e046c19a28815fae317ec860efb36..57b78ead6e22d95bc4f982fcb01aa2fa33550c6e 100644 (file)
@@ -24,15 +24,21 @@ import java.net.UnknownHostException;
 import java.util.Hashtable;
 import java.util.List;
 
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Disabled;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
 import org.openhab.binding.systeminfo.internal.SysteminfoBindingConstants;
 import org.openhab.binding.systeminfo.internal.SysteminfoHandlerFactory;
+import org.openhab.binding.systeminfo.internal.SysteminfoThingTypeProvider;
 import org.openhab.binding.systeminfo.internal.discovery.SysteminfoDiscoveryService;
 import org.openhab.binding.systeminfo.internal.handler.SysteminfoHandler;
 import org.openhab.binding.systeminfo.internal.model.DeviceNotFoundException;
+import org.openhab.binding.systeminfo.internal.model.OSHISysteminfo;
 import org.openhab.binding.systeminfo.internal.model.SysteminfoInterface;
 import org.openhab.core.config.core.Configuration;
 import org.openhab.core.config.discovery.DiscoveryResult;
@@ -61,6 +67,7 @@ import org.openhab.core.thing.ThingTypeUID;
 import org.openhab.core.thing.ThingUID;
 import org.openhab.core.thing.binding.ThingHandler;
 import org.openhab.core.thing.binding.ThingHandlerFactory;
+import org.openhab.core.thing.binding.ThingTypeProvider;
 import org.openhab.core.thing.binding.builder.ChannelBuilder;
 import org.openhab.core.thing.binding.builder.ThingBuilder;
 import org.openhab.core.thing.link.ItemChannelLink;
@@ -78,6 +85,8 @@ import org.openhab.core.types.UnDefType;
  *         but mock data will be used instead, avoiding potential errors from the OS queries.
  * @author Wouter Born - Migrate Groovy to Java tests
  */
+@NonNullByDefault
+@ExtendWith(MockitoExtension.class)
 public class SysteminfoOSGiTest extends JavaOSGiTest {
     private static final String DEFAULT_TEST_THING_NAME = "work";
     private static final String DEFAULT_TEST_ITEM_NAME = "test";
@@ -97,15 +106,13 @@ public class SysteminfoOSGiTest extends JavaOSGiTest {
      */
     private static final int DEFAULT_TEST_INTERVAL_MEDIUM = 3;
 
-    private Thing systemInfoThing;
-    private SysteminfoHandler systemInfoHandler;
-    private GenericItem testItem;
+    private @Nullable Thing systemInfoThing;
+    private @Nullable GenericItem testItem;
 
-    private SysteminfoInterface mockedSystemInfo;
-    private ManagedThingProvider managedThingProvider;
-    private ThingRegistry thingRegistry;
-    private ItemRegistry itemRegistry;
-    private SysteminfoHandlerFactory systeminfoHandlerFactory;
+    private @Mock @NonNullByDefault({}) OSHISysteminfo mockedSystemInfo;
+    private @NonNullByDefault({}) SysteminfoHandlerFactory systeminfoHandlerFactory;
+    private @NonNullByDefault({}) ThingRegistry thingRegistry;
+    private @NonNullByDefault({}) ItemRegistry itemRegistry;
 
     @BeforeEach
     public void setUp() {
@@ -113,48 +120,75 @@ public class SysteminfoOSGiTest extends JavaOSGiTest {
         registerService(volatileStorageService);
 
         // Preparing the mock with OS properties, that are used in the initialize method of SysteminfoHandler
-        mockedSystemInfo = mock(SysteminfoInterface.class);
-        when(mockedSystemInfo.getCpuLogicalCores()).thenReturn(new DecimalType(2));
-        when(mockedSystemInfo.getCpuPhysicalCores()).thenReturn(new DecimalType(2));
-        when(mockedSystemInfo.getOsFamily()).thenReturn(new StringType("Mock OS"));
-        when(mockedSystemInfo.getOsManufacturer()).thenReturn(new StringType("Mock OS Manufacturer"));
-        when(mockedSystemInfo.getOsVersion()).thenReturn(new StringType("Mock Os Version"));
-
-        systeminfoHandlerFactory = getService(ThingHandlerFactory.class, SysteminfoHandlerFactory.class);
-        SysteminfoInterface oshiSystemInfo = getService(SysteminfoInterface.class);
-
-        // Unbind oshiSystemInfo service and bind the mock service to make the systeminfobinding tests independent of
-        // the external OSHI library
-        if (oshiSystemInfo != null) {
-            systeminfoHandlerFactory.unbindSystemInfo(oshiSystemInfo);
+        // Make this lenient because the assertInvalidThingConfigurationValuesAreHandled test does not require them
+        lenient().when(mockedSystemInfo.getCpuLogicalCores()).thenReturn(new DecimalType(2));
+        lenient().when(mockedSystemInfo.getCpuPhysicalCores()).thenReturn(new DecimalType(2));
+        lenient().when(mockedSystemInfo.getOsFamily()).thenReturn(new StringType("Mock OS"));
+        lenient().when(mockedSystemInfo.getOsManufacturer()).thenReturn(new StringType("Mock OS Manufacturer"));
+        lenient().when(mockedSystemInfo.getOsVersion()).thenReturn(new StringType("Mock Os Version"));
+        // Following mock method returns will make sure the thing does not get recreated with extra channels
+        lenient().when(mockedSystemInfo.getNetworkIFCount()).thenReturn(1);
+        lenient().when(mockedSystemInfo.getDisplayCount()).thenReturn(1);
+        lenient().when(mockedSystemInfo.getFileOSStoreCount()).thenReturn(1);
+        lenient().when(mockedSystemInfo.getPowerSourceCount()).thenReturn(1);
+        lenient().when(mockedSystemInfo.getDriveCount()).thenReturn(1);
+        lenient().when(mockedSystemInfo.getFanCount()).thenReturn(1);
+
+        registerService(mockedSystemInfo);
+
+        waitForAssert(() -> {
+            systeminfoHandlerFactory = getService(ThingHandlerFactory.class, SysteminfoHandlerFactory.class);
+            assertThat(systeminfoHandlerFactory, is(notNullValue()));
+        });
+        if (systeminfoHandlerFactory != null) {
+            // Unbind oshiSystemInfo service and bind the mock service to make the systeminfo binding tests independent
+            // of the external OSHI library
+            SysteminfoInterface oshiSystemInfo = getService(SysteminfoInterface.class);
+            if (oshiSystemInfo != null) {
+                systeminfoHandlerFactory.unbindSystemInfo(oshiSystemInfo);
+            }
+            systeminfoHandlerFactory.bindSystemInfo(mockedSystemInfo);
         }
-        systeminfoHandlerFactory.bindSystemInfo(mockedSystemInfo);
 
-        managedThingProvider = getService(ThingProvider.class, ManagedThingProvider.class);
-        assertThat(managedThingProvider, is(notNullValue()));
+        waitForAssert(() -> {
+            ThingTypeProvider thingTypeProvider = getService(ThingTypeProvider.class,
+                    SysteminfoThingTypeProvider.class);
+            assertThat(thingTypeProvider, is(notNullValue()));
+        });
+        waitForAssert(() -> {
+            SysteminfoThingTypeProvider systeminfoThingTypeProvider = getService(SysteminfoThingTypeProvider.class);
+            assertThat(systeminfoThingTypeProvider, is(notNullValue()));
+        });
 
-        thingRegistry = getService(ThingRegistry.class);
-        assertThat(thingRegistry, is(notNullValue()));
+        waitForAssert(() -> {
+            thingRegistry = getService(ThingRegistry.class);
+            assertThat(thingRegistry, is(notNullValue()));
+        });
 
-        itemRegistry = getService(ItemRegistry.class);
-        assertThat(itemRegistry, is(notNullValue()));
+        waitForAssert(() -> {
+            itemRegistry = getService(ItemRegistry.class);
+            assertThat(itemRegistry, is(notNullValue()));
+        });
     }
 
     @AfterEach
     public void tearDown() {
-        if (systemInfoThing != null) {
+        Thing thing = systemInfoThing;
+        if (thing != null) {
             // Remove the systeminfo thing. The handler will be also disposed automatically
-            Thing removedThing = thingRegistry.forceRemove(systemInfoThing.getUID());
+            Thing removedThing = thingRegistry.forceRemove(thing.getUID());
             assertThat("The systeminfo thing cannot be deleted", removedThing, is(notNullValue()));
+            waitForAssert(() -> {
+                ThingHandler systemInfoHandler = thing.getHandler();
+                assertThat(systemInfoHandler, is(nullValue()));
+            });
         }
-        waitForAssert(() -> {
-            ThingHandler systemInfoHandler = systemInfoThing.getHandler();
-            assertThat(systemInfoHandler, is(nullValue()));
-        });
 
         if (testItem != null) {
             itemRegistry.remove(DEFAULT_TEST_ITEM_NAME);
         }
+
+        unregisterService(mockedSystemInfo);
     }
 
     private void initializeThingWithChannelAndPID(String channelID, String acceptedItemType, int pid) {
@@ -214,18 +248,24 @@ public class SysteminfoOSGiTest extends JavaOSGiTest {
         Channel channel = ChannelBuilder.create(channelUID, acceptedItemType).withType(channelTypeUID)
                 .withKind(ChannelKind.STATE).withConfiguration(channelConfig).build();
 
-        systemInfoThing = ThingBuilder.create(thingTypeUID, thingUID).withConfiguration(thingConfiguration)
+        Thing thing = ThingBuilder.create(thingTypeUID, thingUID).withConfiguration(thingConfiguration)
                 .withChannel(channel).build();
+        systemInfoThing = thing;
 
-        managedThingProvider.add(systemInfoThing);
+        ManagedThingProvider managedThingProvider = getService(ThingProvider.class, ManagedThingProvider.class);
+        assertThat(managedThingProvider, is(notNullValue()));
+
+        if (managedThingProvider != null) {
+            managedThingProvider.add(thing);
+        }
 
         waitForAssert(() -> {
-            systemInfoHandler = (SysteminfoHandler) systemInfoThing.getHandler();
-            assertThat(systemInfoHandler, is(notNullValue()));
+            SysteminfoHandler handler = (SysteminfoHandler) thing.getHandler();
+            assertThat(handler, is(notNullValue()));
         });
 
         waitForAssert(() -> {
-            assertThat("Thing is not initilized, before an Item is created", systemInfoThing.getStatus(),
+            assertThat("Thing is not initialized, before an Item is created", thing.getStatus(),
                     anyOf(equalTo(ThingStatus.OFFLINE), equalTo(ThingStatus.ONLINE)));
         });
 
@@ -233,11 +273,15 @@ public class SysteminfoOSGiTest extends JavaOSGiTest {
     }
 
     private void assertItemState(String acceptedItemType, String itemName, String priority, State expectedState) {
+        Thing thing = systemInfoThing;
+        if (thing == null) {
+            throw new AssertionError("Thing is null");
+        }
         waitForAssert(() -> {
-            ThingStatusDetail thingStatusDetail = systemInfoThing.getStatusInfo().getStatusDetail();
-            String description = systemInfoThing.getStatusInfo().getDescription();
+            ThingStatusDetail thingStatusDetail = thing.getStatusInfo().getStatusDetail();
+            String description = thing.getStatusInfo().getDescription();
             assertThat("Thing status detail is " + thingStatusDetail + " with description " + description,
-                    systemInfoThing.getStatus(), is(equalTo(ThingStatus.ONLINE)));
+                    thing.getStatus(), is(equalTo(ThingStatus.ONLINE)));
         });
         // The binding starts all refresh tasks in SysteminfoHandler.scheduleUpdates() after this delay !
         try {
@@ -254,9 +298,9 @@ public class SysteminfoOSGiTest extends JavaOSGiTest {
         }
 
         int waitTime;
-        if (priority.equals("High")) {
+        if ("High".equals(priority)) {
             waitTime = DEFAULT_TEST_INTERVAL_HIGH * 1000;
-        } else if (priority.equals("Medium")) {
+        } else if ("Medium".equals(priority)) {
             waitTime = DEFAULT_TEST_INTERVAL_MEDIUM * 1000;
         } else {
             waitTime = 100;
@@ -269,16 +313,25 @@ public class SysteminfoOSGiTest extends JavaOSGiTest {
     }
 
     private void intializeItem(ChannelUID channelUID, String itemName, String acceptedItemType) {
-        if (acceptedItemType.equals("Number")) {
-            testItem = new NumberItem(itemName);
-        } else if (acceptedItemType.equals("String")) {
-            testItem = new StringItem(itemName);
+        GenericItem item = null;
+        if ("Number".equals(acceptedItemType)) {
+            item = new NumberItem(itemName);
+        } else if ("String".equals(acceptedItemType)) {
+            item = new StringItem(itemName);
+        }
+        if (item == null) {
+            throw new AssertionError("Item is null");
         }
-        itemRegistry.add(testItem);
+        itemRegistry.add(item);
+        testItem = item;
 
         ManagedItemChannelLinkProvider itemChannelLinkProvider = getService(ManagedItemChannelLinkProvider.class);
         assertThat(itemChannelLinkProvider, is(notNullValue()));
 
+        if (itemChannelLinkProvider == null) {
+            return;
+        }
+
         itemChannelLinkProvider.add(new ItemChannelLink(itemName, channelUID));
     }
 
@@ -300,29 +353,13 @@ public class SysteminfoOSGiTest extends JavaOSGiTest {
 
     private void testInvalidConfiguration() {
         waitForAssert(() -> {
-            assertThat("Invalid configuratuin is used !", systemInfoThing.getStatus(),
-                    is(equalTo(ThingStatus.OFFLINE)));
-            assertThat(systemInfoThing.getStatusInfo().getStatusDetail(),
-                    is(equalTo(ThingStatusDetail.HANDLER_INITIALIZING_ERROR)));
-            assertThat(systemInfoThing.getStatusInfo().getDescription(), is(equalTo("Thing cannot be initialized!")));
-        });
-    }
-
-    @Test
-    public void assertThingStatusIsUninitializedWhenThereIsNoSysteminfoServiceProvided() {
-        // Unbind the mock service to verify the systeminfo thing will not be initialized when no systeminfo service is
-        // provided
-        systeminfoHandlerFactory.unbindSystemInfo(mockedSystemInfo);
-
-        ThingTypeUID thingTypeUID = SysteminfoBindingConstants.THING_TYPE_COMPUTER;
-        ThingUID thingUID = new ThingUID(thingTypeUID, DEFAULT_TEST_THING_NAME);
-
-        systemInfoThing = ThingBuilder.create(thingTypeUID, thingUID).build();
-        managedThingProvider.add(systemInfoThing);
-
-        waitForAssert(() -> {
-            assertThat("The thing status is uninitialized when systeminfo service is missing",
-                    systemInfoThing.getStatus(), equalTo(ThingStatus.UNINITIALIZED));
+            Thing thing = systemInfoThing;
+            if (thing != null) {
+                assertThat("Invalid configuration is used !", thing.getStatus(), is(equalTo(ThingStatus.OFFLINE)));
+                assertThat(thing.getStatusInfo().getStatusDetail(),
+                        is(equalTo(ThingStatusDetail.HANDLER_INITIALIZING_ERROR)));
+                assertThat(thing.getStatusInfo().getDescription(), is(equalTo("@text/offline.cannot-initialize")));
+            }
         });
     }
 
@@ -672,7 +709,7 @@ public class SysteminfoOSGiTest extends JavaOSGiTest {
                 mockedDriveSerialNumber);
     }
 
-    @Disabled
+    // Re-enable this previously disabled test, as it is not relying on hardware anymore, but a mocked object
     // There is a bug opened for this issue - https://github.com/dblock/oshi/issues/185
     @Test
     public void assertChannelSensorsCpuTempIsUpdated() {
@@ -874,7 +911,7 @@ public class SysteminfoOSGiTest extends JavaOSGiTest {
 
         @Override
         protected String getHostName() throws UnknownHostException {
-            if (hostname.equals("unresolved")) {
+            if ("unresolved".equals(hostname)) {
                 throw new UnknownHostException();
             }
             return hostname;
@@ -938,6 +975,10 @@ public class SysteminfoOSGiTest extends JavaOSGiTest {
         Inbox inbox = getService(Inbox.class);
         assertThat(inbox, is(notNullValue()));
 
+        if (inbox == null) {
+            return;
+        }
+
         waitForAssert(() -> {
             List<DiscoveryResult> results = inbox.stream().filter(InboxPredicates.forThingUID(computerUID))
                     .collect(toList());
@@ -951,8 +992,13 @@ public class SysteminfoOSGiTest extends JavaOSGiTest {
             assertThat(systemInfoThing, is(notNullValue()));
         });
 
+        Thing thing = systemInfoThing;
+        if (thing == null) {
+            return;
+        }
+
         waitForAssert(() -> {
-            assertThat("Thing is not initialized.", systemInfoThing.getStatus(), is(equalTo(ThingStatus.ONLINE)));
+            assertThat("Thing is not initialized.", thing.getStatus(), is(equalTo(ThingStatus.ONLINE)));
         });
     }
 
@@ -1020,7 +1066,7 @@ public class SysteminfoOSGiTest extends JavaOSGiTest {
         // The pid of the System idle process in Windows
         int pid = 0;
 
-        PercentType mockedProcessLoad = new PercentType(3);
+        DecimalType mockedProcessLoad = new DecimalType(3);
         when(mockedSystemInfo.getProcessCpuUsage(pid)).thenReturn(mockedProcessLoad);
 
         initializeThingWithChannelAndPID(channnelID, acceptedItemType, pid);
@@ -1037,15 +1083,27 @@ public class SysteminfoOSGiTest extends JavaOSGiTest {
         String acceptedItemType = "Number";
         initializeThingWithChannel(DEFAULT_TEST_CHANNEL_ID, acceptedItemType);
 
-        Channel channel = systemInfoThing.getChannel(DEFAULT_TEST_CHANNEL_ID);
+        Thing thing = systemInfoThing;
+        if (thing == null) {
+            throw new AssertionError("Thing is null");
+        }
+        Channel channel = thing.getChannel(DEFAULT_TEST_CHANNEL_ID);
         if (channel == null) {
             throw new AssertionError("Channel '" + DEFAULT_TEST_CHANNEL_ID + "' is null");
         }
 
+        ThingHandler thingHandler = thing.getHandler();
+        if (thingHandler == null) {
+            throw new AssertionError("Thing handler is null");
+        }
+        if (!(thingHandler.getClass().equals(SysteminfoHandler.class))) {
+            throw new AssertionError("Thing handler not of class SysteminfoHandler");
+        }
+        SysteminfoHandler handler = (SysteminfoHandler) thingHandler;
         waitForAssert(() -> {
             assertThat("The initial priority of channel " + channel.getUID() + " is not as expected.",
                     channel.getConfiguration().get(priorityKey), is(equalTo(initialPriority)));
-            assertThat(systemInfoHandler.getHighPriorityChannels().contains(channel.getUID()), is(true));
+            assertThat(handler.getHighPriorityChannels().contains(channel.getUID()), is(true));
         });
 
         // Change the priority of a channel, keep the pid
@@ -1056,15 +1114,15 @@ public class SysteminfoOSGiTest extends JavaOSGiTest {
                 .withType(channel.getChannelTypeUID()).withKind(channel.getKind()).withConfiguration(updatedConfig)
                 .build();
 
-        Thing updatedThing = ThingBuilder.create(systemInfoThing.getThingTypeUID(), systemInfoThing.getUID())
-                .withConfiguration(systemInfoThing.getConfiguration()).withChannel(updatedChannel).build();
+        Thing updatedThing = ThingBuilder.create(thing.getThingTypeUID(), thing.getUID())
+                .withConfiguration(thing.getConfiguration()).withChannel(updatedChannel).build();
 
-        systemInfoHandler.thingUpdated(updatedThing);
+        handler.thingUpdated(updatedThing);
 
         waitForAssert(() -> {
             assertThat("The prority of the channel was not updated: ", channel.getConfiguration().get(priorityKey),
                     is(equalTo(newPriority)));
-            assertThat(systemInfoHandler.getLowPriorityChannels().contains(channel.getUID()), is(true));
+            assertThat(handler.getLowPriorityChannels().contains(channel.getUID()), is(true));
         });
     }
 }