# $Id: owfss.py 787 2014-01-20 02:39:50Z mwall $
# Copyright 2013 Matthew Wall
"""weewx module to collect data from one-wire devices via owfs

This module requires one-wire file system including python bindings.  On
debian systems these can be installed like this:

sudo apt-get install owfs python-ow

The owftpd, owhttpd, and owserver services do not need to be running.

Installation

Put this file in the bin/user directory.

Configuration

Only numeric sensor values are supported.  By default, data from each sensor
is treated as a gauge.  Data types include:

  gauge - record the value as it is read from the sensor
  delta - difference between current and last reading
  average - time average by calculating the delta divided by time period

Add the following to weewx.conf:

[OWFSService]
    interface = u
    [[sensor_map]]
        extraTemp1 = /uncached/28.8A071E050000/temperature
        UV = /uncached/EE.1F20CB020800/UVI/UVI
        radiation = /26.FB67E1000000/S3-R1-A/current
        lightning = /1D.1AD00F000000/counters.A
    [[data_type]]
        lightning = delta

[Engines]
    [[WxEngine]]
        service_list = ... , weewx.wxengine.StdQC, user.owfss.OWFSService, weewx.wxengine.StdArchive, ...
"""

import syslog
import time
import ow
import weewx
import weewx.units
from weewx.wxengine import StdService

SERVICE_VERSION = '0.4'

def logmsg(level, msg):
    syslog.syslog(level, 'owfss: %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)

class OWFSService(StdService):
    """Collect data from one-wire devices via owfs."""

    def __init__(self, engine, config_dict):
        """
        interface: Where to find the one-wire sensors.  Options include
        u, /dev/ttyS0
        [Required. Default is u (usb)]

        sensor_map: Associate sensor values with database fields.
        [Required]

        data_type: Indicates how data should be processed before saving.
        [Optional. Default is gauge]
        """
        super(OWFSService, self).__init__(engine, config_dict)

        d = config_dict.get('OWFSService', {})
        self.sensor_map = d['sensor_map']
        self.data_type  = d.get('data_type', {})
        self.interface  = d.get('interface', 'u')
        self.max_tries  = int(d.get('max_tries', 3))
        self.retry_wait = int(d.get('retry_wait', 5))

        self.last_data = {}

        loginf('interface is %s' % self.interface)
        loginf('sensor map is %s' % self.sensor_map)
        loginf('sensor type map is %s' % self.data_type)
        ow.init(self.interface)
        self.bind(weewx.NEW_ARCHIVE_RECORD, self.getData)

    def getData(self, event):
        delta = time.time() - event.record['dateTime']
        if delta > event.record['interval'] * 60:
            loginf("Skipping record: time difference %s too big" % delta)
            return

        ntries = 0
        while ntries < self.max_tries:
            ntries += 1
            try:
                p = {}
                p['usUnits'] = weewx.METRIC
                for s in self.sensor_map:
                    v = float(ow.owfs_get(self.sensor_map[s]))
                    if self.data_type.get(s) == 'delta':
                        if v is not None and self.last_data.get(s) is not None:
                            p[s] = v - self.last_data.get(s)
                        else:
                            p[s] = None
                        self.last_data[s] = v
                    elif self.data_type.get(s) == 'average':
                        if v is not None and self.last_data.get(s) is not None:
                            (oldv, oldt) = self.last_data.get(s)
                            p[s] = (v - oldv)/(event.record['dateTime'] - oldt)
                        else:
                            p[s] = None
                        self.last_data[s] = (v, event.record['dateTime'])
                    else:
                        p[s] = v
                ntries = 0
                if event.record['usUnits'] != weewx.METRIC:
                    converter = weewx.units.StdUnitConverters[event.record['usUnits']]
                    p = converter.convertDict(p)
                event.record.update(p)
                break
            except ow.exError, e:
                logerr("Failed attempt %d of %d to get onewire data: %s" %
                       (ntries, self.max_tries, e))
                logdbg("Waiting %d seconds before retry" % self.retry_wait)
                time.sleep(self.retry_wait)
        else:
            msg = "Max retries (%d) exceeded for onewire data" % self.max_tries
            logerr(msg)
            raise weewx.RetriesExceeded(msg)


# define a main entry point for basic testing without weewx overhead.
# invoke this as follows from the weewx root dir:
#
# PYTHONPATH=bin python bin/user/owfss.py
if __name__ == '__main__':
    usage = """%prog [options] [--debug] [--help]"""

    def main():
        import optparse
        syslog.openlog('wee_owfss', syslog.LOG_PID | syslog.LOG_CONS)
        parser = optparse.OptionParser(usage=usage)
        parser.add_option('--version', dest='version', action='store_true',
                          help='display driver version')
        parser.add_option('--debug', dest='debug', action='store_true',
                          help='display diagnostic information while running')
        parser.add_option("--iface", dest="iface", type=str, metavar="IFACE",
                          help="specify the interface, e.g., u or /dev/ttyS0")
        parser.add_option('--sensors', dest='sensors', action='store_true',
                          help='display list attached sensors')
        parser.add_option('--readings', dest='readings', action='store_true',
                          help='display sensor readings')
        (options, args) = parser.parse_args()

        if options.version:
            print "owfss version %s" % SERVICE_VERSION
            exit(1)

        # default to usb for the interface
        iface = options.iface if options.iface is not None else 'u'

        if options.debug is not None:
            syslog.setlogmask(syslog.LOG_UPTO(syslog.LOG_DEBUG))
        else:
            syslog.setlogmask(syslog.LOG_UPTO(syslog.LOG_INFO))

        if options.sensors:
            ow.init(iface)
            traverse(ow.Sensor('/'), identify_sensor)
        elif options.readings:
            ow.init(iface)
            traverse(ow.Sensor('/'), display_sensor_info)

    def identify_sensor(s):
        print '%s: %s %s' % (s.id, s._path, s._type)

    def display_sensor_info(s):
        print s.id
        display_dict(s.__dict__)

    def display_dict(d, level=0):
        for k in d:
            if isinstance(d[k], dict):
                display_dict(d[k], level=level+1)
            elif k == 'alias':
                pass
            elif k.startswith('_'):
                print '%s%s: %s' % ('  '*level, k, d[k])
            else:
                print '%s%s: %s' % ('  '*level, d[k], ow.owfs_get(d[k]))

    def traverse(device, func):
        for s in device.sensors():
            if s._type in ['DS2409']:
                traverse(s, func)
            else:
                func(s)

if __name__ == '__main__':
    main()
