diff --git a/platypush/backend/http/webapp/src/components/panels/Entities/NetworkInterface.vue b/platypush/backend/http/webapp/src/components/panels/Entities/NetworkInterface.vue
new file mode 100644
index 0000000000..9b7a915097
--- /dev/null
+++ b/platypush/backend/http/webapp/src/components/panels/Entities/NetworkInterface.vue
@@ -0,0 +1,167 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Dropped inbound packets
+
+
+
+
+
+
+
Dropped outbound packets
+
+
+
+
+
+
+
+
+
+
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 436a885208..801665c76b 100644
--- a/platypush/backend/http/webapp/src/components/panels/Entities/meta.json
+++ b/platypush/backend/http/webapp/src/components/panels/Entities/meta.json
@@ -71,6 +71,14 @@
}
},
+ "network_interface": {
+ "name": "System",
+ "name_plural": "System",
+ "icon": {
+ "class": "fas fa-ethernet"
+ }
+ },
+
"current_sensor": {
"name": "Sensor",
"name_plural": "Sensors",
diff --git a/platypush/entities/system.py b/platypush/entities/system.py
index dd9d4040b9..bdadc9dd47 100644
--- a/platypush/entities/system.py
+++ b/platypush/entities/system.py
@@ -175,3 +175,30 @@ if 'disk' not in Base.metadata:
__mapper_args__ = {
'polymorphic_identity': __tablename__,
}
+
+
+if 'network_interface' not in Base.metadata:
+
+ class NetworkInterface(Entity):
+ """
+ ``NetworkInterface`` ORM model.
+ """
+
+ __tablename__ = 'network_interface'
+
+ id = Column(
+ Integer, ForeignKey(Entity.id, ondelete='CASCADE'), primary_key=True
+ )
+
+ bytes_sent = Column(Integer)
+ bytes_recv = Column(Integer)
+ packets_sent = Column(Integer)
+ packets_recv = Column(Integer)
+ errors_in = Column(Integer)
+ errors_out = Column(Integer)
+ drop_in = Column(Integer)
+ drop_out = Column(Integer)
+
+ __mapper_args__ = {
+ 'polymorphic_identity': __tablename__,
+ }
diff --git a/platypush/message/response/system/__init__.py b/platypush/message/response/system/__init__.py
index b3fe3538d7..98374a2ee1 100644
--- a/platypush/message/response/system/__init__.py
+++ b/platypush/message/response/system/__init__.py
@@ -28,38 +28,6 @@ class SensorResponse(SystemResponse):
pass
-class NetworkIoCountersResponse(NetworkResponse):
- def __init__(
- self,
- bytes_sent: int,
- bytes_recv: int,
- packets_sent: int,
- packets_recv: int,
- errin: int,
- errout: int,
- dropin: int,
- dropout: int,
- nic: Optional[str] = None,
- *args,
- **kwargs
- ):
- super().__init__(
- *args,
- output={
- 'bytes_sent': bytes_sent,
- 'bytes_recv': bytes_recv,
- 'packets_sent': packets_sent,
- 'packets_recv': packets_recv,
- 'errin': errin,
- 'errout': errout,
- 'dropin': dropin,
- 'dropout': dropout,
- 'nic': nic,
- },
- **kwargs
- )
-
-
class NetworkConnectionResponse(NetworkResponse):
# noinspection PyShadowingBuiltins
def __init__(
diff --git a/platypush/plugins/system/__init__.py b/platypush/plugins/system/__init__.py
index 3e0d2b4d11..4b71e3c4b3 100644
--- a/platypush/plugins/system/__init__.py
+++ b/platypush/plugins/system/__init__.py
@@ -18,10 +18,10 @@ from platypush.entities.system import (
CpuTimes as CpuTimesModel,
Disk as DiskModel,
MemoryStats as MemoryStatsModel,
+ NetworkInterface as NetworkInterfaceModel,
SwapStats as SwapStatsModel,
)
from platypush.message.response.system import (
- NetworkIoCountersResponse,
NetworkResponseList,
NetworkConnectionResponse,
NetworkAddressResponse,
@@ -50,6 +50,8 @@ from platypush.schemas.system import (
DiskSchema,
MemoryStats,
MemoryStatsSchema,
+ NetworkInterface,
+ NetworkInterfaceSchema,
SwapStats,
SwapStatsSchema,
SystemInfoSchema,
@@ -257,48 +259,45 @@ class SystemPlugin(SensorPlugin, EntityManager):
"""
return DiskSchema().dump(self._disk_info(), many=True)
- @action
- def net_io_counters(
- self, nic: Optional[str] = None, per_nic: bool = False
- ) -> Union[NetworkIoCountersResponse, NetworkResponseList]:
- """
- Get the I/O counters stats for the network interfaces.
-
- :param nic: Select the stats for a specific network device (e.g. 'eth0'). Default: get stats for all NICs.
- :param per_nic: Return the stats broken down per interface (default: False).
- :return: :class:`platypush.message.response.system.NetIoCountersResponse` or list of
- :class:`platypush.message.response.system.NetIoCountersResponse`.
- """
-
- def _expand_response(_nic, _stats):
- return NetworkIoCountersResponse(
- bytes_sent=_stats.bytes_sent,
- bytes_recv=_stats.bytes_recv,
- packets_sent=_stats.packets_sent,
- packets_recv=_stats.packets_recv,
- errin=_stats.errin,
- errout=_stats.errout,
- dropin=_stats.dropin,
- dropout=_stats.dropout,
- nic=_nic,
- )
-
- if nic:
- per_nic = True
-
- io = psutil.net_io_counters(pernic=per_nic)
- if nic:
- stats = [d for name, d in io.items() if name == nic]
- assert stats, 'No such network interface: {}'.format(nic)
- return _expand_response(nic, stats[0])
-
- if not per_nic:
- return _expand_response(nic, io)
-
- return NetworkResponseList(
- [_expand_response(nic, stats) for nic, stats in io.items()]
+ def _net_io_counters(self) -> List[NetworkInterface]:
+ return NetworkInterfaceSchema().load( # type: ignore
+ [
+ {'interface': interface, **stats._asdict()}
+ for interface, stats in psutil.net_io_counters(pernic=True).items()
+ if any(bool(val) for val in stats._asdict().values())
+ ],
+ many=True,
)
+ def _net_io_counters_avg(self) -> NetworkInterface:
+ stats = psutil.net_io_counters(pernic=False)
+ return NetworkInterfaceSchema().load( # type: ignore
+ {
+ 'interface': None,
+ **stats._asdict(),
+ }
+ )
+
+ @action
+ def net_io_counters(self, per_nic: bool = False):
+ """
+ Get the information and statistics for the network interfaces.
+
+ :param per_nic: Return the stats grouped by interface (default: False).
+ :return: If ``per_nic=False``:
+
+ .. schema:: system.NetworkInterfaceSchema
+
+ If ``per_nic=True`` then a list will be returned, where each item
+ identifies the statistics per network interface:
+
+ .. schema:: system.NetworkInterfaceSchema(many=True)
+ """
+
+ if per_nic:
+ return NetworkInterfaceSchema().dump(self._net_io_counters(), many=True)
+ return NetworkInterfaceSchema().dump(self._net_io_counters_avg())
+
@action
def net_connections(
self, type: Optional[str] = None
@@ -688,6 +687,7 @@ class SystemPlugin(SensorPlugin, EntityManager):
'memory': self._mem_virtual(),
'swap': self._mem_swap(),
'disks': self._disk_info(),
+ 'network': self._net_io_counters(),
}
)
@@ -772,6 +772,14 @@ class SystemPlugin(SensorPlugin, EntityManager):
)
for disk in entities['disks']
],
+ *[
+ NetworkInterfaceModel(
+ id=f'system:network_interface:{nic["interface"]}',
+ name=nic.pop('interface'),
+ **nic,
+ )
+ for nic in entities['network']
+ ],
]
diff --git a/platypush/schemas/system.py b/platypush/schemas/system.py
deleted file mode 100644
index adcd05777c..0000000000
--- a/platypush/schemas/system.py
+++ /dev/null
@@ -1,522 +0,0 @@
-from dataclasses import dataclass, field
-from typing import List, Optional, Tuple
-
-from marshmallow import pre_load
-from marshmallow.validate import Range
-from marshmallow_dataclass import class_schema
-
-from platypush.schemas.dataclasses import DataClassSchema
-
-
-def percent_field(**kwargs):
- """
- Field used to model percentage float fields between 0 and 1.
- """
- return field(
- default_factory=float,
- metadata={
- 'validate': Range(min=0, max=1),
- **kwargs,
- },
- )
-
-
-class CpuInfoBaseSchema(DataClassSchema):
- """
- Base schema for CPU info.
- """
-
- @pre_load
- def pre_load(self, data: dict, **_) -> dict:
- if data.get('hz_advertised'):
- data['frequency_advertised'] = data.pop('hz_advertised')[0]
- if data.get('hz_actual'):
- data['frequency_actual'] = data.pop('hz_actual')[0]
-
- return data
-
-
-class MemoryStatsBaseSchema(DataClassSchema):
- """
- Base schema for memory stats.
- """
-
- @pre_load
- def pre_load(self, data: dict, **_) -> dict:
- # Normalize the percentage between 0 and 1
- if data.get('percent') is not None:
- data['percent'] /= 100
- return data
-
-
-class CpuTimesBaseSchema(DataClassSchema):
- """
- Base schema for CPU times.
- """
-
- @pre_load
- def pre_load(self, data, **_) -> dict:
- """
- Convert the underlying object to dict and normalize all the percentage
- values from [0, 100] to [0, 1].
- """
- return {
- key: value / 100.0
- for key, value in (
- data if isinstance(data, dict) else data._asdict()
- ).items()
- }
-
-
-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:
- """
- CPU info data class.
- """
-
- architecture: Optional[str] = field(
- metadata={
- 'data_key': 'arch_string_raw',
- 'metadata': {
- 'description': 'CPU architecture',
- 'example': 'x86_64',
- },
- }
- )
-
- bits: int = field(
- metadata={
- 'metadata': {
- 'description': 'CPU bits / register size',
- 'example': 64,
- }
- }
- )
-
- cores: int = field(
- metadata={
- 'data_key': 'count',
- 'metadata': {
- 'description': 'Number of cores',
- 'example': 4,
- },
- }
- )
-
- vendor: Optional[str] = field(
- metadata={
- 'data_key': 'vendor_id_raw',
- 'metadata': {
- 'description': 'Vendor string',
- 'example': 'GenuineIntel',
- },
- }
- )
-
- brand: Optional[str] = field(
- metadata={
- 'data_key': 'brand_raw',
- 'metadata': {
- 'description': 'CPU brand string',
- 'example': 'Intel(R) Core(TM) i7-5500U CPU @ 2.40GHz',
- },
- }
- )
-
- frequency_advertised: Optional[int] = field(
- metadata={
- 'metadata': {
- 'description': 'Advertised CPU frequency, in Hz',
- 'example': 2400000000,
- }
- }
- )
-
- frequency_actual: Optional[int] = field(
- metadata={
- 'metadata': {
- 'description': 'Actual CPU frequency, in Hz',
- 'example': 2350000000,
- }
- }
- )
-
- flags: List[str] = field(
- metadata={
- 'metadata': {
- 'description': 'CPU flags',
- 'example': ['acpi', 'aes', 'cpuid'],
- }
- }
- )
-
- l1_instruction_cache_size: Optional[int] = field(
- metadata={
- 'metadata': {
- 'description': 'Size of the L1 instruction cache, in bytes',
- 'example': 65536,
- }
- }
- )
-
- l1_data_cache_size: Optional[int] = field(
- metadata={
- 'metadata': {
- 'description': 'Size of the L1 data cache, in bytes',
- 'example': 65536,
- }
- }
- )
-
- l2_cache_size: Optional[int] = field(
- metadata={
- 'metadata': {
- 'description': 'Size of the L2 cache, in bytes',
- 'example': 524288,
- }
- }
- )
-
- l3_cache_size: Optional[int] = field(
- metadata={
- 'metadata': {
- 'description': 'Size of the L2 cache, in bytes',
- 'example': 4194304,
- }
- }
- )
-
-
-@dataclass
-class CpuTimes:
- """
- CPU times data class.
- """
-
- user: Optional[float] = percent_field()
- nice: Optional[float] = percent_field()
- system: Optional[float] = percent_field()
- idle: Optional[float] = percent_field()
- iowait: Optional[float] = percent_field()
- irq: Optional[float] = percent_field()
- softirq: Optional[float] = percent_field()
- steal: Optional[float] = percent_field()
- guest: Optional[float] = percent_field()
- guest_nice: Optional[float] = percent_field()
-
-
-@dataclass
-class CpuStats:
- """
- CPU stats data class.
- """
-
- ctx_switches: int
- interrupts: int
- soft_interrupts: int
- syscalls: int
-
-
-@dataclass
-class CpuFrequency:
- """
- CPU frequency data class.
- """
-
- current: float
- min: float
- max: float
-
-
-@dataclass
-class CpuData:
- """
- CPU data aggregate dataclass.
- """
-
- info: CpuInfo
- times: CpuTimes
- frequency: CpuFrequency
- stats: CpuStats
- load_avg: Tuple[float, float, float]
- percent: float = percent_field()
-
-
-@dataclass
-class MemoryStats:
- """
- Memory stats data class.
- """
-
- total: int = field(
- metadata={
- 'metadata': {
- 'description': 'Total available memory, in bytes',
- }
- }
- )
-
- available: int = field(
- metadata={
- 'metadata': {
- 'description': 'Available memory, in bytes',
- }
- }
- )
-
- used: int = field(
- metadata={
- 'metadata': {
- 'description': 'Used memory, in bytes',
- }
- }
- )
-
- free: int = field(
- metadata={
- 'metadata': {
- 'description': 'Free memory, in bytes',
- }
- }
- )
-
- active: int = field(
- metadata={
- 'metadata': {
- 'description': 'Size of the active memory, in bytes',
- }
- }
- )
-
- inactive: int = field(
- metadata={
- 'metadata': {
- 'description': 'Size of the inactive memory, in bytes',
- }
- }
- )
-
- buffers: int = field(
- metadata={
- 'metadata': {
- 'description': 'Size of the buffered memory, in bytes',
- }
- }
- )
-
- cached: int = field(
- metadata={
- 'metadata': {
- 'description': 'Size of the cached memory, in bytes',
- }
- }
- )
-
- shared: int = field(
- metadata={
- 'metadata': {
- 'description': 'Size of the shared memory, in bytes',
- }
- }
- )
-
- percent: float = percent_field()
-
-
-@dataclass
-class SwapStats:
- """
- Swap memory stats data class.
- """
-
- total: int = field(
- metadata={
- 'metadata': {
- 'description': 'Total available memory, in bytes',
- }
- }
- )
-
- used: int = field(
- metadata={
- 'metadata': {
- 'description': 'Used memory, in bytes',
- }
- }
- )
-
- free: int = field(
- metadata={
- 'metadata': {
- 'description': 'Free memory, in bytes',
- }
- }
- )
-
- 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:
- """
- Aggregate system info dataclass.
- """
-
- 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)
diff --git a/platypush/schemas/system/__init__.py b/platypush/schemas/system/__init__.py
new file mode 100644
index 0000000000..2e45aae894
--- /dev/null
+++ b/platypush/schemas/system/__init__.py
@@ -0,0 +1,39 @@
+from ._cpu import (
+ Cpu,
+ CpuFrequency,
+ CpuFrequencySchema,
+ CpuInfo,
+ CpuInfoSchema,
+ CpuStats,
+ CpuStatsSchema,
+ CpuTimes,
+ CpuTimesSchema,
+)
+from ._disk import Disk, DiskSchema
+from ._memory import MemoryStats, MemoryStatsSchema, SwapStats, SwapStatsSchema
+from ._model import SystemInfo
+from ._network import NetworkInterface, NetworkInterfaceSchema
+from ._schemas import SystemInfoSchema
+
+
+__all__ = [
+ "Cpu",
+ "CpuFrequency",
+ "CpuFrequencySchema",
+ "CpuInfo",
+ "CpuInfoSchema",
+ "CpuStats",
+ "CpuStatsSchema",
+ "CpuTimes",
+ "CpuTimesSchema",
+ "Disk",
+ "DiskSchema",
+ "MemoryStats",
+ "MemoryStatsSchema",
+ "SwapStats",
+ "SwapStatsSchema",
+ "NetworkInterface",
+ "NetworkInterfaceSchema",
+ "SystemInfo",
+ "SystemInfoSchema",
+]
diff --git a/platypush/schemas/system/_base.py b/platypush/schemas/system/_base.py
new file mode 100644
index 0000000000..aa7addaf82
--- /dev/null
+++ b/platypush/schemas/system/_base.py
@@ -0,0 +1,16 @@
+from dataclasses import field
+
+from marshmallow.validate import Range
+
+
+def percent_field(**kwargs):
+ """
+ Field used to model percentage float fields between 0 and 1.
+ """
+ return field(
+ default_factory=float,
+ metadata={
+ 'validate': Range(min=0, max=1),
+ **kwargs,
+ },
+ )
diff --git a/platypush/schemas/system/_cpu/__init__.py b/platypush/schemas/system/_cpu/__init__.py
new file mode 100644
index 0000000000..e2c2b01e39
--- /dev/null
+++ b/platypush/schemas/system/_cpu/__init__.py
@@ -0,0 +1,15 @@
+from ._model import Cpu, CpuFrequency, CpuInfo, CpuStats, CpuTimes
+from ._schemas import CpuFrequencySchema, CpuInfoSchema, CpuStatsSchema, CpuTimesSchema
+
+
+__all__ = [
+ "Cpu",
+ "CpuFrequency",
+ "CpuFrequencySchema",
+ "CpuInfo",
+ "CpuInfoSchema",
+ "CpuStats",
+ "CpuStatsSchema",
+ "CpuTimes",
+ "CpuTimesSchema",
+]
diff --git a/platypush/schemas/system/_cpu/_base.py b/platypush/schemas/system/_cpu/_base.py
new file mode 100644
index 0000000000..eb49a5109c
--- /dev/null
+++ b/platypush/schemas/system/_cpu/_base.py
@@ -0,0 +1,37 @@
+from marshmallow import pre_load
+
+from platypush.schemas.dataclasses import DataClassSchema
+
+
+class CpuInfoBaseSchema(DataClassSchema):
+ """
+ Base schema for CPU info.
+ """
+
+ @pre_load
+ def pre_load(self, data: dict, **_) -> dict:
+ if data.get('hz_advertised'):
+ data['frequency_advertised'] = data.pop('hz_advertised')[0]
+ if data.get('hz_actual'):
+ data['frequency_actual'] = data.pop('hz_actual')[0]
+
+ return data
+
+
+class CpuTimesBaseSchema(DataClassSchema):
+ """
+ Base schema for CPU times.
+ """
+
+ @pre_load
+ def pre_load(self, data, **_) -> dict:
+ """
+ Convert the underlying object to dict and normalize all the percentage
+ values from [0, 100] to [0, 1].
+ """
+ return {
+ key: value / 100.0
+ for key, value in (
+ data if isinstance(data, dict) else data._asdict()
+ ).items()
+ }
diff --git a/platypush/schemas/system/_cpu/_model.py b/platypush/schemas/system/_cpu/_model.py
new file mode 100644
index 0000000000..2f4f30b5c8
--- /dev/null
+++ b/platypush/schemas/system/_cpu/_model.py
@@ -0,0 +1,178 @@
+from dataclasses import dataclass, field
+from typing import List, Optional, Tuple
+
+from .._base import percent_field
+
+
+@dataclass
+class CpuInfo:
+ """
+ CPU info data class.
+ """
+
+ architecture: Optional[str] = field(
+ metadata={
+ 'data_key': 'arch_string_raw',
+ 'metadata': {
+ 'description': 'CPU architecture',
+ 'example': 'x86_64',
+ },
+ }
+ )
+
+ bits: int = field(
+ metadata={
+ 'metadata': {
+ 'description': 'CPU bits / register size',
+ 'example': 64,
+ }
+ }
+ )
+
+ cores: int = field(
+ metadata={
+ 'data_key': 'count',
+ 'metadata': {
+ 'description': 'Number of cores',
+ 'example': 4,
+ },
+ }
+ )
+
+ vendor: Optional[str] = field(
+ metadata={
+ 'data_key': 'vendor_id_raw',
+ 'metadata': {
+ 'description': 'Vendor string',
+ 'example': 'GenuineIntel',
+ },
+ }
+ )
+
+ brand: Optional[str] = field(
+ metadata={
+ 'data_key': 'brand_raw',
+ 'metadata': {
+ 'description': 'CPU brand string',
+ 'example': 'Intel(R) Core(TM) i7-5500U CPU @ 2.40GHz',
+ },
+ }
+ )
+
+ frequency_advertised: Optional[int] = field(
+ metadata={
+ 'metadata': {
+ 'description': 'Advertised CPU frequency, in Hz',
+ 'example': 2400000000,
+ }
+ }
+ )
+
+ frequency_actual: Optional[int] = field(
+ metadata={
+ 'metadata': {
+ 'description': 'Actual CPU frequency, in Hz',
+ 'example': 2350000000,
+ }
+ }
+ )
+
+ flags: List[str] = field(
+ metadata={
+ 'metadata': {
+ 'description': 'CPU flags',
+ 'example': ['acpi', 'aes', 'cpuid'],
+ }
+ }
+ )
+
+ l1_instruction_cache_size: Optional[int] = field(
+ metadata={
+ 'metadata': {
+ 'description': 'Size of the L1 instruction cache, in bytes',
+ 'example': 65536,
+ }
+ }
+ )
+
+ l1_data_cache_size: Optional[int] = field(
+ metadata={
+ 'metadata': {
+ 'description': 'Size of the L1 data cache, in bytes',
+ 'example': 65536,
+ }
+ }
+ )
+
+ l2_cache_size: Optional[int] = field(
+ metadata={
+ 'metadata': {
+ 'description': 'Size of the L2 cache, in bytes',
+ 'example': 524288,
+ }
+ }
+ )
+
+ l3_cache_size: Optional[int] = field(
+ metadata={
+ 'metadata': {
+ 'description': 'Size of the L2 cache, in bytes',
+ 'example': 4194304,
+ }
+ }
+ )
+
+
+@dataclass
+class CpuTimes:
+ """
+ CPU times data class.
+ """
+
+ user: Optional[float] = percent_field()
+ nice: Optional[float] = percent_field()
+ system: Optional[float] = percent_field()
+ idle: Optional[float] = percent_field()
+ iowait: Optional[float] = percent_field()
+ irq: Optional[float] = percent_field()
+ softirq: Optional[float] = percent_field()
+ steal: Optional[float] = percent_field()
+ guest: Optional[float] = percent_field()
+ guest_nice: Optional[float] = percent_field()
+
+
+@dataclass
+class CpuStats:
+ """
+ CPU stats data class.
+ """
+
+ ctx_switches: int
+ interrupts: int
+ soft_interrupts: int
+ syscalls: int
+
+
+@dataclass
+class CpuFrequency:
+ """
+ CPU frequency data class.
+ """
+
+ current: float
+ min: float
+ max: float
+
+
+@dataclass
+class Cpu:
+ """
+ CPU data aggregate dataclass.
+ """
+
+ info: CpuInfo
+ times: CpuTimes
+ frequency: CpuFrequency
+ stats: CpuStats
+ load_avg: Tuple[float, float, float]
+ percent: float = percent_field()
diff --git a/platypush/schemas/system/_cpu/_schemas.py b/platypush/schemas/system/_cpu/_schemas.py
new file mode 100644
index 0000000000..f3cbf939c8
--- /dev/null
+++ b/platypush/schemas/system/_cpu/_schemas.py
@@ -0,0 +1,12 @@
+from marshmallow_dataclass import class_schema
+
+from platypush.schemas.dataclasses import DataClassSchema
+
+from ._base import CpuInfoBaseSchema, CpuTimesBaseSchema
+from ._model import CpuFrequency, CpuInfo, CpuStats, CpuTimes
+
+
+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)
diff --git a/platypush/schemas/system/_disk/__init__.py b/platypush/schemas/system/_disk/__init__.py
new file mode 100644
index 0000000000..7f8ac1b691
--- /dev/null
+++ b/platypush/schemas/system/_disk/__init__.py
@@ -0,0 +1,5 @@
+from ._model import Disk
+from ._schemas import DiskSchema
+
+
+__all__ = ["Disk", "DiskSchema"]
diff --git a/platypush/schemas/system/_disk/_base.py b/platypush/schemas/system/_disk/_base.py
new file mode 100644
index 0000000000..35826e7d85
--- /dev/null
+++ b/platypush/schemas/system/_disk/_base.py
@@ -0,0 +1,22 @@
+from marshmallow import pre_load
+
+from platypush.schemas.dataclasses import 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
diff --git a/platypush/schemas/system/_disk/_model.py b/platypush/schemas/system/_disk/_model.py
new file mode 100644
index 0000000000..8cfd6a199a
--- /dev/null
+++ b/platypush/schemas/system/_disk/_model.py
@@ -0,0 +1,129 @@
+from dataclasses import dataclass, field
+from typing import Optional
+
+from .._base import 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()
diff --git a/platypush/schemas/system/_disk/_schemas.py b/platypush/schemas/system/_disk/_schemas.py
new file mode 100644
index 0000000000..564910ae4d
--- /dev/null
+++ b/platypush/schemas/system/_disk/_schemas.py
@@ -0,0 +1,7 @@
+from marshmallow_dataclass import class_schema
+
+from ._base import DiskBaseSchema
+from ._model import Disk
+
+
+DiskSchema = class_schema(Disk, base_schema=DiskBaseSchema)
diff --git a/platypush/schemas/system/_memory/__init__.py b/platypush/schemas/system/_memory/__init__.py
new file mode 100644
index 0000000000..c98dc5a752
--- /dev/null
+++ b/platypush/schemas/system/_memory/__init__.py
@@ -0,0 +1,5 @@
+from ._model import MemoryStats, SwapStats
+from ._schemas import MemoryStatsSchema, SwapStatsSchema
+
+
+__all__ = ["MemoryStats", "MemoryStatsSchema", "SwapStats", "SwapStatsSchema"]
diff --git a/platypush/schemas/system/_memory/_base.py b/platypush/schemas/system/_memory/_base.py
new file mode 100644
index 0000000000..73bf1c5149
--- /dev/null
+++ b/platypush/schemas/system/_memory/_base.py
@@ -0,0 +1,16 @@
+from marshmallow import pre_load
+
+from platypush.schemas.dataclasses import DataClassSchema
+
+
+class MemoryStatsBaseSchema(DataClassSchema):
+ """
+ Base schema for memory stats.
+ """
+
+ @pre_load
+ def pre_load(self, data: dict, **_) -> dict:
+ # Normalize the percentage between 0 and 1
+ if data.get('percent') is not None:
+ data['percent'] /= 100
+ return data
diff --git a/platypush/schemas/system/_memory/_model.py b/platypush/schemas/system/_memory/_model.py
new file mode 100644
index 0000000000..7de7f66dac
--- /dev/null
+++ b/platypush/schemas/system/_memory/_model.py
@@ -0,0 +1,117 @@
+from dataclasses import dataclass, field
+
+from .._base import percent_field
+
+
+@dataclass
+class MemoryStats:
+ """
+ Memory stats data class.
+ """
+
+ total: int = field(
+ metadata={
+ 'metadata': {
+ 'description': 'Total available memory, in bytes',
+ }
+ }
+ )
+
+ available: int = field(
+ metadata={
+ 'metadata': {
+ 'description': 'Available memory, in bytes',
+ }
+ }
+ )
+
+ used: int = field(
+ metadata={
+ 'metadata': {
+ 'description': 'Used memory, in bytes',
+ }
+ }
+ )
+
+ free: int = field(
+ metadata={
+ 'metadata': {
+ 'description': 'Free memory, in bytes',
+ }
+ }
+ )
+
+ active: int = field(
+ metadata={
+ 'metadata': {
+ 'description': 'Size of the active memory, in bytes',
+ }
+ }
+ )
+
+ inactive: int = field(
+ metadata={
+ 'metadata': {
+ 'description': 'Size of the inactive memory, in bytes',
+ }
+ }
+ )
+
+ buffers: int = field(
+ metadata={
+ 'metadata': {
+ 'description': 'Size of the buffered memory, in bytes',
+ }
+ }
+ )
+
+ cached: int = field(
+ metadata={
+ 'metadata': {
+ 'description': 'Size of the cached memory, in bytes',
+ }
+ }
+ )
+
+ shared: int = field(
+ metadata={
+ 'metadata': {
+ 'description': 'Size of the shared memory, in bytes',
+ }
+ }
+ )
+
+ percent: float = percent_field()
+
+
+@dataclass
+class SwapStats:
+ """
+ Swap memory stats data class.
+ """
+
+ total: int = field(
+ metadata={
+ 'metadata': {
+ 'description': 'Total available memory, in bytes',
+ }
+ }
+ )
+
+ used: int = field(
+ metadata={
+ 'metadata': {
+ 'description': 'Used memory, in bytes',
+ }
+ }
+ )
+
+ free: int = field(
+ metadata={
+ 'metadata': {
+ 'description': 'Free memory, in bytes',
+ }
+ }
+ )
+
+ percent: float = percent_field()
diff --git a/platypush/schemas/system/_memory/_schemas.py b/platypush/schemas/system/_memory/_schemas.py
new file mode 100644
index 0000000000..9d7bc21df8
--- /dev/null
+++ b/platypush/schemas/system/_memory/_schemas.py
@@ -0,0 +1,8 @@
+from marshmallow_dataclass import class_schema
+
+from ._base import MemoryStatsBaseSchema
+from ._model import MemoryStats, SwapStats
+
+
+MemoryStatsSchema = class_schema(MemoryStats, base_schema=MemoryStatsBaseSchema)
+SwapStatsSchema = class_schema(SwapStats, base_schema=MemoryStatsBaseSchema)
diff --git a/platypush/schemas/system/_model.py b/platypush/schemas/system/_model.py
new file mode 100644
index 0000000000..89bd7261fc
--- /dev/null
+++ b/platypush/schemas/system/_model.py
@@ -0,0 +1,20 @@
+from dataclasses import dataclass
+from typing import List
+
+from ._cpu import Cpu
+from ._disk import Disk
+from ._memory import MemoryStats, SwapStats
+from ._network import NetworkInterface
+
+
+@dataclass
+class SystemInfo:
+ """
+ Aggregate system info dataclass.
+ """
+
+ cpu: Cpu
+ memory: MemoryStats
+ swap: SwapStats
+ disks: List[Disk]
+ network: List[NetworkInterface]
diff --git a/platypush/schemas/system/_network/__init__.py b/platypush/schemas/system/_network/__init__.py
new file mode 100644
index 0000000000..969c954986
--- /dev/null
+++ b/platypush/schemas/system/_network/__init__.py
@@ -0,0 +1,8 @@
+from ._model import NetworkInterface
+from ._schemas import NetworkInterfaceSchema
+
+
+__all__ = [
+ "NetworkInterface",
+ "NetworkInterfaceSchema",
+]
diff --git a/platypush/schemas/system/_network/_base.py b/platypush/schemas/system/_network/_base.py
new file mode 100644
index 0000000000..b0e2da2b9f
--- /dev/null
+++ b/platypush/schemas/system/_network/_base.py
@@ -0,0 +1,22 @@
+from marshmallow import pre_load
+
+from platypush.schemas.dataclasses import DataClassSchema
+
+
+class NetworkInterfaceBaseSchema(DataClassSchema):
+ """
+ Base schema for network interface stats.
+ """
+
+ @pre_load
+ def pre_load(self, data: dict, **_) -> dict:
+ for in_attr, out_attr in {
+ 'errin': 'errors_in',
+ 'errout': 'errors_out',
+ 'dropin': 'drop_in',
+ 'dropout': 'drop_out',
+ }.items():
+ if in_attr in data:
+ data[out_attr] = data.pop(in_attr)
+
+ return data
diff --git a/platypush/schemas/system/_network/_model.py b/platypush/schemas/system/_network/_model.py
new file mode 100644
index 0000000000..98db4e4bbf
--- /dev/null
+++ b/platypush/schemas/system/_network/_model.py
@@ -0,0 +1,82 @@
+from dataclasses import dataclass, field
+from typing import Optional
+
+
+@dataclass
+class NetworkInterface:
+ """
+ Network interface statistics data class.
+ """
+
+ interface: Optional[str] = field(
+ metadata={
+ 'metadata': {
+ 'description': 'Network interface identifier',
+ 'example': 'eth0',
+ }
+ }
+ )
+
+ bytes_sent: int = field(
+ metadata={
+ 'metadata': {
+ 'description': 'Number of bytes sent',
+ }
+ }
+ )
+
+ bytes_recv: int = field(
+ metadata={
+ 'metadata': {
+ 'description': 'Number of bytes received',
+ }
+ }
+ )
+
+ packets_sent: int = field(
+ metadata={
+ 'metadata': {
+ 'description': 'Number of packets sent',
+ }
+ }
+ )
+
+ packets_recv: int = field(
+ metadata={
+ 'metadata': {
+ 'description': 'Number of packets received',
+ }
+ }
+ )
+
+ errors_in: int = field(
+ metadata={
+ 'metadata': {
+ 'description': 'Number of errors on incoming traffic',
+ },
+ }
+ )
+
+ errors_out: int = field(
+ metadata={
+ 'metadata': {
+ 'description': 'Number of errors on outgoing traffic',
+ },
+ }
+ )
+
+ drop_in: int = field(
+ metadata={
+ 'metadata': {
+ 'description': 'Number of packets dropped on incoming traffic',
+ },
+ }
+ )
+
+ drop_out: int = field(
+ metadata={
+ 'metadata': {
+ 'description': 'Number of packets dropped on outgoing traffic',
+ },
+ }
+ )
diff --git a/platypush/schemas/system/_network/_schemas.py b/platypush/schemas/system/_network/_schemas.py
new file mode 100644
index 0000000000..e960b111b3
--- /dev/null
+++ b/platypush/schemas/system/_network/_schemas.py
@@ -0,0 +1,9 @@
+from marshmallow_dataclass import class_schema
+
+from ._base import NetworkInterfaceBaseSchema
+from ._model import NetworkInterface
+
+
+NetworkInterfaceSchema = class_schema(
+ NetworkInterface, base_schema=NetworkInterfaceBaseSchema
+)
diff --git a/platypush/schemas/system/_schemas.py b/platypush/schemas/system/_schemas.py
new file mode 100644
index 0000000000..0bd2992fc4
--- /dev/null
+++ b/platypush/schemas/system/_schemas.py
@@ -0,0 +1,8 @@
+from marshmallow_dataclass import class_schema
+
+from platypush.schemas.dataclasses import DataClassSchema
+
+from ._model import SystemInfo
+
+
+SystemInfoSchema = class_schema(SystemInfo, base_schema=DataClassSchema)