Ralph,

Try this version of cmon.py. I did a quick port to Python 3. It should work
with Python 2 or 3, and under either WeeWX V3.9, or 4.0.

-tk

On Mon, Dec 16, 2019 at 6:53 PM Ralph Underwood <[email protected]> wrote:

> I found most of those items, but I was still getting a bunch of errors. My
> system running 3.?? died a few days ago and my backups were not good so I
> was installing the version 4 as a replacement - consequently I had less
> that 24 hours data on the system I mistakenly installed cmon on.
>
> I am interested in cmon because I want to track down why my "old" system
> was periodically crashing - I was suspecting a memory leak. The memory in
> use would just creep up, seemed to crash
>
> I just made a clean installation using a newly imaged card and Vince's
> script. Copied over the latest ultimeter driver and I am up and running.
>
> I still have to get my MQTT stuff working again - I stumbled upon how to
> get the python 3 version of paho.mqtt installed yesterday, but didn't make
> good notes.
>
> Thanks again to Tom, Vince and Gary!
>
>
>
>
> --
> You received this message because you are subscribed to the Google Groups
> "weewx-development" group.
> To unsubscribe from this group and stop receiving emails from it, send an
> email to [email protected].
> To view this discussion on the web visit
> https://groups.google.com/d/msgid/weewx-development/c5f6e1cb-4934-4ae3-b62c-aab93668bd07%40googlegroups.com
> <https://groups.google.com/d/msgid/weewx-development/c5f6e1cb-4934-4ae3-b62c-aab93668bd07%40googlegroups.com?utm_medium=email&utm_source=footer>
> .
>

-- 
You received this message because you are subscribed to the Google Groups 
"weewx-development" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to [email protected].
To view this discussion on the web visit 
https://groups.google.com/d/msgid/weewx-development/CAPq0zECp_PGoOtZwn_Zi320%3DDFa14b4bYxXvNOrkzOU_kgfc_g%40mail.gmail.com.
# $Id: cmon.py 1651 2017-01-16 18:10:37Z mwall $
# Copyright 2013 Matthew Wall
"""weewx module that records cpu, memory, disk, and network usage.

This file contains both a weewx driver and a weewx service.

Installation

Put this file in the bin/user directory.


Service Configuration

Add the following to weewx.conf:

[ComputerMonitor]
    data_binding = cmon_binding
    max_age = 2592000 # 30 days; None to store indefinitely

[DataBindings]
    [[cmon_binding]]
        database = cmon_sqlite
        manager = weewx.manager.DaySummaryManager
        table_name = archive
        schema = user.cmon.schema

[Databases]
    [[cmon_sqlite]]
        root = %(WEEWX_ROOT)s
        database_name = archive/cmon.sdb
        driver = weedb.sqlite

[Engine]
    [[Services]]
        archive_services = ..., user.cmon.ComputerMonitor


Driver Configuration

Add the following to weewx.conf:

[Station]
    station_type = ComputerMonitor

[ComputerMonitor]
    polling_interval = 30
    driver = user.cmon


Schema

The default schema is defined in this file.  If you prefer to maintain a schema
different than the default, specify the desired schema in the configuration.
For example, this would be a schema that stores only memory and network data,
and uses eth1 instead of the default eth0:

[DataBindings]
    [[cmon_binding]]
        database = cmon_sqlite
        manager = weewx.manager.DaySummaryManager
        table_name = archive
        [[[schema]]]
            dateTime = INTEGER NOT NULL PRIMARY KEY
            usUnits = INTEGER NOT NULL
            interval = INTEGER NOT NULL
            mem_total = INTEGER
            mem_free = INTEGER
            mem_used = INTEGER
            swap_total = INTEGER
            swap_free = INTEGER
            swap_used = INTEGER
            net_eth1_rbytes = INTEGER
            net_eth1_rpackets = INTEGER
            net_eth1_rerrs = INTEGER
            net_eth1_rdrop = INTEGER
            net_eth1_tbytes = INTEGER
            net_eth1_tpackets = INTEGER
            net_eth1_terrs = INTEGER
            net_eth1_tdrop = INTEGER

Another approach to maintaining a custom schema is to define the schema in the
file user/extensions.py as cmonSchema:

cmonSchema = [
    ('dateTime', 'INTEGER NOT NULL PRIMARY KEY'),
    ('usUnits', 'INTEGER NOT NULL'),
    ('interval', 'INTEGER NOT NULL'),
    ('mem_total','INTEGER'),
    ('mem_free','INTEGER'),
    ('mem_used','INTEGER'),
    ('net_eth1_rbytes','INTEGER'),
    ('net_eth1_rpackets','INTEGER'),
    ('net_eth1_rerrs','INTEGER'),
    ('net_eth1_rdrop','INTEGER'),
    ('net_eth1_tbytes','INTEGER'),
    ('net_eth1_tpackets','INTEGER'),
    ('net_eth1_terrs','INTEGER'),
    ('net_eth1_tdrop','INTEGER'),
    ]

then load it using this configuration:

[DataBindings]
    [[cmon_binding]]
        database = cmon_sqlite
        manager = weewx.manager.DaySummaryManager
        table_name = archive
        schema = user.extensions.cmonSchema
"""

