2 * Copyright (c) 2010-2022 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.systeminfo.internal.handler;
15 import static org.openhab.binding.systeminfo.internal.SysteminfoBindingConstants.*;
17 import java.math.BigDecimal;
18 import java.util.HashSet;
19 import java.util.Iterator;
20 import java.util.List;
23 import java.util.concurrent.ScheduledFuture;
24 import java.util.concurrent.TimeUnit;
26 import org.eclipse.jdt.annotation.NonNullByDefault;
27 import org.eclipse.jdt.annotation.Nullable;
28 import org.openhab.binding.systeminfo.internal.model.DeviceNotFoundException;
29 import org.openhab.binding.systeminfo.internal.model.SysteminfoInterface;
30 import org.openhab.core.config.core.Configuration;
31 import org.openhab.core.library.types.PercentType;
32 import org.openhab.core.library.types.QuantityType;
33 import org.openhab.core.library.unit.Units;
34 import org.openhab.core.thing.Channel;
35 import org.openhab.core.thing.ChannelUID;
36 import org.openhab.core.thing.Thing;
37 import org.openhab.core.thing.ThingStatus;
38 import org.openhab.core.thing.ThingStatusDetail;
39 import org.openhab.core.thing.binding.BaseThingHandler;
40 import org.openhab.core.types.Command;
41 import org.openhab.core.types.RefreshType;
42 import org.openhab.core.types.State;
43 import org.openhab.core.types.UnDefType;
44 import org.slf4j.Logger;
45 import org.slf4j.LoggerFactory;
48 * The {@link SysteminfoHandler} is responsible for providing real time information about the system
49 * (CPU, Memory, Storage, Display and others).
51 * @author Svilen Valkanov - Initial contribution
52 * @author Lyubomir Papzov - Separate the creation of the systeminfo object and its initialization
53 * @author Wouter Born - Add null annotations
56 public class SysteminfoHandler extends BaseThingHandler {
58 * Refresh interval for {@link #highPriorityChannels} in seconds.
60 private @NonNullByDefault({}) BigDecimal refreshIntervalHighPriority;
63 * Refresh interval for {@link #mediumPriorityChannels} in seconds.
65 private @NonNullByDefault({}) BigDecimal refreshIntervalMediumPriority;
68 * Channels with priority configuration parameter set to High. They usually need frequent update of the state like
69 * CPU load, or information about the free and used memory.
70 * They are updated periodically at {@link #refreshIntervalHighPriority}.
72 private final Set<ChannelUID> highPriorityChannels = new HashSet<>();
75 * Channels with priority configuration parameter set to Medium. These channels usually need update of the
76 * state not so oft like battery capacity, storage used and etc.
77 * They are updated periodically at {@link #refreshIntervalMediumPriority}.
79 private final Set<ChannelUID> mediumPriorityChannels = new HashSet<>();
82 * Channels with priority configuration parameter set to Low. They represent static information or information
83 * that is updated rare- e.g. CPU name, storage name and etc.
84 * They are updated only at {@link #initialize()}.
86 private final Set<ChannelUID> lowPriorityChannels = new HashSet<>();
89 * Wait time for the creation of Item-Channel links in seconds. This delay is needed, because the Item-Channel
90 * links have to be created before the thing state is updated, otherwise item state will not be updated.
92 public static final int WAIT_TIME_CHANNEL_ITEM_LINK_INIT = 1;
94 private SysteminfoInterface systeminfo;
96 private @Nullable ScheduledFuture<?> highPriorityTasks;
97 private @Nullable ScheduledFuture<?> mediumPriorityTasks;
99 private Logger logger = LoggerFactory.getLogger(SysteminfoHandler.class);
101 public SysteminfoHandler(Thing thing, @Nullable SysteminfoInterface systeminfo) {
103 if (systeminfo != null) {
104 this.systeminfo = systeminfo;
106 throw new IllegalArgumentException("No systeminfo service was provided");
111 public void initialize() {
112 if (instantiateSysteminfoLibrary() && isConfigurationValid() && updateProperties()) {
113 groupChannelsByPriority();
115 updateStatus(ThingStatus.ONLINE);
117 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.HANDLER_INITIALIZING_ERROR,
118 "Thing cannot be initialized!");
122 private boolean instantiateSysteminfoLibrary() {
124 systeminfo.initializeSysteminfo();
125 logger.debug("Systeminfo implementation is instantiated!");
127 } catch (Exception e) {
128 logger.warn("Cannot instantiate Systeminfo object!", e);
133 private boolean isConfigurationValid() {
134 logger.debug("Start reading Thing configuration.");
136 refreshIntervalMediumPriority = (BigDecimal) this.thing.getConfiguration()
137 .get(MEDIUM_PRIORITY_REFRESH_TIME);
138 refreshIntervalHighPriority = (BigDecimal) this.thing.getConfiguration().get(HIGH_PRIORITY_REFRESH_TIME);
140 if (refreshIntervalHighPriority.intValue() <= 0 || refreshIntervalMediumPriority.intValue() <= 0) {
141 throw new IllegalArgumentException("Refresh time must be positive number!");
143 logger.debug("Refresh time for medium priority channels set to {} s", refreshIntervalMediumPriority);
144 logger.debug("Refresh time for high priority channels set to {} s", refreshIntervalHighPriority);
146 } catch (IllegalArgumentException e) {
147 logger.warn("Refresh time value is invalid! Please change the thing configuration!");
149 } catch (ClassCastException e) {
150 logger.debug("Channel configuration cannot be read!");
155 private boolean updateProperties() {
156 Map<String, String> properties = editProperties();
158 properties.put(PROPERTY_CPU_LOGICAL_CORES, systeminfo.getCpuLogicalCores().toString());
159 properties.put(PROPERTY_CPU_PHYSICAL_CORES, systeminfo.getCpuPhysicalCores().toString());
160 properties.put(PROPERTY_OS_FAMILY, systeminfo.getOsFamily().toString());
161 properties.put(PROPERTY_OS_MANUFACTURER, systeminfo.getOsManufacturer().toString());
162 properties.put(PROPERTY_OS_VERSION, systeminfo.getOsVersion().toString());
163 updateProperties(properties);
164 logger.debug("Properties updated!");
166 } catch (Exception e) {
167 logger.debug("Cannot get system properties! Please try to restart the binding.", e);
172 private void groupChannelsByPriority() {
173 logger.trace("Grouping channels by priority.");
174 List<Channel> channels = this.thing.getChannels();
176 for (Channel channel : channels) {
177 Configuration properties = channel.getConfiguration();
178 String priority = (String) properties.get(PRIOIRITY_PARAM);
179 if (priority == null) {
180 logger.debug("Channel with UID {} will not be updated. The channel has no priority set !",
186 highPriorityChannels.add(channel.getUID());
189 mediumPriorityChannels.add(channel.getUID());
192 lowPriorityChannels.add(channel.getUID());
195 logger.debug("Invalid priority configuration parameter. Channel will not be updated!");
200 private void changeChannelPriority(ChannelUID channelUID, String priority) {
203 mediumPriorityChannels.remove(channelUID);
204 lowPriorityChannels.remove(channelUID);
205 highPriorityChannels.add(channelUID);
208 lowPriorityChannels.remove(channelUID);
209 highPriorityChannels.remove(channelUID);
210 mediumPriorityChannels.add(channelUID);
213 highPriorityChannels.remove(channelUID);
214 mediumPriorityChannels.remove(channelUID);
215 lowPriorityChannels.add(channelUID);
218 logger.debug("Invalid priority configuration parameter. Channel will not be updated!");
222 private void scheduleUpdates() {
223 logger.debug("Schedule high priority tasks at fixed rate {} s.", refreshIntervalHighPriority);
224 highPriorityTasks = scheduler.scheduleWithFixedDelay(() -> {
225 publishData(highPriorityChannels);
226 }, WAIT_TIME_CHANNEL_ITEM_LINK_INIT, refreshIntervalHighPriority.intValue(), TimeUnit.SECONDS);
228 logger.debug("Schedule medium priority tasks at fixed rate {} s.", refreshIntervalMediumPriority);
229 mediumPriorityTasks = scheduler.scheduleWithFixedDelay(() -> {
230 publishData(mediumPriorityChannels);
231 }, WAIT_TIME_CHANNEL_ITEM_LINK_INIT, refreshIntervalMediumPriority.intValue(), TimeUnit.SECONDS);
233 logger.debug("Schedule one time update for low priority tasks.");
234 scheduler.schedule(() -> {
235 publishData(lowPriorityChannels);
236 }, WAIT_TIME_CHANNEL_ITEM_LINK_INIT, TimeUnit.SECONDS);
239 private void publishData(Set<ChannelUID> channels) {
240 Iterator<ChannelUID> iter = channels.iterator();
241 while (iter.hasNext()) {
242 ChannelUID channeUID = iter.next();
243 if (isLinked(channeUID.getId())) {
244 publishDataForChannel(channeUID);
249 private void publishDataForChannel(ChannelUID channelUID) {
250 State state = getInfoForChannel(channelUID);
251 String channelID = channelUID.getId();
252 updateState(channelID, state);
255 public Set<ChannelUID> getHighPriorityChannels() {
256 return highPriorityChannels;
259 public Set<ChannelUID> getMediumPriorityChannels() {
260 return mediumPriorityChannels;
263 public Set<ChannelUID> getLowPriorityChannels() {
264 return lowPriorityChannels;
268 * This method gets the information for specific channel through the {@link SysteminfoInterface}. It uses the
269 * channel ID to call the correct method from the {@link SysteminfoInterface} with deviceIndex parameter (in case of
270 * multiple devices, for reference see {@link #getDeviceIndex(String)}})
272 * @param channelUID the UID of the channel
273 * @return State object or null, if there is no information for the device with this index
275 private State getInfoForChannel(ChannelUID channelUID) {
278 String channelID = channelUID.getId();
279 String channelIDWithoutGroup = channelUID.getIdWithoutGroup();
280 String channelGroupID = channelUID.getGroupId();
282 int deviceIndex = getDeviceIndex(channelUID);
284 // The channelGroup may contain deviceIndex. It must be deleted from the channelID, because otherwise the
285 // switch will not find the correct method below.
286 // All digits are deleted from the ID
287 if (channelGroupID != null) {
288 channelID = channelGroupID.replaceAll("\\d+", "") + "#" + channelIDWithoutGroup;
293 case CHANNEL_MEMORY_HEAP_AVAILABLE:
294 state = new QuantityType<>(Runtime.getRuntime().freeMemory(), Units.BYTE);
296 case CHANNEL_MEMORY_USED_HEAP_PERCENT:
297 state = new QuantityType<>((Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory())
298 * 100 / Runtime.getRuntime().maxMemory(), Units.PERCENT);
300 case CHANNEL_DISPLAY_INFORMATION:
301 state = systeminfo.getDisplayInformation(deviceIndex);
303 case CHANNEL_BATTERY_NAME:
304 state = systeminfo.getBatteryName(deviceIndex);
306 case CHANNEL_BATTERY_REMAINING_CAPACITY:
307 state = systeminfo.getBatteryRemainingCapacity(deviceIndex);
309 case CHANNEL_BATTERY_REMAINING_TIME:
310 state = systeminfo.getBatteryRemainingTime(deviceIndex);
312 case CHANNEL_SENSORS_CPU_TEMPERATURE:
313 state = systeminfo.getSensorsCpuTemperature();
315 case CHANNEL_SENOSRS_CPU_VOLTAGE:
316 state = systeminfo.getSensorsCpuVoltage();
318 case CHANNEL_SENSORS_FAN_SPEED:
319 state = systeminfo.getSensorsFanSpeed(deviceIndex);
321 case CHANNEL_CPU_LOAD:
322 PercentType cpuLoad = systeminfo.getSystemCpuLoad();
323 state = (cpuLoad != null) ? new QuantityType<>(cpuLoad, Units.PERCENT) : null;
325 case CHANNEL_CPU_LOAD_1:
326 state = systeminfo.getCpuLoad1();
328 case CHANNEL_CPU_LOAD_5:
329 state = systeminfo.getCpuLoad5();
331 case CHANNEL_CPU_LOAD_15:
332 state = systeminfo.getCpuLoad15();
334 case CHANNEL_CPU_UPTIME:
335 state = systeminfo.getCpuUptime();
337 case CHANNEL_CPU_THREADS:
338 state = systeminfo.getCpuThreads();
340 case CHANNEL_CPU_DESCRIPTION:
341 state = systeminfo.getCpuDescription();
343 case CHANNEL_CPU_NAME:
344 state = systeminfo.getCpuName();
346 case CHANNEL_MEMORY_AVAILABLE:
347 state = systeminfo.getMemoryAvailable();
349 case CHANNEL_MEMORY_USED:
350 state = systeminfo.getMemoryUsed();
352 case CHANNEL_MEMORY_TOTAL:
353 state = systeminfo.getMemoryTotal();
355 case CHANNEL_MEMORY_AVAILABLE_PERCENT:
356 state = systeminfo.getMemoryAvailablePercent();
358 case CHANNEL_MEMORY_USED_PERCENT:
359 state = systeminfo.getMemoryUsedPercent();
361 case CHANNEL_SWAP_AVAILABLE:
362 state = systeminfo.getSwapAvailable();
364 case CHANNEL_SWAP_USED:
365 state = systeminfo.getSwapUsed();
367 case CHANNEL_SWAP_TOTAL:
368 state = systeminfo.getSwapTotal();
370 case CHANNEL_SWAP_AVAILABLE_PERCENT:
371 state = systeminfo.getSwapAvailablePercent();
373 case CHANNEL_SWAP_USED_PERCENT:
374 state = systeminfo.getSwapUsedPercent();
376 case CHANNEL_DRIVE_MODEL:
377 state = systeminfo.getDriveModel(deviceIndex);
379 case CHANNEL_DRIVE_SERIAL:
380 state = systeminfo.getDriveSerialNumber(deviceIndex);
382 case CHANNEL_DRIVE_NAME:
383 state = systeminfo.getDriveName(deviceIndex);
385 case CHANNEL_STORAGE_NAME:
386 state = systeminfo.getStorageName(deviceIndex);
388 case CHANNEL_STORAGE_DESCRIPTION:
389 state = systeminfo.getStorageDescription(deviceIndex);
391 case CHANNEL_STORAGE_AVAILABLE:
392 state = systeminfo.getStorageAvailable(deviceIndex);
394 case CHANNEL_STORAGE_USED:
395 state = systeminfo.getStorageUsed(deviceIndex);
397 case CHANNEL_STORAGE_TOTAL:
398 state = systeminfo.getStorageTotal(deviceIndex);
400 case CHANNEL_STORAGE_TYPE:
401 state = systeminfo.getStorageType(deviceIndex);
403 case CHANNEL_STORAGE_AVAILABLE_PERCENT:
404 state = systeminfo.getStorageAvailablePercent(deviceIndex);
406 case CHANNEL_STORAGE_USED_PERCENT:
407 state = systeminfo.getStorageUsedPercent(deviceIndex);
409 case CHANNEL_NETWORK_IP:
410 state = systeminfo.getNetworkIp(deviceIndex);
412 case CHANNEL_NETWORK_ADAPTER_NAME:
413 state = systeminfo.getNetworkDisplayName(deviceIndex);
415 case CHANNEL_NETWORK_NAME:
416 state = systeminfo.getNetworkName(deviceIndex);
418 case CHANNEL_NETWORK_MAC:
419 state = systeminfo.getNetworkMac(deviceIndex);
421 case CHANNEL_NETWORK_DATA_SENT:
422 state = systeminfo.getNetworkDataSent(deviceIndex);
424 case CHANNEL_NETWORK_DATA_RECEIVED:
425 state = systeminfo.getNetworkDataReceived(deviceIndex);
427 case CHANNEL_NETWORK_PACKETS_RECEIVED:
428 state = systeminfo.getNetworkPacketsReceived(deviceIndex);
430 case CHANNEL_NETWORK_PACKETS_SENT:
431 state = systeminfo.getNetworkPacketsSent(deviceIndex);
433 case CHANNEL_PROCESS_LOAD:
434 PercentType processLoad = systeminfo.getProcessCpuUsage(deviceIndex);
435 state = (processLoad != null) ? new QuantityType<>(processLoad, Units.PERCENT) : null;
437 case CHANNEL_PROCESS_MEMORY:
438 state = systeminfo.getProcessMemoryUsage(deviceIndex);
440 case CHANNEL_PROCESS_NAME:
441 state = systeminfo.getProcessName(deviceIndex);
443 case CHANNEL_PROCESS_PATH:
444 state = systeminfo.getProcessPath(deviceIndex);
446 case CHANNEL_PROCESS_THREADS:
447 state = systeminfo.getProcessThreads(deviceIndex);
450 logger.debug("Channel with unknown ID: {} !", channelID);
452 } catch (DeviceNotFoundException e) {
453 logger.warn("No information for channel {} with device index {} :", channelID, deviceIndex);
454 } catch (Exception e) {
455 logger.debug("Unexpected error occurred while getting system information!", e);
456 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
457 "Cannot get system info as result of unexpected error. Please try to restart the binding (remove and re-add the thing)!");
459 return state != null ? state : UnDefType.UNDEF;
463 * The device index is an optional part of the channelID - the last characters of the groupID. It is used to
464 * identify unique device, when more than one devices are available (e.g. local disks with names C:\, D:\, E"\ - the
465 * first will have deviceIndex=0, the second deviceIndex=1 ant etc).
466 * When no device index is specified, default value of 0 (first device in the list) is returned.
468 * @param channelID the ID of the channel
469 * @return natural number (number >=0)
471 private int getDeviceIndex(ChannelUID channelUID) {
472 String channelGroupID = channelUID.getGroupId();
473 if (channelGroupID == null) {
477 if (channelGroupID.contains(CHANNEL_GROUP_PROCESS)) {
478 // Only in this case the deviceIndex is part of the channel configuration - PID (Process Identifier)
479 int pid = getPID(channelUID);
480 logger.debug("Channel with UID {} tracks process with PID: {}", channelUID, pid);
484 char lastChar = channelGroupID.charAt(channelGroupID.length() - 1);
485 if (Character.isDigit(lastChar)) {
486 // All non-digits are deleted from the ID
487 String deviceIndexPart = channelGroupID.replaceAll("\\D+", "");
488 return Integer.parseInt(deviceIndexPart);
495 * This method gets the process identifier (PID) for specific process
497 * @param channelUID channel unique identifier
498 * @return natural number
500 private int getPID(ChannelUID channelUID) {
503 Channel channel = this.thing.getChannel(channelUID.getId());
504 if (channel != null) {
505 Configuration channelProperties = channel.getConfiguration();
506 BigDecimal pidValue = (BigDecimal) channelProperties.get(PID_PARAM);
507 if (pidValue == null || pidValue.intValue() < 0) {
508 throw new IllegalArgumentException("Invalid value for Process Identifier.");
510 pid = pidValue.intValue();
513 logger.debug("Channel does not exist ! Fall back to default value.");
515 } catch (ClassCastException e) {
516 logger.debug("Channel configuration cannot be read ! Fall back to default value.", e);
517 } catch (IllegalArgumentException e) {
518 logger.debug("PID (Process Identifier) must be positive number. Fall back to default value. ", e);
524 public void handleCommand(ChannelUID channelUID, Command command) {
525 if (thing.getStatus().equals(ThingStatus.ONLINE)) {
526 if (command instanceof RefreshType) {
527 logger.debug("Refresh command received for channel {}!", channelUID);
528 publishDataForChannel(channelUID);
530 logger.debug("Unsupported command {}! Supported commands: REFRESH", command);
533 logger.debug("Cannot handle command. Thing is not ONLINE.");
537 private boolean isConfigurationKeyChanged(Configuration currentConfig, Configuration newConfig, String key) {
538 Object currentValue = currentConfig.get(key);
539 Object newValue = newConfig.get(key);
541 if (currentValue == null) {
542 return (newValue != null);
545 return !currentValue.equals(newValue);
549 public void thingUpdated(Thing thing) {
550 logger.trace("About to update thing.");
551 boolean isChannelConfigChanged = false;
552 List<Channel> channels = thing.getChannels();
554 for (Channel channel : channels) {
555 ChannelUID channelUID = channel.getUID();
556 Configuration newChannelConfig = channel.getConfiguration();
557 Channel oldChannel = this.thing.getChannel(channelUID.getId());
559 if (oldChannel == null) {
560 logger.warn("Channel with UID {} cannot be updated, as it cannot be found !", channelUID);
563 Configuration currentChannelConfig = oldChannel.getConfiguration();
565 if (isConfigurationKeyChanged(currentChannelConfig, newChannelConfig, PRIOIRITY_PARAM)) {
566 isChannelConfigChanged = true;
568 handleChannelConfigurationChange(oldChannel, newChannelConfig, PRIOIRITY_PARAM);
570 String newPriority = (String) newChannelConfig.get(PRIOIRITY_PARAM);
571 changeChannelPriority(channelUID, newPriority);
574 if (isConfigurationKeyChanged(currentChannelConfig, newChannelConfig, PID_PARAM)) {
575 isChannelConfigChanged = true;
576 handleChannelConfigurationChange(oldChannel, newChannelConfig, PID_PARAM);
580 if (!(isInitialized() && isChannelConfigChanged)) {
581 super.thingUpdated(thing);
585 private void handleChannelConfigurationChange(Channel channel, Configuration newConfig, String parameter) {
586 Configuration configuration = channel.getConfiguration();
587 Object oldValue = configuration.get(parameter);
589 configuration.put(parameter, newConfig.get(parameter));
591 Object newValue = newConfig.get(parameter);
592 logger.debug("Channel with UID {} has changed its {} from {} to {}", channel.getUID(), parameter, oldValue,
594 publishDataForChannel(channel.getUID());
597 private void stopScheduledUpdates() {
598 ScheduledFuture<?> localHighPriorityTasks = highPriorityTasks;
599 if (localHighPriorityTasks != null) {
600 logger.debug("High prioriy tasks will not be run anymore !");
601 localHighPriorityTasks.cancel(true);
604 ScheduledFuture<?> localMediumPriorityTasks = mediumPriorityTasks;
605 if (localMediumPriorityTasks != null) {
606 logger.debug("Medium prioriy tasks will not be run anymore !");
607 localMediumPriorityTasks.cancel(true);
612 public void dispose() {
613 stopScheduledUpdates();