diff --git a/platypush/backend/http/webapp/src/components/panels/Entities/Disk.vue b/platypush/backend/http/webapp/src/components/panels/Entities/Disk.vue
new file mode 100644
index 000000000..58c0eca02
--- /dev/null
+++ b/platypush/backend/http/webapp/src/components/panels/Entities/Disk.vue
@@ -0,0 +1,225 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/platypush/backend/http/webapp/src/components/panels/Entities/meta.json b/platypush/backend/http/webapp/src/components/panels/Entities/meta.json
index 49e45a605..436a88520 100644
--- a/platypush/backend/http/webapp/src/components/panels/Entities/meta.json
+++ b/platypush/backend/http/webapp/src/components/panels/Entities/meta.json
@@ -63,6 +63,14 @@
}
},
+ "disk": {
+ "name": "System",
+ "name_plural": "System",
+ "icon": {
+ "class": "fas fa-hard-drive"
+ }
+ },
+
"current_sensor": {
"name": "Sensor",
"name_plural": "Sensors",
diff --git a/platypush/entities/system.py b/platypush/entities/system.py
index cb9fbc1ff..dd9d4040b 100644
--- a/platypush/entities/system.py
+++ b/platypush/entities/system.py
@@ -142,3 +142,36 @@ if 'swap_stats' not in Base.metadata:
__mapper_args__ = {
'polymorphic_identity': __tablename__,
}
+
+
+if 'disk' not in Base.metadata:
+
+ class Disk(Entity):
+ """
+ ``Disk`` ORM model.
+ """
+
+ __tablename__ = 'disk'
+
+ id = Column(
+ Integer, ForeignKey(Entity.id, ondelete='CASCADE'), primary_key=True
+ )
+
+ mountpoint = Column(String)
+ fstype = Column(String)
+ opts = Column(String)
+ total = Column(Integer)
+ used = Column(Integer)
+ free = Column(Integer)
+ percent = Column(Float)
+ read_count = Column(Integer)
+ write_count = Column(Integer)
+ read_bytes = Column(Integer)
+ write_bytes = Column(Integer)
+ read_time = Column(Float)
+ write_time = Column(Float)
+ busy_time = Column(Float)
+
+ __mapper_args__ = {
+ 'polymorphic_identity': __tablename__,
+ }
diff --git a/platypush/message/response/system/__init__.py b/platypush/message/response/system/__init__.py
index ac9b4157a..b3fe3538d 100644
--- a/platypush/message/response/system/__init__.py
+++ b/platypush/message/response/system/__init__.py
@@ -28,86 +28,6 @@ class SensorResponse(SystemResponse):
pass
-class DiskPartitionResponse(DiskResponse):
- def __init__(
- self,
- device: str,
- mount_point: str,
- fstype: Optional[str] = None,
- opts: Optional[str] = None,
- *args,
- **kwargs
- ):
- super().__init__(
- *args,
- output={
- 'device': device,
- 'mount_point': mount_point,
- 'fstype': fstype,
- 'opts': opts,
- },
- **kwargs
- )
-
-
-class DiskUsageResponse(DiskResponse):
- def __init__(
- self,
- path: str,
- total: int,
- used: int,
- free: int,
- percent: float,
- *args,
- **kwargs
- ):
- super().__init__(
- *args,
- output={
- 'path': path,
- 'total': total,
- 'used': used,
- 'free': free,
- 'percent': percent,
- },
- **kwargs
- )
-
-
-class DiskIoCountersResponse(DiskResponse):
- def __init__(
- self,
- read_count: int,
- write_count: int,
- read_bytes: int,
- write_bytes: int,
- read_time: int,
- write_time: int,
- read_merged_count: int,
- write_merged_count: int,
- busy_time: int,
- disk: Optional[str] = None,
- *args,
- **kwargs
- ):
- super().__init__(
- *args,
- output={
- 'read_count': read_count,
- 'write_count': write_count,
- 'read_bytes': read_bytes,
- 'write_bytes': write_bytes,
- 'read_time': read_time,
- 'write_time': write_time,
- 'read_merged_count': read_merged_count,
- 'write_merged_count': write_merged_count,
- 'busy_time': busy_time,
- 'disk': disk,
- },
- **kwargs
- )
-
-
class NetworkIoCountersResponse(NetworkResponse):
def __init__(
self,
@@ -363,11 +283,6 @@ class SystemResponseList(SystemResponse):
super().__init__(output=[r.output for r in responses], *args, **kwargs)
-class DiskResponseList(DiskResponse, SystemResponseList):
- def __init__(self, responses: List[DiskResponse], *args, **kwargs):
- super().__init__(responses=responses, *args, **kwargs)
-
-
class NetworkResponseList(NetworkResponse, SystemResponseList):
def __init__(self, responses: List[NetworkResponse], *args, **kwargs):
super().__init__(responses=responses, *args, **kwargs)
diff --git a/platypush/plugins/system/__init__.py b/platypush/plugins/system/__init__.py
index 1031a6332..3e0d2b4d1 100644
--- a/platypush/plugins/system/__init__.py
+++ b/platypush/plugins/system/__init__.py
@@ -1,9 +1,12 @@
+import os
import socket
from datetime import datetime
from typing import Tuple, Union, List, Optional, Dict
from typing_extensions import override
+import psutil
+
from platypush.entities import Entity
from platypush.entities.devices import Device
from platypush.entities.managers import EntityManager
@@ -13,14 +16,11 @@ from platypush.entities.system import (
CpuInfo as CpuInfoModel,
CpuStats as CpuStatsModel,
CpuTimes as CpuTimesModel,
+ Disk as DiskModel,
MemoryStats as MemoryStatsModel,
SwapStats as SwapStatsModel,
)
from platypush.message.response.system import (
- DiskResponseList,
- DiskPartitionResponse,
- DiskUsageResponse,
- DiskIoCountersResponse,
NetworkIoCountersResponse,
NetworkResponseList,
NetworkConnectionResponse,
@@ -46,6 +46,8 @@ from platypush.schemas.system import (
CpuStatsSchema,
CpuTimes,
CpuTimesSchema,
+ Disk,
+ DiskSchema,
MemoryStats,
MemoryStatsSchema,
SwapStats,
@@ -95,16 +97,12 @@ class SystemPlugin(SensorPlugin, EntityManager):
@classmethod
def _cpu_times_avg(cls, percent=True) -> CpuTimes:
- import psutil
-
method = psutil.cpu_times_percent if percent else psutil.cpu_times
times = method(percpu=False)
return cls._load_cpu_times(times, many=False) # type: ignore
@classmethod
def _cpu_times_per_cpu(cls, percent=True) -> List[CpuTimes]:
- import psutil
-
method = psutil.cpu_times_percent if percent else psutil.cpu_times
times = method(percpu=True)
return cls._load_cpu_times(times, many=True) # type: ignore
@@ -143,8 +141,6 @@ class SystemPlugin(SensorPlugin, EntityManager):
function be called with at least 0.1 seconds between calls.
:return: float if ``per_cpu=False``, ``list[float]`` otherwise.
"""
- import psutil
-
percent = psutil.cpu_percent(percpu=per_cpu, interval=interval)
if per_cpu:
@@ -152,8 +148,6 @@ class SystemPlugin(SensorPlugin, EntityManager):
return percent
def _cpu_stats(self) -> CpuStats:
- import psutil
-
stats = psutil.cpu_stats()
return CpuStatsSchema().load(stats._asdict()) # type: ignore
@@ -167,14 +161,10 @@ class SystemPlugin(SensorPlugin, EntityManager):
return CpuStatsSchema().dump(self._cpu_stats()) # type: ignore
def _cpu_frequency_avg(self) -> CpuFrequency:
- import psutil
-
freq = psutil.cpu_freq(percpu=False)
return CpuFrequencySchema().load(freq._asdict()) # type: ignore
def _cpu_frequency_per_cpu(self) -> List[CpuFrequency]:
- import psutil
-
freq = psutil.cpu_freq(percpu=True)
return CpuFrequencySchema().load(freq._asdict(), many=True) # type: ignore
@@ -203,13 +193,9 @@ class SystemPlugin(SensorPlugin, EntityManager):
"""
Get the average load as a vector that represents the load within the last 1, 5 and 15 minutes.
"""
- import psutil
-
return psutil.getloadavg()
def _mem_virtual(self) -> MemoryStats:
- import psutil
-
return MemoryStatsSchema().load(
psutil.virtual_memory()._asdict()
) # type: ignore
@@ -224,8 +210,6 @@ class SystemPlugin(SensorPlugin, EntityManager):
return MemoryStatsSchema().dump(self._mem_virtual()) # type: ignore
def _mem_swap(self) -> SwapStats:
- import psutil
-
return SwapStatsSchema().load(psutil.swap_memory()._asdict()) # type: ignore
@action
@@ -237,111 +221,41 @@ class SystemPlugin(SensorPlugin, EntityManager):
"""
return SwapStatsSchema().dump(self._mem_swap()) # type: ignore
- @action
- def disk_partitions(self) -> DiskResponseList:
- """
- Get the list of partitions mounted on the system.
- :return: list of :class:`platypush.message.response.system.DiskPartitionResponse`
- """
- import psutil
+ def _disk_info(self) -> List[Disk]:
+ parts = {part.device: part._asdict() for part in psutil.disk_partitions()}
+ basename_parts = {os.path.basename(part): part for part in parts}
- parts = psutil.disk_partitions()
- return DiskResponseList(
+ io_stats = {
+ basename_parts[disk]: stats._asdict()
+ for disk, stats in psutil.disk_io_counters(perdisk=True).items()
+ if disk in basename_parts
+ }
+
+ usage = {
+ disk: psutil.disk_usage(info['mountpoint'])._asdict()
+ for disk, info in parts.items()
+ }
+
+ return DiskSchema().load( # type: ignore
[
- DiskPartitionResponse(
- device=p.device,
- mount_point=p.mountpoint,
- fstype=p.fstype,
- opts=p.opts,
- )
- for p in parts
- ]
+ {
+ **info,
+ **io_stats[part],
+ **usage[part],
+ }
+ for part, info in parts.items()
+ ],
+ many=True,
)
@action
- def disk_usage(
- self, path: Optional[str] = None
- ) -> Union[DiskUsageResponse, DiskResponseList]:
+ def disk_info(self):
"""
- Get the usage of a mounted disk.
+ Get information about the detected disks and partitions.
- :param path: Path where the device is mounted (default: get stats for all mounted devices).
- :return: :class:`platypush.message.response.system.DiskUsageResponse` or list of
- :class:`platypush.message.response.system.DiskUsageResponse`.
+ :return: .. schema:: system.DiskSchema(many=True)
"""
- import psutil
-
- if path:
- usage = psutil.disk_usage(path)
- return DiskUsageResponse(
- path=path,
- total=usage.total,
- used=usage.used,
- free=usage.free,
- percent=usage.percent,
- )
- else:
- disks = {
- p.mountpoint: psutil.disk_usage(p.mountpoint)
- for p in psutil.disk_partitions()
- }
-
- return DiskResponseList(
- [
- DiskUsageResponse(
- path=path,
- total=disk.total,
- used=disk.used,
- free=disk.free,
- percent=disk.percent,
- )
- for path, disk in disks.items()
- ]
- )
-
- @action
- def disk_io_counters(
- self, disk: Optional[str] = None, per_disk: bool = False
- ) -> Union[DiskIoCountersResponse, DiskResponseList]:
- """
- Get the I/O counter stats for the mounted disks.
-
- :param disk: Select the stats for a specific disk (e.g. 'sda1'). Default: get stats for all mounted disks.
- :param per_disk: Return the stats per disk (default: False).
- :return: :class:`platypush.message.response.system.DiskIoCountersResponse` or list of
- :class:`platypush.message.response.system.DiskIoCountersResponse`.
- """
- import psutil
-
- def _expand_response(_disk, _stats):
- return DiskIoCountersResponse(
- read_count=_stats.read_count,
- write_count=_stats.write_count,
- read_bytes=_stats.read_bytes,
- write_bytes=_stats.write_bytes,
- read_time=_stats.read_time,
- write_time=_stats.write_time,
- read_merged_count=_stats.read_merged_count,
- write_merged_count=_stats.write_merged_count,
- busy_time=_stats.busy_time,
- disk=_disk,
- )
-
- if disk:
- per_disk = True
-
- io = psutil.disk_io_counters(perdisk=per_disk)
- if disk:
- stats = [d for name, d in io.items() if name == disk]
- assert stats, 'No such disk: {}'.format(disk)
- return _expand_response(disk, stats[0])
-
- if not per_disk:
- return _expand_response(None, io)
-
- return DiskResponseList(
- [_expand_response(disk, stats) for disk, stats in io.items()]
- )
+ return DiskSchema().dump(self._disk_info(), many=True)
@action
def net_io_counters(
@@ -355,7 +269,6 @@ class SystemPlugin(SensorPlugin, EntityManager):
:return: :class:`platypush.message.response.system.NetIoCountersResponse` or list of
:class:`platypush.message.response.system.NetIoCountersResponse`.
"""
- import psutil
def _expand_response(_nic, _stats):
return NetworkIoCountersResponse(
@@ -414,8 +327,6 @@ class SystemPlugin(SensorPlugin, EntityManager):
:return: List of :class:`platypush.message.response.system.NetworkConnectionResponse`.
"""
- import psutil
-
conns = psutil.net_connections(kind=type)
return NetworkResponseList(
@@ -446,8 +357,6 @@ class SystemPlugin(SensorPlugin, EntityManager):
:return: :class:`platypush.message.response.system.NetworkAddressResponse` or list of
:class:`platypush.message.response.system.NetworkAddressResponse`.
"""
- import psutil
-
addrs = psutil.net_if_addrs()
def _expand_addresses(_nic, _addrs):
@@ -504,8 +413,6 @@ class SystemPlugin(SensorPlugin, EntityManager):
:return: :class:`platypush.message.response.system.NetworkInterfaceStatsResponse` or list of
:class:`platypush.message.response.system.NetworkInterfaceStatsResponse`.
"""
- import psutil
-
stats = psutil.net_if_stats()
def _expand_stats(_nic, _stats):
@@ -540,8 +447,6 @@ class SystemPlugin(SensorPlugin, EntityManager):
:param sensor: Select the sensor name.
:param fahrenheit: Return the temperature in Fahrenheit (default: Celsius).
"""
- import psutil
-
stats = psutil.sensors_temperatures(fahrenheit=fahrenheit)
if sensor:
@@ -596,8 +501,6 @@ class SystemPlugin(SensorPlugin, EntityManager):
:param sensor: Select the sensor name.
:return: List of :class:`platypush.message.response.system.SensorFanResponse`.
"""
- import psutil
-
stats = psutil.sensors_fans()
def _expand_stats(name, _stats):
@@ -627,8 +530,6 @@ class SystemPlugin(SensorPlugin, EntityManager):
Get stats from the battery sensor.
:return: List of :class:`platypush.message.response.system.SensorFanResponse`.
"""
- import psutil
-
stats = psutil.sensors_battery()
return SensorBatteryResponse(
@@ -643,8 +544,6 @@ class SystemPlugin(SensorPlugin, EntityManager):
Get the list of connected users.
:return: List of :class:`platypush.message.response.system.ConnectUserResponse`.
"""
- import psutil
-
users = psutil.users()
return ConnectedUserResponseList(
@@ -668,8 +567,6 @@ class SystemPlugin(SensorPlugin, EntityManager):
:param filter: Filter the list by name.
:return: List of :class:`platypush.message.response.system.ProcessResponse`.
"""
- import psutil
-
processes = [psutil.Process(pid) for pid in psutil.pids()]
p_list = []
@@ -720,8 +617,6 @@ class SystemPlugin(SensorPlugin, EntityManager):
@staticmethod
def _get_process(pid: int):
- import psutil
-
return psutil.Process(pid)
@action
@@ -730,8 +625,6 @@ class SystemPlugin(SensorPlugin, EntityManager):
:param pid: Process PID.
:return: ``True`` if the process exists, ``False`` otherwise.
"""
- import psutil
-
return psutil.pid_exists(pid)
@action
@@ -794,6 +687,7 @@ class SystemPlugin(SensorPlugin, EntityManager):
},
'memory': self._mem_virtual(),
'swap': self._mem_swap(),
+ 'disks': self._disk_info(),
}
)
@@ -870,6 +764,14 @@ class SystemPlugin(SensorPlugin, EntityManager):
name='Swap',
**entities['swap'],
),
+ *[
+ DiskModel(
+ id=f'system:disk:{disk["device"]}',
+ name=disk.pop('device'),
+ **disk,
+ )
+ for disk in entities['disks']
+ ],
]
diff --git a/platypush/schemas/system.py b/platypush/schemas/system.py
index 927272c10..adcd05777 100644
--- a/platypush/schemas/system.py
+++ b/platypush/schemas/system.py
@@ -68,6 +68,25 @@ class CpuTimesBaseSchema(DataClassSchema):
}
+class DiskBaseSchema(DataClassSchema):
+ """
+ Base schema for disk stats.
+ """
+
+ @pre_load
+ def pre_load(self, data: dict, **_) -> dict:
+ # Convert read/write/busy times from milliseconds to seconds
+ for attr in ['read_time', 'write_time', 'busy_time']:
+ if data.get(attr) is not None:
+ data[attr] /= 1000
+
+ # Normalize the percentage between 0 and 1
+ if data.get('percent') is not None:
+ data['percent'] /= 100
+
+ return data
+
+
@dataclass
class CpuInfo:
"""
@@ -356,6 +375,131 @@ class SwapStats:
percent: float = percent_field()
+@dataclass
+class Disk:
+ """
+ Disk data class.
+ """
+
+ device: str = field(
+ metadata={
+ 'metadata': {
+ 'description': 'Path/identifier of the disk/partition',
+ 'example': '/dev/sda1',
+ }
+ }
+ )
+
+ mountpoint: Optional[str] = field(
+ metadata={
+ 'metadata': {
+ 'description': 'Where the disk is mounted',
+ 'example': '/home',
+ }
+ }
+ )
+
+ fstype: Optional[str] = field(
+ metadata={
+ 'metadata': {
+ 'description': 'Filesystem type',
+ 'example': 'ext4',
+ }
+ }
+ )
+
+ opts: Optional[str] = field(
+ metadata={
+ 'metadata': {
+ 'description': 'Extra mount options passed to the partition',
+ 'example': 'rw,relatime,fmask=0022,dmask=0022,utf8',
+ }
+ }
+ )
+
+ total: int = field(
+ metadata={
+ 'metadata': {
+ 'description': 'Total available space, in bytes',
+ }
+ }
+ )
+
+ used: int = field(
+ metadata={
+ 'metadata': {
+ 'description': 'Used disk space, in bytes',
+ }
+ }
+ )
+
+ free: int = field(
+ metadata={
+ 'metadata': {
+ 'description': 'Free disk space, in bytes',
+ }
+ }
+ )
+
+ read_count: int = field(
+ metadata={
+ 'metadata': {
+ 'description': 'Number of recorded read operations',
+ }
+ }
+ )
+
+ write_count: int = field(
+ metadata={
+ 'metadata': {
+ 'description': 'Number of recorded write operations',
+ }
+ }
+ )
+
+ read_bytes: int = field(
+ metadata={
+ 'metadata': {
+ 'description': 'Number of read bytes',
+ }
+ }
+ )
+
+ write_bytes: int = field(
+ metadata={
+ 'metadata': {
+ 'description': 'Number of written bytes',
+ }
+ }
+ )
+
+ read_time: float = field(
+ metadata={
+ 'metadata': {
+ 'description': 'Time spent reading, in seconds',
+ }
+ }
+ )
+
+ write_time: float = field(
+ metadata={
+ 'metadata': {
+ 'description': 'Time spent writing, in seconds',
+ }
+ }
+ )
+
+ busy_time: float = field(
+ metadata={
+ 'metadata': {
+ 'description': 'Total disk busy time, in seconds',
+ }
+ }
+ )
+
+ percent: float = percent_field()
+
+
@dataclass
class SystemInfo:
"""
@@ -365,12 +509,14 @@ class SystemInfo:
cpu: CpuData
memory: MemoryStats
swap: SwapStats
+ disks: List[Disk]
CpuFrequencySchema = class_schema(CpuFrequency, base_schema=DataClassSchema)
CpuInfoSchema = class_schema(CpuInfo, base_schema=CpuInfoBaseSchema)
CpuTimesSchema = class_schema(CpuTimes, base_schema=CpuTimesBaseSchema)
CpuStatsSchema = class_schema(CpuStats, base_schema=DataClassSchema)
+DiskSchema = class_schema(Disk, base_schema=DiskBaseSchema)
MemoryStatsSchema = class_schema(MemoryStats, base_schema=MemoryStatsBaseSchema)
SwapStatsSchema = class_schema(SwapStats, base_schema=MemoryStatsBaseSchema)
SystemInfoSchema = class_schema(SystemInfo, base_schema=DataClassSchema)