# FIXME: make these methods platform-independent instead of linux-specific
# FIXME: deal with MB/GB in memory sizes
# FIXME: save the total counts instead of the deltas
# FIXME: refactor ups and rpi specialties


from __future__ import with_statement
from __future__ import absolute_import
from __future__ import print_function
import os
import platform
import re
import time
from subprocess import Popen, PIPE
from builtins import input

import weewx
import weeutil.weeutil
from weewx.drivers import AbstractDevice
from weewx.engine import StdService

try:
    # Test for new-style weewx logging by trying to import weeutil.logger
    import weeutil.logger
    import logging
    log = logging.getLogger(__name__)

    def logdbg(msg):
        log.debug(msg)

    def loginf(msg):
        log.info(msg)

    def logerr(msg):
        log.error(msg)

except ImportError:
    # Old-style weewx logging
    import syslog

    def logmsg(level, msg):
        syslog.syslog(level, 'filepile: %s:' % msg)

    def logdbg(msg):
        logmsg(syslog.LOG_DEBUG, msg)

    def loginf(msg):
        logmsg(syslog.LOG_INFO, msg)

    def logerr(msg):
        logmsg(syslog.LOG_ERR, msg)

DRIVER_NAME = "ComputerMonitor"
DRIVER_VERSION = "0.20"

if weewx.__version__ < "3":
    raise weewx.UnsupportedFeature("weewx 3 is required, found %s" %
                                   weewx.__version__)

