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 00000000..9b7a9150
--- /dev/null
+++ b/platypush/backend/http/webapp/src/components/panels/Entities/NetworkInterface.vue
@@ -0,0 +1,167 @@
+<template>
+  <div class="entity network-interface-container">
+    <div class="head" @click.stop="isCollapsed = !isCollapsed">
+      <div class="col-1 icon">
+        <EntityIcon
+          :entity="value"
+          :loading="loading"
+          :error="error" />
+      </div>
+
+      <div class="col-10 label">
+        <div class="name" v-text="value.name" />
+      </div>
+
+      <div class="col-1 collapse-toggler" @click.stop="isCollapsed = !isCollapsed">
+        <i class="fas"
+          :class="{'fa-chevron-down': isCollapsed, 'fa-chevron-up': !isCollapsed}" />
+      </div>
+    </div>
+
+    <div class="body children attributes fade-in" v-if="!isCollapsed">
+      <div class="child" v-if="value.bytes_sent">
+        <div class="col-s-12 col-m-6 label">
+          <div class="name">Bytes sent</div>
+        </div>
+        <div class="value">
+          <div class="name" v-text="convertSize(value.bytes_sent)" />
+        </div>
+      </div>
+
+      <div class="child" v-if="value.bytes_recv">
+        <div class="col-s-12 col-m-6 label">
+          <div class="name">Bytes received</div>
+        </div>
+        <div class="value">
+          <div class="name" v-text="convertSize(value.bytes_recv)" />
+        </div>
+      </div>
+
+      <div class="child" v-if="value.packets_sent">
+        <div class="col-s-12 col-m-6 label">
+          <div class="name">Packets sent</div>
+        </div>
+        <div class="value">
+          <div class="name" v-text="value.packets_sent" />
+        </div>
+      </div>
+
+      <div class="child" v-if="value.packets_recv">
+        <div class="col-s-12 col-m-6 label">
+          <div class="name">Packets received</div>
+        </div>
+        <div class="value">
+          <div class="name" v-text="value.packets_recv" />
+        </div>
+      </div>
+
+      <div class="child" v-if="value.errors_in">
+        <div class="col-s-12 col-m-6 label">
+          <div class="name">Inbound errors</div>
+        </div>
+        <div class="value">
+          <div class="name" v-text="value.errors_in" />
+        </div>
+      </div>
+
+      <div class="child" v-if="value.errors_out">
+        <div class="col-s-12 col-m-6 label">
+          <div class="name">Outbound errors</div>
+        </div>
+        <div class="value">
+          <div class="name" v-text="value.errors_out" />
+        </div>
+      </div>
+
+      <div class="child" v-if="value.drop_in">
+        <div class="col-s-12 col-m-6 label">
+          <div class="name">Dropped inbound packets</div>
+        </div>
+        <div class="value">
+          <div class="name" v-text="value.drop_in" />
+        </div>
+      </div>
+
+      <div class="child" v-if="value.drop_out">
+        <div class="col-s-12 col-m-6 label">
+          <div class="name">Dropped outbound packets</div>
+        </div>
+        <div class="value">
+          <div class="name" v-text="value.drop_out" />
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import EntityMixin from "./EntityMixin"
+import EntityIcon from "./EntityIcon"
+
+export default {
+  name: 'NetworkInterface',
+  components: {EntityIcon},
+  mixins: [EntityMixin],
+
+  data() {
+    return {
+      isCollapsed: true,
+    }
+  },
+}
+</script>
+
+<style lang="scss" scoped>
+@import "common";
+
+.entity {
+  .head {
+    padding: 0.25em;
+
+    .icon {
+      margin-right: 1em;
+    }
+  }
+}
+
+.collapse-toggler {
+  display: flex;
+  align-items: center;
+  flex: 1;
+  min-height: 3em;
+  cursor: pointer;
+
+  &:hover {
+    color: $default-hover-fg;
+  }
+}
+
+.attributes .child {
+  margin: 0 -0.5em;
+  padding: 0.5em 1em;
+
+  &:not(:last-child) {
+    border-bottom: 1px solid $border-color-1;
+  }
+
+  &:hover {
+    cursor: initial;
+  }
+
+  .label {
+    font-weight: bold;
+    @include from($tablet) {
+      @extend .col-m-6;
+    }
+  }
+
+  .value {
+    font-size: 0.95em;
+    text-align: right;
+
+    @include from($tablet) {
+      @extend .col-m-6;
+    }
+  }
+}
+</style>
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 436a8852..801665c7 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 dd9d4040..bdadc9dd 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 b3fe3538..98374a2e 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 3e0d2b4d..4b71e3c4 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 adcd0577..00000000
--- 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 00000000..2e45aae8
--- /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 00000000..aa7addaf
--- /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 00000000..e2c2b01e
--- /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 00000000..eb49a510
--- /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 00000000..2f4f30b5
--- /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 00000000..f3cbf939
--- /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 00000000..7f8ac1b6
--- /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 00000000..35826e7d
--- /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 00000000..8cfd6a19
--- /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 00000000..564910ae
--- /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 00000000..c98dc5a7
--- /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 00000000..73bf1c51
--- /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 00000000..7de7f66d
--- /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 00000000..9d7bc21d
--- /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 00000000..89bd7261
--- /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 00000000..969c9549
--- /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 00000000..b0e2da2b
--- /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 00000000..98db4e4b
--- /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 00000000..e960b111
--- /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 00000000..0bd2992f
--- /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)