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.
* **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:
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" }
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
* 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
/**
// 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
*/
*/
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
*/
*/
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
*/
*/
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
*/
*/
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.
*/
*/
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
*/
*/
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
*/
*/
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
*/
*/
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
*/
*/
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;
* @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;
}
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;
+ }
}
--- /dev/null
+/**
+ * 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();
+ }
+}
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;
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;
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;
* @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 {
*/
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");
}
}
}
}
+ /**
+ * 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;
}
}
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();
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 {
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:
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
* @return natural number (number >=0)
*/
private int getDeviceIndex(ChannelUID channelUID) {
+ String channelID = channelUID.getId();
String channelGroupID = channelUID.getGroupId();
if (channelGroupID == null) {
return 0;
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;
}
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);
}
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.");
}
@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) {
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();
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);
}
}
* @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>
*/
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;
}
return new DecimalType(getSizeInMB(bytesRecv));
}
+ @Override
+ public int getCurrentProcessID() {
+ return operatingSystem.getProcessId();
+ }
+
@Override
public @Nullable StringType getProcessName(int pid) throws DeviceNotFoundException {
if (pid > 0) {
}
@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;
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;
+ }
}
*
* @author Svilen Valkanov - Initial contribution
* @author Wouter Born - Add null annotations
+ * @author Mark Herwege - Add dynamic creation of extra channels
*/
@NonNullByDefault
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
*
* 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
* @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();
}
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
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
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
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)!
<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">
<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"/>
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)'
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;
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;
* 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";
*/
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() {
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) {
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)));
});
}
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 {
}
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;
}
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));
}
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")));
+ }
});
}
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() {
@Override
protected String getHostName() throws UnknownHostException {
- if (hostname.equals("unresolved")) {
+ if ("unresolved".equals(hostname)) {
throw new UnknownHostException();
}
return hostname;
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());
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)));
});
}
// 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);
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
.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));
});
}
}