schema = [
    ('dateTime', 'INTEGER NOT NULL PRIMARY KEY'),
    ('usUnits', 'INTEGER NOT NULL'),
    ('interval', 'INTEGER NOT NULL'),
    ('mem_total', 'INTEGER'),
    ('mem_free', 'INTEGER'),
    ('mem_used', 'INTEGER'),
    ('swap_total', 'INTEGER'),
    ('swap_free', 'INTEGER'),
    ('swap_used', 'INTEGER'),
    ('cpu_user', 'INTEGER'),
    ('cpu_nice', 'INTEGER'),
    ('cpu_system', 'INTEGER'),
    ('cpu_idle', 'INTEGER'),
    ('cpu_iowait', 'INTEGER'),
    ('cpu_irq', 'INTEGER'),
    ('cpu_softirq', 'INTEGER'),
    ('load1', 'REAL'),
    ('load5', 'REAL'),
    ('load15', 'REAL'),
    ('proc_active', 'INTEGER'),
    ('proc_total', 'INTEGER'),

# measure cpu temperature (not all platforms support this)
    ('cpu_temp', 'REAL'),  # degree C
    ('cpu_temp1', 'REAL'), # degree C
    ('cpu_temp2', 'REAL'), # degree C
    ('cpu_temp3', 'REAL'), # degree C
    ('cpu_temp4', 'REAL'), # degree C

# measure rpi attributes (not all platforms support this)
#    ('core_temp','REAL'), # degree C
#    ('core_volt', 'REAL'),
#    ('core_sdram_c', 'REAL'),
#    ('core_sdram_i', 'REAL'),
#    ('core_sdram_p', 'REAL'),
#    ('arm_mem', 'REAL'),
#    ('gpu_mem', 'REAL'),

# the default interface on most linux systems is eth0
    ('net_eth0_rbytes', 'INTEGER'),
    ('net_eth0_rpackets', 'INTEGER'),
    ('net_eth0_rerrs', 'INTEGER'),
    ('net_eth0_rdrop', 'INTEGER'),
    ('net_eth0_tbytes', 'INTEGER'),
    ('net_eth0_tpackets', 'INTEGER'),
    ('net_eth0_terrs', 'INTEGER'),
    ('net_eth0_tdrop', 'INTEGER'),

#    ('net_eth1_rbytes', 'INTEGER'),
#    ('net_eth1_rpackets', 'INTEGER'),
#    ('net_eth1_rerrs', 'INTEGER'),
#    ('net_eth1_rdrop', 'INTEGER'),
#    ('net_eth1_tbytes', 'INTEGER'),
#    ('net_eth1_tpackets', 'INTEGER'),
#    ('net_eth1_terrs', 'INTEGER'),
#    ('net_eth1_tdrop', 'INTEGER'),

# some systems have a wireless interface as wlan0
    ('net_wlan0_rbytes', 'INTEGER'),
    ('net_wlan0_rpackets', 'INTEGER'),
    ('net_wlan0_rerrs', 'INTEGER'),
    ('net_wlan0_rdrop', 'INTEGER'),
    ('net_wlan0_tbytes', 'INTEGER'),
    ('net_wlan0_tpackets', 'INTEGER'),
    ('net_wlan0_terrs', 'INTEGER'),
    ('net_wlan0_tdrop', 'INTEGER'),

# if the computer is an openvpn server, track the tunnel traffic
#    ('net_tun0_rbytes', 'INTEGER'),
#    ('net_tun0_rpackets', 'INTEGER'),
#    ('net_tun0_rerrs', 'INTEGER'),
#    ('net_tun0_rdrop', 'INTEGER'),
#    ('net_tun0_tbytes', 'INTEGER'),
#    ('net_tun0_tpackets', 'INTEGER'),
#    ('net_tun0_terrs', 'INTEGER'),
#    ('net_tun0_tdrop', 'INTEGER'),

# disk volumes will vary, but root is always present
    ('disk_root_total', 'INTEGER'),
    ('disk_root_free', 'INTEGER'),
    ('disk_root_used', 'INTEGER'),
# separate partition for home is not uncommon
    ('disk_home_total', 'INTEGER'),
    ('disk_home_free', 'INTEGER'),
    ('disk_home_used', 'INTEGER'),

# measure the ups parameters if we can
    ('ups_temp', 'REAL'),    # degree C
    ('ups_load', 'REAL'),    # percent
    ('ups_charge', 'REAL'),  # percent
    ('ups_voltage', 'REAL'), # volt
    ('ups_time', 'REAL'),    # seconds
    ]

# this exension will scan for all mounted file system.  these are the
# filesystems we ignore.
IGNORED_MOUNTS = [
    '/lib/init/rw',
    '/proc',
    '/sys',
    '/dev',
    '/afs',
    '/mit',
    '/run',
    '/var/lib/nfs']


def get_collector(hardware=[], ignored_mounts=IGNORED_MOUNTS):
    # see what we are running on
    s = platform.system()
    if s == 'Linux':
        return LinuxCollector(hardware, ignored_mounts)
    elif s == 'Darwin':
        return MacOSXCollector(hardware, ignored_mounts)
    elif s == 'BSD':
        return BSDCollector(hardware, ignored_mounts)
    raise Exception('unsupported system %s' % s)


