[systeminfo] dynamic channels (#13562)

* 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>
This commit is contained in:
Mark Herwege 2022-11-04 13:28:27 +01:00 committed by GitHub
parent a4f6159f09
commit cf2a1afd56
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 975 additions and 160 deletions

View 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

View 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
*/

View File

@ -12,10 +12,7 @@
*/
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;
}
}

View File

@ -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();
}
}

View 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.thingTypeProvider = thingTypeProvider;
this.systeminfo = systeminfo;
} else {
throw new IllegalArgumentException("No systeminfo service was provided");
}
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()) {
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);
}
}

View 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;
}
}

View 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();
}

View 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)!

View File

@ -107,7 +107,7 @@
<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"/>
@ -116,6 +116,18 @@
</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>
@ -144,6 +156,14 @@
<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>
@ -288,6 +308,14 @@
<config-description-ref uri="channel-type:systeminfo:mediumpriority"/>
</channel-type>
<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"/>
</channel-type>
<channel-type id="load_process">
<item-type>Number:Dimensionless</item-type>
<label>Load</label>
@ -296,14 +324,6 @@
<config-description-ref uri="channel-type:systeminfo:highpriority_process"/>
</channel-type>
<channel-type id="cpuLoad">
<item-type>Number:Dimensionless</item-type>
<label>CPU Load</label>
<description>CPU load in percent</description>
<state readOnly="true" pattern="%.1f %%"/>
<config-description-ref uri="channel-type:systeminfo:highpriority"/>
</channel-type>
<channel-type id="loadAverage" advanced="true">
<item-type>Number</item-type>
<label>Load Average</label>

View 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"/>

View 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)'

View 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"));
// 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);
// 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);
}
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()));
});
waitForAssert(() -> {
thingRegistry = getService(ThingRegistry.class);
assertThat(thingRegistry, 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 = systemInfoThing.getHandler();
ThingHandler systemInfoHandler = thing.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);
}
itemRegistry.add(testItem);
if (item == null) {
throw new AssertionError("Item is null");
}
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(),
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(systemInfoThing.getStatusInfo().getDescription(), is(equalTo("Thing cannot be initialized!")));
});
assertThat(thing.getStatusInfo().getDescription(), is(equalTo("@text/offline.cannot-initialize")));
}
@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));
});
}
@ -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));
});
}
}