diff --git a/bundles/org.openhab.binding.systeminfo/README.md b/bundles/org.openhab.binding.systeminfo/README.md index 197482b83..500c63131 100644 --- a/bundles/org.openhab.binding.systeminfo/README.md +++ b/bundles/org.openhab.binding.systeminfo/README.md @@ -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" { chann Number Sensor_CPUVoltage "CPU Voltage" { channel="systeminfo:computer:work:sensors#cpuVoltage" } Number Sensor_FanSpeed "Fan speed" { channel="systeminfo:computer:work:sensors#fanSpeed" } +/* Current process information*/ +Number Current_process_load "Load" { channel="systeminfo:computer:work:currentProcess#load" } +Number Current_process_used "Used" { channel="systeminfo:computer:work:currentProcess#used" } +String Current_process_name "Name" { channel="systeminfo:computer:work:currentProcess#name" } +Number Current_process_threads "Threads" { channel="systeminfo:computer:work:currentProcess#threads" } +String Current_process_path "Path" { channel="systeminfo:computer:work:currentProcess#path" } + /* Process information*/ Number Process_load "Load" { channel="systeminfo:computer:work:process#load" } Number Process_used "Used" { 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 diff --git a/bundles/org.openhab.binding.systeminfo/src/main/java/org/openhab/binding/systeminfo/internal/SysteminfoBindingConstants.java b/bundles/org.openhab.binding.systeminfo/src/main/java/org/openhab/binding/systeminfo/internal/SysteminfoBindingConstants.java index d88327fe3..941271625 100644 --- a/bundles/org.openhab.binding.systeminfo/src/main/java/org/openhab/binding/systeminfo/internal/SysteminfoBindingConstants.java +++ b/bundles/org.openhab.binding.systeminfo/src/main/java/org/openhab/binding/systeminfo/internal/SysteminfoBindingConstants.java @@ -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 */ diff --git a/bundles/org.openhab.binding.systeminfo/src/main/java/org/openhab/binding/systeminfo/internal/SysteminfoHandlerFactory.java b/bundles/org.openhab.binding.systeminfo/src/main/java/org/openhab/binding/systeminfo/internal/SysteminfoHandlerFactory.java index efc75acc5..6b88bff8e 100644 --- a/bundles/org.openhab.binding.systeminfo/src/main/java/org/openhab/binding/systeminfo/internal/SysteminfoHandlerFactory.java +++ b/bundles/org.openhab.binding.systeminfo/src/main/java/org/openhab/binding/systeminfo/internal/SysteminfoHandlerFactory.java @@ -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 SUPPORTED_THING_TYPES_UIDS = Collections.singleton(THING_TYPE_COMPUTER); - private @NonNullByDefault({}) SysteminfoInterface systeminfo; + private @NonNullByDefault({}) SysteminfoThingTypeProvider thingTypeProvider; @Override public boolean supportsThingType(ThingTypeUID thingTypeUID) { - return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID); + return BINDING_ID.equals(thingTypeUID.getBindingId()) + && thingTypeUID.getId().startsWith(THING_TYPE_COMPUTER_ID); } @Override protected @Nullable ThingHandler createHandler(Thing thing) { ThingTypeUID thingTypeUID = thing.getThingTypeUID(); - - if (thingTypeUID.equals(THING_TYPE_COMPUTER)) { - return new SysteminfoHandler(thing, systeminfo); + if (supportsThingType(thingTypeUID)) { + String extString = "-" + thing.getUID().getId(); + ThingTypeUID extThingTypeUID = new ThingTypeUID(BINDING_ID, THING_TYPE_COMPUTER_ID + extString); + if (thingTypeProvider.getThingType(extThingTypeUID, null) == null) { + thingTypeProvider.createThingType(extThingTypeUID); + thingTypeProvider.storeChannelsConfig(thing); // Save the current channels configs, will be restored + // after thing type change. + } + return new SysteminfoHandler(thing, thingTypeProvider, systeminfo); } - return null; } @@ -69,4 +71,13 @@ public class SysteminfoHandlerFactory extends BaseThingHandlerFactory { public void unbindSystemInfo(SysteminfoInterface systeminfo) { this.systeminfo = null; } + + @Reference + public void setSysteminfoThingTypeProvider(SysteminfoThingTypeProvider thingTypeProvider) { + this.thingTypeProvider = thingTypeProvider; + } + + public void unsetSysteminfoThingTypeProvider(SysteminfoThingTypeProvider thingTypeProvider) { + this.thingTypeProvider = null; + } } diff --git a/bundles/org.openhab.binding.systeminfo/src/main/java/org/openhab/binding/systeminfo/internal/SysteminfoThingTypeProvider.java b/bundles/org.openhab.binding.systeminfo/src/main/java/org/openhab/binding/systeminfo/internal/SysteminfoThingTypeProvider.java new file mode 100644 index 000000000..61cd3aad7 --- /dev/null +++ b/bundles/org.openhab.binding.systeminfo/src/main/java/org/openhab/binding/systeminfo/internal/SysteminfoThingTypeProvider.java @@ -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 thingTypes = new HashMap<>(); + + private final Map> 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 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 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 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 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 restoreChannelsConfig(ThingUID UID) { + Map configs = thingChannelsConfig.remove(UID); + return configs != null ? configs : Collections.emptyMap(); + } +} diff --git a/bundles/org.openhab.binding.systeminfo/src/main/java/org/openhab/binding/systeminfo/internal/handler/SysteminfoHandler.java b/bundles/org.openhab.binding.systeminfo/src/main/java/org/openhab/binding/systeminfo/internal/handler/SysteminfoHandler.java index 50be4a14c..3b8efb316 100644 --- a/bundles/org.openhab.binding.systeminfo/src/main/java/org/openhab/binding/systeminfo/internal/handler/SysteminfoHandler.java +++ b/bundles/org.openhab.binding.systeminfo/src/main/java/org/openhab/binding/systeminfo/internal/handler/SysteminfoHandler.java @@ -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 cpuLoadCache = new ExpiringCache<>(MIN_PROCESS_LOAD_REFRESH_INTERVAL_MS, + () -> getSystemCpuLoad()); + private ExpiringCacheMap processLoadCache = new ExpiringCacheMap<>( + MIN_PROCESS_LOAD_REFRESH_INTERVAL_MS); - public SysteminfoHandler(Thing thing, @Nullable SysteminfoInterface systeminfo) { + private final Logger logger = LoggerFactory.getLogger(SysteminfoHandler.class); + + public SysteminfoHandler(Thing thing, SysteminfoThingTypeProvider thingTypeProvider, + SysteminfoInterface systeminfo) { super(thing); - if (systeminfo != null) { - this.systeminfo = systeminfo; - } else { - throw new IllegalArgumentException("No systeminfo service was provided"); - } + this.thingTypeProvider = thingTypeProvider; + this.systeminfo = systeminfo; + + idExtString = "-" + thing.getUID().getId(); } @Override public void initialize() { + logger.trace("Initializing thing {} with thing type {}", thing.getUID().getId(), + thing.getThingTypeUID().getId()); + restoreChannelsConfig(); // After a thing type change, previous channel configs will have been stored, and will + // be restored here. if (instantiateSysteminfoLibrary() && isConfigurationValid() && updateProperties()) { - groupChannelsByPriority(); - scheduleUpdates(); - updateStatus(ThingStatus.ONLINE); + if (!addDynamicChannels()) { // If there are new channel groups, the thing will get recreated with a new + // thing type and this handler will be disposed. Therefore do not do anything + // further here. + groupChannelsByPriority(); + scheduleUpdates(); + updateStatus(ThingStatus.ONLINE); + } } else { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.HANDLER_INITIALIZING_ERROR, - "Thing cannot be initialized!"); + "@text/offline.cannot-initialize"); } } @@ -169,15 +209,129 @@ public class SysteminfoHandler extends BaseThingHandler { } } + /** + * Retrieve info on available storages, drives, displays, batteries, network interfaces and fans in the system. If + * there is more than 1, create additional channel groups and channels representing each of the entities with an + * index added to the channel groups and channels. The base channel groups and channels will remain without index + * and are equal to the channel groups and channels with index 0. If there is only one entity in a group, do not add + * a channels group and channels with index 0. + *