class Collector(object):

    _CPU_KEYS = ['user','nice','system','idle','iowait','irq','softirq']

    # bytes received
    # packets received
    # packets dropped
    # fifo buffer errors
    # packet framing errors
    # compressed packets
    # multicast frames
    _NET_KEYS = [
        'rbytes','rpackets','rerrs','rdrop','rfifo','rframe','rcomp','rmulti',
        'tbytes','tpackets','terrs','tdrop','tfifo','tframe','tcomp','tmulti'
        ]

    def __init__(self, hardware):
        self.hardware = hardware

    def get_data(self, now_ts=None):
        if now_ts is None:
            now_ts = int(time.time() + 0.5)
        record = dict()
        record['dateTime'] = now_ts
        record['usUnits'] = weewx.METRIC
        if 'rpi' in self.hardware:
            record.update(self._get_rpi_info())
        if 'apcups' in self.hardware:
            record.update(self._get_apcups_info())
        return record

    _DIGITS = re.compile('[\d.]+')

    @staticmethod
    def _get_apcups_info():
        record = dict()
        try:
            cmd = '/sbin/apcaccess'
            p = Popen(cmd, shell=True, stdout=PIPE)
            o = p.communicate()[0]
            for line in o.split('\n'):
                if line.startswith('ITEMP'):
                    m = Collector._DIGITS.search(line)
                    if m:
                        record['ups_temp'] = float(m.group())
                elif line.startswith('LOADPCT'):
                    m = Collector._DIGITS.search(line)
                    if m:
                        record['ups_load'] = float(m.group())
                elif line.startswith('BCHARGE'):
                    m = Collector._DIGITS.search(line)
                    if m:
                        record['ups_charge'] = float(m.group())
                elif line.startswith('OUTPUTV') or line.startswith('BATTV'):
                    m = Collector._DIGITS.search(line)
                    if m:
                        record['ups_voltage'] = float(m.group())
                elif line.startswith('TIMELEFT'):
                    m = Collector._DIGITS.search(line)
                    if m:
                        record['ups_time'] = float(m.group())
        except (ValueError, IOError, KeyError) as e:
            logerr('apcups_info failed: %s' % e)
        return record

    _RPI_VCGENCMD = '/opt/vc/bin/vcgencmd'
    _RPI_VOLT = re.compile('([^:]+):\s+volt=([\d.]+)')
    _RPI_MEM = re.compile('([^=]+)=([\d]+)')

    @staticmethod
    def _get_rpi_info():
        # get raspberry pi measurements
        record = dict()
        try:
            cmd = '%s measure_temp' % Collector._RPI_VCGENCMD
            p = Popen(cmd, shell=True, stdout=PIPE)
            o = p.communicate()[0]
            record['core_temp'] = float(o.replace("'C\n", '').partition('=')[2])
            cmd = '%s measure_volts' % Collector._RPI_VCGENCMD
            p = Popen(cmd, shell=True, stdout=PIPE)
            o = p.communicate()[0]
            for line in o.split('\n'):
                m = Collector._RPI_VOLT.search(line)
                if m:
                    record[m.group(1) + '_volt'] = float(m.group(2))
            cmd = '%s measure_volts' % Collector._RPI_VCGENCMD
            p = Popen(cmd, shell=True, stdout=PIPE)
            o = p.communicate()[0]
            for line in o.split('\n'):
                m = Collector._RPI_MEM.search(line)
                if m:
                    record[m.group(1) + '_mem'] = float(m.group(2))
        except (ValueError, IOError, KeyError) as e:
            logerr('rpi_info failed: %s' % e)
        return record


