diff --git a/docs/source/conf.py b/docs/source/conf.py index 7128a749..5aaaa375 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -236,6 +236,8 @@ autodoc_mock_imports = ['googlesamples.assistant.grpc.audio_helpers', 'pyfirmata2', 'cups', 'graphyte', + 'cpuinfo', + 'psutil', ] sys.path.insert(0, os.path.abspath('../..')) diff --git a/docs/source/platypush/plugins/system.rst b/docs/source/platypush/plugins/system.rst new file mode 100644 index 00000000..c0eadf1f --- /dev/null +++ b/docs/source/platypush/plugins/system.rst @@ -0,0 +1,5 @@ +``platypush.plugins.system`` +============================ + +.. automodule:: platypush.plugins.system + :members: diff --git a/docs/source/plugins.rst b/docs/source/plugins.rst index ba72f741..3677dba8 100644 --- a/docs/source/plugins.rst +++ b/docs/source/plugins.rst @@ -88,6 +88,7 @@ Plugins platypush/plugins/switch.switchbot.rst platypush/plugins/switch.tplink.rst platypush/plugins/switch.wemo.rst + platypush/plugins/system.rst platypush/plugins/tcp.rst platypush/plugins/todoist.rst platypush/plugins/torrent.rst diff --git a/platypush/message/response/system/__init__.py b/platypush/message/response/system/__init__.py new file mode 100644 index 00000000..3061aa32 --- /dev/null +++ b/platypush/message/response/system/__init__.py @@ -0,0 +1,504 @@ +from datetime import datetime +from typing import Optional, List, Union + +from platypush.message.response import Response + + +class SystemResponse(Response): + pass + + +class CpuResponse(SystemResponse): + pass + + +class MemoryResponse(SystemResponse): + pass + + +class DiskResponse(SystemResponse): + pass + + +class NetworkResponse(SystemResponse): + pass + + +class SensorResponse(SystemResponse): + pass + + +class CpuInfoResponse(CpuResponse): + def __init__(self, + arch: str, + bits: int, + count: int, + vendor_id: str, + brand: str, + hz_advertised: int, + hz_actual: int, + model: int, + flags: List[str], + family: Optional[int], + stepping: Optional[int], + l1_instruction_cache_size: Optional[Union[int, str]], + l1_data_cache_size: Optional[Union[int, str]], + l2_cache_size: Optional[Union[int, str]], + l3_cache_size: Optional[Union[int, str]], + *args, **kwargs): + super().__init__( + *args, output={ + 'arch': arch, + 'bits': bits, + 'count': count, + 'vendor_id': vendor_id, + 'brand': brand, + 'hz_advertised': hz_advertised, + 'hz_actual': hz_actual, + 'stepping': stepping, + 'model': model, + 'family': family, + 'flags': flags, + 'l1_instruction_cache_size': l1_instruction_cache_size, + 'l1_data_cache_size': l1_data_cache_size, + 'l2_cache_size': l2_cache_size, + 'l3_cache_size': l3_cache_size, + }, **kwargs + ) + + +class CpuTimesResponse(CpuResponse): + def __init__(self, + user: float, + nice: float, + system: float, + idle: float, + iowait: float, + irq: float, + softirq: float, + steal: float, + guest: float, + guest_nice: float, + *args, **kwargs): + super().__init__( + *args, output={ + 'user': user, + 'nice': nice, + 'system': system, + 'idle': idle, + 'iowait': iowait, + 'irq': irq, + 'softirq': softirq, + 'steal': steal, + 'guest': guest, + 'guest_nice': guest_nice, + }, **kwargs + ) + + +class CpuStatsResponse(CpuResponse): + def __init__(self, + ctx_switches: int, + interrupts: int, + soft_interrupts: int, + syscalls: int, + *args, **kwargs): + super().__init__( + *args, output={ + 'ctx_switches': ctx_switches, + 'interrupts': interrupts, + 'soft_interrupts': soft_interrupts, + 'syscalls': syscalls, + }, **kwargs + ) + + +class CpuFrequencyResponse(CpuResponse): + # noinspection PyShadowingBuiltins + def __init__(self, + min: int, + max: int, + current: int, + *args, **kwargs): + super().__init__( + *args, output={ + 'min': min, + 'max': max, + 'current': current, + }, **kwargs + ) + + +class VirtualMemoryUsageResponse(MemoryResponse): + def __init__(self, + total: int, + available: int, + percent: float, + used: int, + free: int, + active: int, + inactive: int, + buffers: int, + cached: int, + shared: int, + *args, **kwargs): + super().__init__( + *args, output={ + 'total': total, + 'available': available, + 'percent': percent, + 'used': used, + 'free': free, + 'active': active, + 'inactive': inactive, + 'buffers': buffers, + 'cached': cached, + 'shared': shared, + }, **kwargs + ) + + +class SwapMemoryUsageResponse(MemoryResponse): + def __init__(self, + total: int, + percent: float, + used: int, + free: int, + sin: int, + sout: int, + *args, **kwargs): + super().__init__( + *args, output={ + 'total': total, + 'percent': percent, + 'used': used, + 'free': free, + 'sin': sin, + 'sout': sout, + }, **kwargs + ) + + +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, + 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__(self, + fd: int, + family: str, + type: str, + local_address: str, + local_port: int, + remote_address: str, + remote_port: int, + status: str, + pid: int, + *args, **kwargs): + super().__init__( + *args, output={ + 'fd': fd, + 'family': family, + 'type': type, + 'local_address': local_address, + 'local_port': local_port, + 'remote_address': remote_address, + 'remote_port': remote_port, + 'status': status, + 'pid': pid, + }, **kwargs + ) + + +class NetworkAddressResponse(NetworkResponse): + def __init__(self, + nic: str, + ipv4_address: Optional[str] = None, + ipv4_netmask: Optional[str] = None, + ipv4_broadcast: Optional[str] = None, + ipv6_address: Optional[str] = None, + ipv6_netmask: Optional[str] = None, + ipv6_broadcast: Optional[str] = None, + mac_address: Optional[str] = None, + mac_broadcast: Optional[str] = None, + ptp: Optional[str] = None, + *args, **kwargs): + super().__init__( + *args, output={ + 'nic': nic, + 'ipv4_address': ipv4_address, + 'ipv4_netmask': ipv4_netmask, + 'ipv4_broadcast': ipv4_broadcast, + 'ipv6_address': ipv6_address, + 'ipv6_netmask': ipv6_netmask, + 'ipv6_broadcast': ipv6_broadcast, + 'mac_address': mac_address, + 'mac_broadcast': mac_broadcast, + 'ptp': ptp, + }, **kwargs + ) + + +class NetworkInterfaceStatsResponse(NetworkResponse): + def __init__(self, + nic: str, + is_up: bool, + duplex: str, + speed: int, + mtu: int, + *args, **kwargs): + super().__init__( + *args, output={ + 'nic': nic, + 'is_up': is_up, + 'duplex': duplex, + 'speed': speed, + 'mtu': mtu, + }, **kwargs + ) + + +class SensorTemperatureResponse(SensorResponse): + def __init__(self, + name: str, + current: float, + high: Optional[float] = None, + critical: Optional[float] = None, + label: Optional[str] = None, + *args, **kwargs): + super().__init__( + *args, output={ + 'name': name, + 'current': current, + 'high': high, + 'critical': critical, + 'label': label, + }, **kwargs + ) + + +class SensorFanResponse(SensorResponse): + def __init__(self, + name: str, + current: int, + label: Optional[str] = None, + *args, **kwargs): + super().__init__( + *args, output={ + 'name': name, + 'current': current, + 'label': label, + }, **kwargs + ) + + +class SensorBatteryResponse(SensorResponse): + def __init__(self, + percent: float, + secsleft: int, + power_plugged: bool, + *args, **kwargs): + super().__init__( + *args, output={ + 'percent': percent, + 'secsleft': secsleft, + 'power_plugged': power_plugged, + }, **kwargs + ) + + +class ConnectUserResponse(SystemResponse): + def __init__(self, + name: str, + terminal: str, + host: str, + started: datetime, + pid: Optional[int] = None, + *args, **kwargs): + super().__init__( + *args, output={ + 'name': name, + 'terminal': terminal, + 'host': host, + 'started': started, + 'pid': pid, + }, **kwargs + ) + + +class ProcessResponse(SystemResponse): + def __init__(self, + pid: int, + name: str, + started: datetime, + ppid: Optional[int], + children: Optional[List[int]] = None, + exe: Optional[List[str]] = None, + status: Optional[str] = None, + username: Optional[str] = None, + terminal: Optional[str] = None, + cpu_user_time: Optional[float] = None, + cpu_system_time: Optional[float] = None, + cpu_children_user_time: Optional[float] = None, + cpu_children_system_time: Optional[float] = None, + mem_rss: Optional[int] = None, + mem_vms: Optional[int] = None, + mem_shared: Optional[int] = None, + mem_text: Optional[int] = None, + mem_data: Optional[int] = None, + mem_lib: Optional[int] = None, + mem_dirty: Optional[int] = None, + mem_percent: Optional[float] = None, + *args, **kwargs): + super().__init__( + *args, output={ + 'pid': pid, + 'name': name, + 'started': started, + 'ppid': ppid, + 'exe': exe, + 'status': status, + 'username': username, + 'terminal': terminal, + 'cpu_user_time': cpu_user_time, + 'cpu_system_time': cpu_system_time, + 'cpu_children_user_time': cpu_children_user_time, + 'cpu_children_system_time': cpu_children_system_time, + 'mem_rss': mem_rss, + 'mem_vms': mem_vms, + 'mem_shared': mem_shared, + 'mem_text': mem_text, + 'mem_data': mem_data, + 'mem_lib': mem_lib, + 'mem_dirty': mem_dirty, + 'mem_percent': mem_percent, + 'children': children or [], + }, **kwargs + ) + + +class SystemResponseList(SystemResponse): + def __init__(self, responses: List[SystemResponse], *args, **kwargs): + super().__init__(output=[r.output for r in responses], *args, **kwargs) + + +class CpuResponseList(CpuResponse, SystemResponseList): + def __init__(self, responses: List[CpuResponse], *args, **kwargs): + super().__init__(responses=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) + + +class SensorResponseList(SensorResponse, SystemResponseList): + def __init__(self, responses: List[SensorResponse], *args, **kwargs): + super().__init__(responses=responses, *args, **kwargs) + + +class ConnectedUserResponseList(SystemResponseList): + def __init__(self, responses: List[ConnectUserResponse], *args, **kwargs): + super().__init__(responses=responses, *args, **kwargs) + + +class ProcessResponseList(SystemResponseList): + def __init__(self, responses: List[ProcessResponse], *args, **kwargs): + super().__init__(responses=responses, *args, **kwargs) + + +# vim:sw=4:ts=4:et: diff --git a/platypush/plugins/system/__init__.py b/platypush/plugins/system/__init__.py new file mode 100644 index 00000000..37882199 --- /dev/null +++ b/platypush/plugins/system/__init__.py @@ -0,0 +1,628 @@ +import socket + +from datetime import datetime +from typing import Union, List, Optional + +from platypush.message.response.system import CpuInfoResponse, CpuTimesResponse, CpuResponseList, CpuStatsResponse, \ + CpuFrequencyResponse, VirtualMemoryUsageResponse, SwapMemoryUsageResponse, DiskResponseList, \ + DiskPartitionResponse, DiskUsageResponse, DiskIoCountersResponse, NetworkIoCountersResponse, NetworkResponseList, \ + NetworkConnectionResponse, NetworkAddressResponse, NetworkInterfaceStatsResponse, SensorTemperatureResponse, \ + SensorResponseList, SensorFanResponse, SensorBatteryResponse, ConnectedUserResponseList, ConnectUserResponse, \ + ProcessResponseList, ProcessResponse + +from platypush.plugins import Plugin, action + + +class SystemPlugin(Plugin): + """ + Plugin to get system info. + + Requires: + + - **py-cpuinfo** (``pip install py-cpuinfo``) for CPU model and info. + - **psutil** (``pip install psutil``) for CPU load and stats. + + """ + + @action + def cpu_info(self) -> CpuInfoResponse: + """ + Get CPU info. + :return: :class:`platypush.message.response.system.CpuInfoResponse` + """ + from cpuinfo import get_cpu_info + info = get_cpu_info() + + return CpuInfoResponse( + arch=info.get('raw_arch_string'), + bits=info.get('bits'), + count=info.get('count'), + vendor_id=info.get('vendor_id'), + brand=info.get('brand'), + hz_advertised=info.get('hz_advertised_raw')[0], + hz_actual=info.get('hz_actual_raw')[0], + stepping=info.get('stepping'), + model=info.get('model'), + family=info.get('family'), + flags=info.get('flags'), + l1_instruction_cache_size=info.get('l1_instruction_cache_size'), + l1_data_cache_size=info.get('l1_data_cache_size'), + l2_cache_size=info.get('l2_cache_size'), + l3_cache_size=info.get('l3_cache_size'), + ) + + @action + def cpu_times(self, per_cpu=False, percent=False) -> Union[CpuTimesResponse, CpuResponseList]: + """ + Get the CPU times stats. + + :param per_cpu: Get per-CPU stats (default: False). + :param percent: Get the stats in percentage (default: False). + :return: :class:`platypush.message.response.system.CpuTimesResponse` + """ + import psutil + + times = psutil.cpu_times_percent(percpu=per_cpu) if percent else \ + psutil.cpu_times(percpu=per_cpu) + + if per_cpu: + return CpuResponseList([ + CpuTimesResponse( + user=t.user, + nice=t.nice, + system=t.system, + idle=t.idle, + iowait=t.iowait, + irq=t.irq, + softirq=t.softirq, + steal=t.steal, + guest=t.guest, + guest_nice=t.guest_nice, + ) + for t in times + ]) + + return CpuTimesResponse( + user=times.user, + nice=times.nice, + system=times.system, + idle=times.idle, + iowait=times.iowait, + irq=times.irq, + softirq=times.softirq, + steal=times.steal, + guest=times.guest, + guest_nice=times.guest_nice, + ) + + @action + def cpu_percent(self, per_cpu: bool = False, interval: Optional[float] = None) -> Union[float, List[float]]: + """ + Get the CPU load percentage. + + :param per_cpu: Get per-CPU stats (default: False). + :param interval: When *interval* is 0.0 or None compares system CPU times elapsed since last call or module + import, returning immediately (non blocking). That means the first time this is called it will + return a meaningless 0.0 value which you should ignore. In this case is recommended for accuracy that this + 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: + return [p for p in percent] + return percent + + @action + def cpu_stats(self) -> CpuStatsResponse: + """ + Get CPU stats. + :return: :class:`platypush.message.response.system.CpuStatsResponse` + """ + import psutil + stats = psutil.cpu_stats() + + return CpuStatsResponse( + ctx_switches=stats.ctx_switches, + interrupts=stats.interrupts, + soft_interrupts=stats.soft_interrupts, + syscalls=stats.syscalls, + ) + + @action + def cpu_frequency(self, per_cpu: bool = False) -> Union[CpuFrequencyResponse, CpuResponseList]: + """ + Get CPU stats. + + :param per_cpu: Get per-CPU stats (default: False). + :return: :class:`platypush.message.response.system.CpuFrequencyResponse` + """ + import psutil + freq = psutil.cpu_freq(percpu=per_cpu) + + if per_cpu: + return CpuResponseList([ + CpuFrequencyResponse( + min=f.min, + max=f.max, + current=f.current, + ) + for f in freq + ]) + + return CpuFrequencyResponse( + min=freq.min, + max=freq.max, + current=freq.current, + ) + + @action + def load_avg(self) -> List[float]: + """ + Get the average load as a vector that represents the load within the last 1, 5 and 15 minutes. + """ + import psutil + return psutil.getloadavg() + + @action + def mem_virtual(self) -> VirtualMemoryUsageResponse: + """ + Get the current virtual memory usage stats. + :return: list of :class:`platypush.message.response.system.VirtualMemoryUsageResponse` + """ + import psutil + mem = psutil.virtual_memory() + return VirtualMemoryUsageResponse( + total=mem.total, + available=mem.available, + percent=mem.percent, + used=mem.used, + free=mem.free, + active=mem.active, + inactive=mem.inactive, + buffers=mem.buffers, + cached=mem.cached, + shared=mem.shared, + ) + + @action + def mem_swap(self) -> SwapMemoryUsageResponse: + """ + Get the current virtual memory usage stats. + :return: list of :class:`platypush.message.response.system.SwapMemoryUsageResponse` + """ + import psutil + mem = psutil.swap_memory() + return SwapMemoryUsageResponse( + total=mem.total, + percent=mem.percent, + used=mem.used, + free=mem.free, + sin=mem.sin, + sout=mem.sout, + ) + + @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 + parts = psutil.disk_partitions() + return DiskResponseList([ + DiskPartitionResponse( + device=p.device, + mount_point=p.mountpoint, + fstype=p.fstype, + opts=p.opts, + ) for p in parts + ]) + + @action + def disk_usage(self, path: Optional[str] = None) -> Union[DiskUsageResponse, DiskResponseList]: + """ + Get the usage of a mounted disk. + + :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`. + """ + 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() + ]) + + @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`. + """ + import psutil + + 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() + ]) + + # noinspection PyShadowingBuiltins + @action + def net_connections(self, type: Optional[str] = None) -> Union[NetworkConnectionResponse, NetworkResponseList]: + """ + Get the list of active network connections. + On macOS this function requires root privileges. + + :param type: Connection type to filter. Supported types: + + +------------+----------------------------------------------------+ + | Kind Value | Connections using | + +------------+----------------------------------------------------+ + | inet | IPv4 and IPv6 | + | inet4 | IPv4 | + | inet6 | IPv6 | + | tcp | TCP | + | tcp4 | TCP over IPv4 | + | tcp6 | TCP over IPv6 | + | udp | UDP | + | udp4 | UDP over IPv4 | + | udp6 | UDP over IPv6 | + | unix | UNIX socket (both UDP and TCP protocols) | + | all | the sum of all the possible families and protocols | + +------------+----------------------------------------------------+ + + :return: List of :class:`platypush.message.response.system.NetworkConnectionResponse`. + """ + import psutil + conns = psutil.net_connections(kind=type) + + return NetworkResponseList([ + NetworkConnectionResponse( + fd=conn.fd, + family=conn.family.name, + type=conn.type.name, + local_address=conn.laddr[0] if conn.laddr else None, + local_port=conn.laddr[1] if len(conn.laddr) > 1 else None, + remote_address=conn.raddr[0] if conn.raddr else None, + remote_port=conn.raddr[1] if len(conn.raddr) > 1 else None, + status=conn.status, + pid=conn.pid, + ) for conn in conns + ]) + + @action + def net_addresses(self, nic: Optional[str] = None) -> Union[NetworkAddressResponse, NetworkResponseList]: + """ + Get address info associated to the network interfaces. + + :param nic: Select the stats for a specific network device (e.g. 'eth0'). Default: get stats for all NICs. + :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): + args = {'nic': _nic} + + for addr in _addrs: + if addr.family == socket.AddressFamily.AF_INET: + args.update({ + 'ipv4_address': addr.address, + 'ipv4_netmask': addr.netmask, + 'ipv4_broadcast': addr.broadcast, + }) + elif addr.family == socket.AddressFamily.AF_INET6: + args.update({ + 'ipv6_address': addr.address, + 'ipv6_netmask': addr.netmask, + 'ipv6_broadcast': addr.broadcast, + }) + elif addr.family == socket.AddressFamily.AF_PACKET: + args.update({ + 'mac_address': addr.address, + 'mac_broadcast': addr.broadcast, + }) + + if addr.ptp and not args.get('ptp'): + args['ptp'] = addr.ptp + + return NetworkAddressResponse(**args) + + if nic: + addrs = [addr for name, addr in addrs.items() if name == nic] + assert addrs, 'No such network interface: {}'.format(nic) + addr = addrs[0] + return _expand_addresses(nic, addr) + + return NetworkResponseList([ + _expand_addresses(nic, addr) + for nic, addr in addrs.items() + ]) + + @action + def net_stats(self, nic: Optional[str] = None) -> Union[NetworkInterfaceStatsResponse, NetworkResponseList]: + """ + Get stats about the network interfaces. + + :param nic: Select the stats for a specific network device (e.g. 'eth0'). Default: get stats for all NICs. + :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): + return NetworkInterfaceStatsResponse( + nic=_nic, + is_up=_stats.isup, + duplex=_stats.duplex.name, + speed=_stats.speed, + mtu=_stats.mtu, + ) + + if nic: + stats = [addr for name, addr in stats.items() if name == nic] + assert stats, 'No such network interface: {}'.format(nic) + return _expand_stats(nic, stats[0]) + + return NetworkResponseList([ + _expand_stats(nic, addr) + for nic, addr in stats.items() + ]) + + # noinspection DuplicatedCode + @action + def sensors_temperature(self, sensor: Optional[str] = None, fahrenheit: bool = False) -> SensorResponseList: + """ + Get stats from the temperature sensors. + + :param sensor: Select the sensor name. + :param fahrenheit: Return the temperature in Fahrenheit (default: Celsius). + :return: List of :class:`platypush.message.response.system.SensorTemperatureResponse`. + """ + import psutil + stats = psutil.sensors_temperatures(fahrenheit=fahrenheit) + + def _expand_stats(name, _stats): + return SensorResponseList([ + SensorTemperatureResponse( + name=name, + current=s.current, + high=s.high, + critical=s.critical, + label=s.label, + ) + for s in _stats + ]) + + if sensor: + stats = [addr for name, addr in stats.items() if name == sensor] + assert stats, 'No such sensor name: {}'.format(sensor) + return _expand_stats(sensor, stats[0]) + + return SensorResponseList([ + _expand_stats(name, stat) + for name, stat in stats.items() + ]) + + # noinspection DuplicatedCode + @action + def sensors_fan(self, sensor: Optional[str] = None) -> SensorResponseList: + """ + Get stats from the fan sensors. + + :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): + return SensorResponseList([ + SensorFanResponse( + name=name, + current=s.current, + label=s.label, + ) + for s in _stats + ]) + + if sensor: + stats = [addr for name, addr in stats.items() if name == sensor] + assert stats, 'No such sensor name: {}'.format(sensor) + return _expand_stats(sensor, stats[0]) + + return SensorResponseList([ + _expand_stats(name, stat) + for name, stat in stats.items() + ]) + + @action + def sensors_battery(self) -> SensorBatteryResponse: + """ + Get stats from the battery sensor. + :return: List of :class:`platypush.message.response.system.SensorFanResponse`. + """ + import psutil + stats = psutil.sensors_battery() + + return SensorBatteryResponse( + percent=stats.percent, + secsleft=stats.secsleft, + power_plugged=stats.power_plugged, + ) + + @action + def connected_users(self) -> ConnectedUserResponseList: + """ + Get the list of connected users. + :return: List of :class:`platypush.message.response.system.ConnectUserResponse`. + """ + import psutil + users = psutil.users() + + return ConnectedUserResponseList([ + ConnectUserResponse( + name=u.name, + terminal=u.terminal, + host=u.host, + started=datetime.fromtimestamp(u.started), + pid=u.pid, + ) + for u in users + ]) + + # noinspection PyShadowingBuiltins + @action + def processes(self, filter: Optional[str] = '') -> ProcessResponseList: + """ + Get the list of running processes. + + :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 = [] + + for p in processes: + if filter and filter not in p.name(): + continue + + args = {} + + try: + args.update( + pid=p.pid, + name=p.name(), + started=datetime.fromtimestamp(p.create_time()), + ppid=p.ppid(), + children=[pp.pid for pp in p.children()], + status=p.status(), + username=p.username(), + terminal=p.terminal(), + cpu_user_time=p.cpu_times().user, + cpu_system_time=p.cpu_times().system, + cpu_children_user_time=p.cpu_times().children_user, + cpu_children_system_time=p.cpu_times().children_system, + mem_rss=p.memory_info().rss, + mem_vms=p.memory_info().vms, + mem_shared=p.memory_info().shared, + mem_text=p.memory_info().text, + mem_data=p.memory_info().data, + mem_lib=p.memory_info().lib, + mem_dirty=p.memory_info().dirty, + mem_percent=p.memory_percent(), + ) + except psutil.Error: + continue + + try: + args.update( + exe=p.exe(), + ) + except psutil.Error: + pass + + p_list.append(ProcessResponse(**args)) + + return ProcessResponseList(p_list) + + +# vim:sw=4:ts=4:et: diff --git a/requirements.txt b/requirements.txt index 0b20bad6..c410c652 100644 --- a/requirements.txt +++ b/requirements.txt @@ -217,3 +217,7 @@ croniter # Support for Graphite integration # graphyte +# Support for CPU and memory monitoring and info +# py-cpuinfo +# psutil + diff --git a/setup.py b/setup.py index 25d755a1..b869f71a 100755 --- a/setup.py +++ b/setup.py @@ -273,5 +273,7 @@ setup( 'cups': ['pycups'], # Support for Graphite integration 'graphite': ['graphyte'], + # Support for CPU and memory monitoring and info + 'sys': ['py-cpuinfo', 'psutil'], }, )