+ * 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 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 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 createChannelGroups(ThingUID thingUID, String channelGroupID, + String channelGroupTypeID, int count) { + if (count <= 1) { + return Collections.emptyList(); + } + + List channelGroups = thingTypeProvider.getChannelGroupDefinitions(thing.getThingTypeUID()).stream() + .map(ChannelGroupDefinition::getId).collect(Collectors.toList()); + + List 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 createChannels(ThingUID thingUID, String channelID, int count) { + if (count <= 1) { + return Collections.emptyList(); + } + + List 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 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 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 channels) { + // if handler disposed while waiting for the links, don't update the channel states + if (!ThingStatus.ONLINE.equals(thing.getStatus())) { + return; + } Iterator 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 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); } } diff --git a/bundles/org.openhab.binding.systeminfo/src/main/java/org/openhab/binding/systeminfo/internal/model/OSHISysteminfo.java b/bundles/org.openhab.binding.systeminfo/src/main/java/org/openhab/binding/systeminfo/internal/model/OSHISysteminfo.java index 5ab70b3ad..4c4b3903c 100644 --- a/bundles/org.openhab.binding.systeminfo/src/main/java/org/openhab/binding/systeminfo/internal/model/OSHISysteminfo.java +++ b/bundles/org.openhab.binding.systeminfo/src/main/java/org/openhab/binding/systeminfo/internal/model/OSHISysteminfo.java @@ -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 OSHI GitHub repository */ @@ -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; + } } diff --git a/bundles/org.openhab.binding.systeminfo/src/main/java/org/openhab/binding/systeminfo/internal/model/SysteminfoInterface.java b/bundles/org.openhab.binding.systeminfo/src/main/java/org/openhab/binding/systeminfo/internal/model/SysteminfoInterface.java index a606f7a26..87a7c9761 100644 --- a/bundles/org.openhab.binding.systeminfo/src/main/java/org/openhab/binding/systeminfo/internal/model/SysteminfoInterface.java +++ b/bundles/org.openhab.binding.systeminfo/src/main/java/org/openhab/binding/systeminfo/internal/model/SysteminfoInterface.java @@ -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(); } diff --git a/bundles/org.openhab.binding.systeminfo/src/main/resources/OH-INF/i18n/systeminfo.properties b/bundles/org.openhab.binding.systeminfo/src/main/resources/OH-INF/i18n/systeminfo.properties index ebb3eded3..289ef8f0d 100644 --- a/bundles/org.openhab.binding.systeminfo/src/main/resources/OH-INF/i18n/systeminfo.properties +++ b/bundles/org.openhab.binding.systeminfo/src/main/resources/OH-INF/i18n/systeminfo.properties @@ -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)! diff --git a/bundles/org.openhab.binding.systeminfo/src/main/resources/OH-INF/thing/channels.xml b/bundles/org.openhab.binding.systeminfo/src/main/resources/OH-INF/thing/channels.xml index 16ddabce8..5add60584 100644 --- a/bundles/org.openhab.binding.systeminfo/src/main/resources/OH-INF/thing/channels.xml +++ b/bundles/org.openhab.binding.systeminfo/src/main/resources/OH-INF/thing/channels.xml @@ -107,7 +107,7 @@ - + @@ -116,6 +116,18 @@ + + + Current process information + + + + + + + + + System process information @@ -144,6 +156,14 @@ + + String + + The full path + + + + String @@ -288,6 +308,14 @@ + + Number:Dimensionless + + Load in percent + + + + Number:Dimensionless @@ -296,14 +324,6 @@ - - Number:Dimensionless - - CPU load in percent - - - - Number diff --git a/bundles/org.openhab.binding.systeminfo/src/main/resources/OH-INF/thing/computer.xml b/bundles/org.openhab.binding.systeminfo/src/main/resources/OH-INF/thing/computer.xml index ef680220d..31c543de7 100644 --- a/bundles/org.openhab.binding.systeminfo/src/main/resources/OH-INF/thing/computer.xml +++ b/bundles/org.openhab.binding.systeminfo/src/main/resources/OH-INF/thing/computer.xml @@ -16,7 +16,7 @@ - + diff --git a/itests/org.openhab.binding.systeminfo.tests/itest.bndrun b/itests/org.openhab.binding.systeminfo.tests/itest.bndrun index 146b3bc64..022b2e258 100644 --- a/itests/org.openhab.binding.systeminfo.tests/itest.bndrun +++ b/itests/org.openhab.binding.systeminfo.tests/itest.bndrun @@ -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)' diff --git a/itests/org.openhab.binding.systeminfo.tests/src/main/java/org/openhab/binding/systeminfo/test/SysteminfoOSGiTest.java b/itests/org.openhab.binding.systeminfo.tests/src/main/java/org/openhab/binding/systeminfo/test/SysteminfoOSGiTest.java index 24d908036..57b78ead6 100644 --- a/itests/org.openhab.binding.systeminfo.tests/src/main/java/org/openhab/binding/systeminfo/test/SysteminfoOSGiTest.java +++ b/itests/org.openhab.binding.systeminfo.tests/src/main/java/org/openhab/binding/systeminfo/test/SysteminfoOSGiTest.java @@ -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); - systeminfoHandlerFactory = getService(ThingHandlerFactory.class, SysteminfoHandlerFactory.class); - SysteminfoInterface oshiSystemInfo = getService(SysteminfoInterface.class); + registerService(mockedSystemInfo); - // 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); + waitForAssert(() -> { + systeminfoHandlerFactory = getService(ThingHandlerFactory.class, SysteminfoHandlerFactory.class); + assertThat(systeminfoHandlerFactory, is(notNullValue())); + }); + if (systeminfoHandlerFactory != null) { + // Unbind oshiSystemInfo service and bind the mock service to make the systeminfo binding tests independent + // of the external OSHI library + SysteminfoInterface oshiSystemInfo = getService(SysteminfoInterface.class); + if (oshiSystemInfo != null) { + systeminfoHandlerFactory.unbindSystemInfo(oshiSystemInfo); + } + systeminfoHandlerFactory.bindSystemInfo(mockedSystemInfo); } - systeminfoHandlerFactory.bindSystemInfo(mockedSystemInfo); - managedThingProvider = getService(ThingProvider.class, ManagedThingProvider.class); - assertThat(managedThingProvider, is(notNullValue())); + waitForAssert(() -> { + ThingTypeProvider thingTypeProvider = getService(ThingTypeProvider.class, + SysteminfoThingTypeProvider.class); + assertThat(thingTypeProvider, is(notNullValue())); + }); + waitForAssert(() -> { + SysteminfoThingTypeProvider systeminfoThingTypeProvider = getService(SysteminfoThingTypeProvider.class); + assertThat(systeminfoThingTypeProvider, is(notNullValue())); + }); - thingRegistry = getService(ThingRegistry.class); - assertThat(thingRegistry, is(notNullValue())); + waitForAssert(() -> { + thingRegistry = getService(ThingRegistry.class); + assertThat(thingRegistry, is(notNullValue())); + }); - itemRegistry = getService(ItemRegistry.class); - assertThat(itemRegistry, is(notNullValue())); + waitForAssert(() -> { + itemRegistry = getService(ItemRegistry.class); + assertThat(itemRegistry, is(notNullValue())); + }); } @AfterEach public void tearDown() { - if (systemInfoThing != null) { + Thing thing = systemInfoThing; + if (thing != null) { // Remove the systeminfo thing. The handler will be also disposed automatically - Thing removedThing = thingRegistry.forceRemove(systemInfoThing.getUID()); + Thing removedThing = thingRegistry.forceRemove(thing.getUID()); assertThat("The systeminfo thing cannot be deleted", removedThing, is(notNullValue())); + waitForAssert(() -> { + ThingHandler systemInfoHandler = thing.getHandler(); + assertThat(systemInfoHandler, is(nullValue())); + }); } - waitForAssert(() -> { - ThingHandler systemInfoHandler = systemInfoThing.getHandler(); - assertThat(systemInfoHandler, is(nullValue())); - }); if (testItem != null) { itemRegistry.remove(DEFAULT_TEST_ITEM_NAME); } + + unregisterService(mockedSystemInfo); } private void initializeThingWithChannelAndPID(String channelID, String acceptedItemType, int pid) { @@ -214,18 +248,24 @@ public class SysteminfoOSGiTest extends JavaOSGiTest { Channel channel = ChannelBuilder.create(channelUID, acceptedItemType).withType(channelTypeUID) .withKind(ChannelKind.STATE).withConfiguration(channelConfig).build(); - systemInfoThing = ThingBuilder.create(thingTypeUID, thingUID).withConfiguration(thingConfiguration) + Thing thing = ThingBuilder.create(thingTypeUID, thingUID).withConfiguration(thingConfiguration) .withChannel(channel).build(); + systemInfoThing = thing; - managedThingProvider.add(systemInfoThing); + ManagedThingProvider managedThingProvider = getService(ThingProvider.class, ManagedThingProvider.class); + assertThat(managedThingProvider, is(notNullValue())); + + if (managedThingProvider != null) { + managedThingProvider.add(thing); + } waitForAssert(() -> { - systemInfoHandler = (SysteminfoHandler) systemInfoThing.getHandler(); - assertThat(systemInfoHandler, is(notNullValue())); + SysteminfoHandler handler = (SysteminfoHandler) thing.getHandler(); + assertThat(handler, is(notNullValue())); }); waitForAssert(() -> { - assertThat("Thing is not initilized, before an Item is created", systemInfoThing.getStatus(), + assertThat("Thing is not initialized, before an Item is created", thing.getStatus(), anyOf(equalTo(ThingStatus.OFFLINE), equalTo(ThingStatus.ONLINE))); }); @@ -233,11 +273,15 @@ public class SysteminfoOSGiTest extends JavaOSGiTest { } private void assertItemState(String acceptedItemType, String itemName, String priority, State expectedState) { + Thing thing = systemInfoThing; + if (thing == null) { + throw new AssertionError("Thing is null"); + } waitForAssert(() -> { - ThingStatusDetail thingStatusDetail = systemInfoThing.getStatusInfo().getStatusDetail(); - String description = systemInfoThing.getStatusInfo().getDescription(); + ThingStatusDetail thingStatusDetail = thing.getStatusInfo().getStatusDetail(); + String description = thing.getStatusInfo().getDescription(); assertThat("Thing status detail is " + thingStatusDetail + " with description " + description, - systemInfoThing.getStatus(), is(equalTo(ThingStatus.ONLINE))); + thing.getStatus(), is(equalTo(ThingStatus.ONLINE))); }); // The binding starts all refresh tasks in SysteminfoHandler.scheduleUpdates() after this delay ! try { @@ -254,9 +298,9 @@ public class SysteminfoOSGiTest extends JavaOSGiTest { } int waitTime; - if (priority.equals("High")) { + if ("High".equals(priority)) { waitTime = DEFAULT_TEST_INTERVAL_HIGH * 1000; - } else if (priority.equals("Medium")) { + } else if ("Medium".equals(priority)) { waitTime = DEFAULT_TEST_INTERVAL_MEDIUM * 1000; } else { waitTime = 100; @@ -269,16 +313,25 @@ public class SysteminfoOSGiTest extends JavaOSGiTest { } private void intializeItem(ChannelUID channelUID, String itemName, String acceptedItemType) { - if (acceptedItemType.equals("Number")) { - testItem = new NumberItem(itemName); - } else if (acceptedItemType.equals("String")) { - testItem = new StringItem(itemName); + GenericItem item = null; + if ("Number".equals(acceptedItemType)) { + item = new NumberItem(itemName); + } else if ("String".equals(acceptedItemType)) { + item = new StringItem(itemName); } - 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(), - is(equalTo(ThingStatusDetail.HANDLER_INITIALIZING_ERROR))); - assertThat(systemInfoThing.getStatusInfo().getDescription(), is(equalTo("Thing cannot be initialized!"))); - }); - } - - @Test - public void assertThingStatusIsUninitializedWhenThereIsNoSysteminfoServiceProvided() { - // Unbind the mock service to verify the systeminfo thing will not be initialized when no systeminfo service is - // provided - systeminfoHandlerFactory.unbindSystemInfo(mockedSystemInfo); - - ThingTypeUID thingTypeUID = SysteminfoBindingConstants.THING_TYPE_COMPUTER; - ThingUID thingUID = new ThingUID(thingTypeUID, DEFAULT_TEST_THING_NAME); - - systemInfoThing = ThingBuilder.create(thingTypeUID, thingUID).build(); - managedThingProvider.add(systemInfoThing); - - waitForAssert(() -> { - assertThat("The thing status is uninitialized when systeminfo service is missing", - systemInfoThing.getStatus(), equalTo(ThingStatus.UNINITIALIZED)); + Thing thing = systemInfoThing; + if (thing != null) { + assertThat("Invalid configuration is used !", thing.getStatus(), is(equalTo(ThingStatus.OFFLINE))); + assertThat(thing.getStatusInfo().getStatusDetail(), + is(equalTo(ThingStatusDetail.HANDLER_INITIALIZING_ERROR))); + assertThat(thing.getStatusInfo().getDescription(), is(equalTo("@text/offline.cannot-initialize"))); + } }); } @@ -672,7 +709,7 @@ public class SysteminfoOSGiTest extends JavaOSGiTest { mockedDriveSerialNumber); } - @Disabled + // Re-enable this previously disabled test, as it is not relying on hardware anymore, but a mocked object // There is a bug opened for this issue - https://github.com/dblock/oshi/issues/185 @Test public void assertChannelSensorsCpuTempIsUpdated() { @@ -874,7 +911,7 @@ public class SysteminfoOSGiTest extends JavaOSGiTest { @Override protected String getHostName() throws UnknownHostException { - if (hostname.equals("unresolved")) { + if ("unresolved".equals(hostname)) { throw new UnknownHostException(); } return hostname; @@ -938,6 +975,10 @@ public class SysteminfoOSGiTest extends JavaOSGiTest { Inbox inbox = getService(Inbox.class); assertThat(inbox, is(notNullValue())); + if (inbox == null) { + return; + } + waitForAssert(() -> { List 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)); }); } }