# this should work on any linux running kernel 2.2 or later
class LinuxCollector(Collector):
    def __init__(self, hardware, ignored_mounts):
        super(LinuxCollector, self).__init__(hardware)

        # provide info about the system on which we are running
        loginf('sysinfo: %s' % ' '.join(os.uname()))
        fn = '/proc/cpuinfo'
        try:
            cpuinfo = self._readproc_dict(fn)
            for key in cpuinfo:
                loginf('cpuinfo: %s: %s' % (key, cpuinfo[key]))
        except Exception as e:
            logdbg("read failed for %s: %s" % (fn, e))

        self.ignored_mounts = ignored_mounts
        self.last_cpu = dict()
        self.last_net = dict()

    @staticmethod
    def _readproc_line(filename):
        """read single line proc file, return the string"""
        info = ''
        with open(filename) as fp:
            info = fp.readline().strip()
        return info

    @staticmethod
    def _readproc_lines(filename):
        """read proc file that has 'name value' format for each line"""
        info = dict()
        with open(filename) as fp:
            for line in fp:
                line = line.replace('  ', ' ')
                (label, data) = line.split(' ', 1)
                info[label] = data
        return info

    @staticmethod
    def _readproc_dict(filename):
        """read proc file that has 'name:value' format for each line"""
        info = dict()
        with open(filename) as fp:
            for line in fp:
                if line.find(':') >= 0:
                    (n, v) = line.split(':', 1)
                    info[n.strip()] = v.strip()
        return info

    def get_data(self, now=None):
        record = super(LinuxCollector, self).get_data(now)

        # read memory status
        fn = '/proc/meminfo'
        try:
            meminfo = self._readproc_dict(fn)
            if meminfo:
                record['mem_total'] = int(meminfo['MemTotal'].split()[0]) # kB
                record['mem_free'] = int(meminfo['MemFree'].split()[0]) # kB
                record['mem_used'] = record['mem_total'] - record['mem_free']
                record['swap_total'] = int(meminfo['SwapTotal'].split()[0]) # kB
                record['swap_free'] = int(meminfo['SwapFree'].split()[0]) # kB
                record['swap_used'] = record['swap_total'] - record['swap_free']
        except Exception as e:
            logdbg("read failed for %s: %s" % (fn, e))

        # get cpu usage
        fn = '/proc/stat'
        try:
            cpuinfo = self._readproc_lines(fn)
            if cpuinfo:
                values = cpuinfo['cpu'].split()[0:7]
                for i, k in enumerate(self._CPU_KEYS):
                    if k in self.last_cpu:
                        record['cpu_' + k] = int(values[i]) - self.last_cpu[k]
                    self.last_cpu[k] = int(values[i])
        except Exception as e:
            logdbg("read failed for %s: %s" % (fn, e))

        # get network usage
        fn = '/proc/net/dev'
        try:
            netinfo = self._readproc_dict(fn)
            if netinfo:
                for iface in netinfo:
                    values = netinfo[iface].split()
                    for i, k in enumerate(self._NET_KEYS):
                        if iface not in self.last_net:
                            self.last_net[iface] = {}
                        if k in self.last_net[iface]:
                            x = int(values[i]) - self.last_net[iface][k]
                            if x < 0:
                                maxcnt = 0x100000000 # 32-bit counter
                                if x + maxcnt < 0:
                                    maxcnt = 0x10000000000000000 # 64-bit counter
                                x += maxcnt
                            record['net_' + iface + '_' + k] = x
                        self.last_net[iface][k] = int(values[i])
        except Exception as e:
            logdbg("read failed for %s: %s" % (fn, e))

#        uptimestr = _readproc_line('/proc/uptime')
#        (uptime,idletime) = uptimestr.split()

        # get load and process information
        fn = '/proc/loadavg'
        try:
            loadstr = self._readproc_line(fn)
            if loadstr:
                (load1, load5, load15, nproc) = loadstr.split()[0:4]
                record['load1'] = float(load1)
                record['load5'] = float(load5)
                record['load15'] = float(load15)
                (num_proc, tot_proc) = nproc.split('/')
                record['proc_active'] = int(num_proc)
                record['proc_total'] = int(tot_proc)
        except Exception as e:
            logdbg("read failed for %s: %s" % (fn, e))

        # read cpu temperature
        tdir = '/sys/class/hwmon/hwmon0/device'
        # rpi keeps cpu temperature in a different location
        tfile = '/sys/class/thermal/thermal_zone0/temp'
        if os.path.exists(tdir):
            try:
                for f in os.listdir(tdir):
                    if f.endswith('_input'):
                        s = self._readproc_line(os.path.join(tdir, f))
                        if s and len(s):
                            n = f.replace('_input', '')
                            t_C = int(s) / 1000 # degree C
                            record['cpu_' + n] = t_C
            except Exception as e:
                logdbg("read failed for %s: %s" % (tdir, e))
        elif os.path.exists(tfile):
            try:
                s = self._readproc_line(tfile)
                t_C = int(s) / 1000 # degree C
                record['cpu_temp'] = t_C
            except Exception as e:
                logdbg("read failed for %s: %s" % (tfile, e))

        # get stats on mounted filesystems
        fn = '/proc/mounts'
        disks = []
        try:
            mntlines = self._readproc_lines(fn)
            if mntlines:
                for mnt in mntlines:
                    mntpt = mntlines[mnt].split()[0]
                    ignore = False
                    if mnt.find(':') >= 0:
                        ignore = True
                    for m in self.ignored_mounts:
                        if mntpt.startswith(m):
                            ignore = True
                            break
                    if not ignore:
                        disks.append(mntpt)
        except Exception as e:
            logdbg("read failed for %s: %s" % (fn, e))
        for disk in disks:
            label = disk.replace('/', '_')
            if label == '_':
                label = '_root'
            st = os.statvfs(disk)
            free = int((st.f_bavail * st.f_frsize) / 1024) # kB
            total = int((st.f_blocks * st.f_frsize) / 1024) # kB
            used = int(((st.f_blocks - st.f_bfree) * st.f_frsize) / 1024) # kB
            record['disk' + label + '_free'] = free
            record['disk' + label + '_total'] = total
            record['disk' + label + '_used'] = used

        return record


class MacOSXCollector(Collector):
    def __init__(self, hardware, ignored_mounts):
        super(MacOSXCollector, self).__init__(hardware)

        # provide info about the system on which we are running
        loginf('sysinfo: %s' % ' '.join(os.uname()))
        self.ignored_mounts = ignored_mounts
        self.last_cpu = dict()
        self.last_net = dict()

    def get_data(self, now=None):
        import psutil

        record = super(MacOSXCollector, self).get_data(now)

        # read memory status
        info = psutil.virtual_memory()
        record['mem_total'] = info.total
        record['mem_free'] = info.free
        record['mem_used'] = info.used
        record['mem_available'] = info.available
        record['mem_active'] = info.active
        record['mem_inactive'] = info.inactive
        record['mem_wired'] = info.wired
        info = psutil.swap_memory()
        record['swap_total'] = info.total
        record['swap_free'] = info.free
        record['swap_used'] = info.used
        record['swap_sin'] = info.sin
        record['swap_sout'] = info.sout

        # get cpu usage
        ncpu = psutil.cpu_count()
        record['cpu_count'] = ncpu
        info = psutil.cpu_times()
        for k in list(set(self._CPU_KEYS) & set(dir(info))):
            v = getattr(info, k, None)
            if k in self.last_cpu:
                if v is not None and self.last_cpu[k] is not None:
                    record['cpu_' + k] = v - self.last_cpu[k]
                else:
                    record['cpu_' + k] = None
            self.last_cpu[k] = v

        # get network usage
        info = psutil.net_io_counters(pernic=True)
        for iface in info:
            for k in list(set(self._NET_KEYS) & set(dir(info[iface]))):
                if iface not in self.last_net:
                    self.last_net[iface] = {}
                v = getattr(info[iface], k, None)
                if k in self.last_net[iface]:
                    x = None
                    if v is not None and self.last_net[iface][k] is not None:
                        x = v - self.last_net[iface][k]
                        if x < 0:
                            maxcnt = 0x100000000 # 32-bit counter
                            if x + maxcnt < 0:
                                maxcnt = 0x10000000000000000 # 64-bit counter
                            x += maxcnt
                    record['net_' + iface + '_' + k] = x
                self.last_net[iface][k] = v

        # uptime
        # FIXME: implement uptime/idletime

        # get load and process information
        # FIXME: implement load

        # read cpu temperature
        # FIXME: implement cpu temperature

        # get stats on mounted filesystems
        info = psutil.disk_partitions()
        for d in info:
            label = d.mountpoint.replace('/', '_')
            if label == '_':
                label = '_root'
            du = psutil.disk_usage(d.mountpoint)
            record['disk' + label + '_free'] = du.free / 1024 # kB
            record['disk' + label + '_total'] = du.total / 1024 # kB
            record['disk' + label + '_used'] = du.used / 1024 # kB
            # FIXME: add io counters per disk

        return record


def loader(config_dict, engine):
    return ComputerMonitorDriver(**config_dict['ComputerMonitor'])

class ComputerMonitorDriver(AbstractDevice):
    """Driver for collecting computer health data."""

    def __init__(self, **stn_dict):
        loginf('driver version is %s' % DRIVER_VERSION)
        self.polling_interval = int(stn_dict.get('polling_interval', 10))
        loginf('polling interval is %s' % self.polling_interval)

        self.ignored_mounts = stn_dict.get('ignored_mounts', IGNORED_MOUNTS)
        self.hardware = stn_dict.get('hardware', [None])
        if not isinstance(self.hardware, list):
            self.hardware = [self.hardware]

        self.collector = get_collector(self.hardware, self.ignored_mounts)

    @property
    def hardware_name(self):
        return 'ComputerMonitor'

    def genLoopPackets(self):
        while True:
            p = self.collector.get_data()
            yield p
            time.sleep(self.polling_interval)
            

class ComputerMonitor(StdService):
    """Collect CPU, Memory, Disk, and other computer information."""

    def __init__(self, engine, config_dict):
        super(ComputerMonitor, self).__init__(engine, config_dict)
        loginf("service version is %s" % DRIVER_VERSION)

        d = config_dict.get('ComputerMonitor', {})
        self.max_age = weeutil.weeutil.to_int(d.get('max_age', 2592000))
        self.ignored_mounts = d.get('ignored_mounts', IGNORED_MOUNTS)
        self.hardware = d.get('hardware', [None])
        if not isinstance(self.hardware, list):
            self.hardware = [self.hardware]

        # get the database parameters we need to function
        binding = d.get('data_binding', 'cmon_binding')
        self.dbm = self.engine.db_binder.get_manager(data_binding=binding,
                                                     initialize=True)

        # be sure schema in database matches the schema we have
        dbcol = self.dbm.connection.columnsOf(self.dbm.table_name)
        dbm_dict = weewx.manager.get_manager_dict(
            config_dict['DataBindings'], config_dict['Databases'], binding)
        memcol = [x[0] for x in dbm_dict['schema']]
        if dbcol != memcol:
            raise Exception('cmon schema mismatch: %s != %s' % (dbcol, memcol))

        self.last_ts = None
        self.collector = get_collector(self.hardware, self.ignored_mounts)
        self.bind(weewx.NEW_ARCHIVE_RECORD, self.new_archive_record)

    def shutDown(self):
        try:
            self.dbm.close()
        except:
            pass

    def new_archive_record(self, event):
        """save data to database then prune old records as needed"""
        now = int(time.time() + 0.5)
        delta = now - event.record['dateTime']
        if delta > event.record['interval'] * 60:
            logdbg("Skipping record: time difference %s too big" % delta)
            return
        if self.last_ts is not None:
            self.save_data(self.get_data(now, self.last_ts))
        self.last_ts = now
        if self.max_age is not None:
            self.prune_data(now - self.max_age)

    def save_data(self, record):
        """save data to database"""
        self.dbm.addRecord(record)

    def prune_data(self, ts):
        """delete records with dateTime older than ts"""
        sql = "delete from %s where dateTime < %d" % (self.dbm.table_name, ts)
        self.dbm.getSql(sql)
        try:
            # sqlite databases need some help to stay small
            self.dbm.getSql('vacuum')
        except Exception:
            pass

    def get_data(self, now_ts, last_ts):
        record = self.collector.get_data(now_ts)
        # calculate the interval (an integer), and be sure it is non-zero
        record['interval'] = max(1, int((now_ts - last_ts) / 60))
        return record
        


# To test this extension, do the following:
#
# cd /home/weewx
# PYTHONPATH=bin python bin/user/cmon.py
#
if __name__ == "__main__":
    usage = """%prog [options] [--help] [--debug]"""

    def main():
        import optparse
        import weecfg
        syslog.openlog('wee_cmon', syslog.LOG_PID | syslog.LOG_CONS)
        parser = optparse.OptionParser(usage=usage)
        parser.add_option('--config', dest='cfgfn', type=str, metavar="FILE",
                          help="Use configuration file FILE. Default is /etc/weewx/weewx.conf or /home/weewx/weewx.conf")
        parser.add_option('--binding', dest="binding", metavar="BINDING",
                          default='cmon_binding',
                          help="The data binding to use. Default is 'cmon_binding'.")
        parser.add_option('--test-collector', dest='tc', action='store_true',
                          help='test the data collector')
        parser.add_option('--test-driver', dest='td', action='store_true',
                          help='test the driver')
        parser.add_option('--test-service', dest='ts', action='store_true',
                          help='test the service')
        parser.add_option('--update', dest='update', action='store_true',
                          help='update schema to v3')
        (options, args) = parser.parse_args()

        if options.update:
            _, config_dict = weecfg.read_config(options.cfgfn, args)
            update_to_v3(config_dict, options.binding)
        elif options.tc:
            test_collector()
        elif options.td:
            test_driver()
        elif options.ts:
            test_service()

    def update_to_v3(config_dict, db_binding):
        import weedb
        import sqlite3
        from weewx.manager import Manager

        # update an old schema to v3-compatible.  this means adding the
        # interval column and populating it.
        mgr_dict = weewx.manager.get_manager_dict(config_dict['DataBindings'],
                                                  config_dict['Databases'],
                                                  db_binding)

        # see if update is actually required
        schema_name = mgr_dict.get('schema')
        if schema_name is None:
            s = schema
        elif isinstance(schema_name, str):
            s = weeutil.weeutil._get_object(schema_name)
        else:
            s = mgr_dict['schema']
        if 'interval' in s:
            print("Column 'interval' already exists, update not necessary")
            return

        # make a copy of the database dict
        new_db_dict = dict(mgr_dict['database_dict'])
        # modify the database name
        new_db_dict['database_name'] = mgr_dict['database_dict']['database_name'] + '_new'
        # see if the new db exists.  if so, see if we can delete it
        try:
            weedb.create(new_db_dict)
        except weedb.DatabaseExists:
            ans = None
            while ans not in ['y', 'n']:
                ans = input("New database '%s' already exists. Delete it (y/n)? " % (new_db_dict['database_name'],))
                if ans == 'y':
                    weedb.drop(new_db_dict)
                elif ans == 'n':
                    print("update aborted")
                    return
        except sqlite3.OperationalError:
            pass

        # see if we really should do this
        ans = None
        while ans not in ['y', 'n']:
            ans = input("Create new database %s (y/n)? " % new_db_dict['database_name'])
            if ans == 'y':
                break
            elif ans == 'n':
                print("update aborted")
                return

        # copy the data over
        cnt = 0
        with Manager.open(mgr_dict['database_dict']) as old_mgr:
            with Manager.open_with_create(new_db_dict, schema=s) as new_mgr:
                last_ts = None
                for r in old_mgr.genBatchRecords():
                    cnt += 1
                    print("record %d\r" % cnt, end=' ')
                    ival = r['dateTime'] - last_ts if last_ts else 0
                    r['interval'] = ival
                    new_mgr.addRecord(r)
                    last_ts = r['dateTime']
        print("copied %d records\n" % cnt)

    def test_collector():
        c = get_collector()
        while True:
            print(c.get_data())
            time.sleep(5)

    def test_driver():
        driver = ComputerMonitorDriver()
        for p in driver.genLoopPackets():
            print(p)

    def test_service():
        from weewx.engine import StdEngine
        config = {
            'Station': {
                'station_type': 'Simulator',
                'altitude': [0, 'foot'],
                'latitude': 0,
                'longitude': 0},
            'Simulator': {
                'driver': 'weewx.drivers.simulator',
                'mode': 'simulator'},
            'ComputerMonitor': {
                'binding': 'cmon_binding'},
            'DataBindings': {
                'cmon_binding': {
                    'database': 'cmon_sqlite',
                    'manager': 'weewx.manager.DaySummaryManager',
                    'table_name': 'archive',
                    'schema': 'user.cmon.schema'}},
            'Databases': {
                'cmon_sqlite': {
                    'root': '%(WEEWX_ROOT)s',
                    'database_name': '/tmp/cmon.sdb',
                    'driver': 'weedb.sqlite'}},
            'Engine': {
                'Services': {
                    'archive_services': 'user.cmon.ComputerMonitor'}}}
        engine = StdEngine(config)
        svc = ComputerMonitor(engine, config)
        now_ts = int(time.time() + 0.5)
        last_ts = None
        for _ in range(4):
            if last_ts is not None:
                record = svc.get_data(now_ts, last_ts)
                print(record)
            last_ts = now_ts
            time.sleep(5)
        os.remove('/tmp/cmon.sdb')

    main()

Reply via email to