#!/usr/bin/python
# $Id: owfs.py 741 2013-12-21 14:00:19Z mwall $
#
# Copyright 2013 Matthew Wall
#
# This program is free software: you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free Software
# Foundation, either version 3 of the License, or any later version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE.
#
# See http://www.gnu.org/licenses/
#
# Thanks to Mark Cressey for implementing onewireweewx.  That made this
# implementation much easier.

"""Classes and functions for interfacing with one-wire sensors 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.

This driver will read raw data from owfs, then the weewx calibration can be
used to convert the raw values to desired scaling or units.  Mapping from
one-wire device and attribute to weewx database field is done in the OWFS
section of weewx.conf.  For example,

[OWFS]
    interface = u
    driver = weewx.drivers.owfs
    [[sensor_map]]
        extraTemp1 = /uncached/28.8A071E050000/temperature
        UV = /uncached/EE.1F20CB020800/UVI/UVI
        radiation = /26.FB67E1000000/S3-R1-A/current

[StdCalibrate]
    [[Corrections]]
        radiation = radiation * 1.730463

The interface indicates where the one-wire devices are attached.  The default
value is u, which is shorthand for 'usb'.  This is the option to use for a
DS9490R USB adaptor.  Other options include a serial port such as /dev/ttyS0,
or remote_system:3003 to get data from a remote host running owserver.

The sensor map is simply a list of database field followed by full path to the
desired sensor reading.  Only sensor values that can be converted to float
are supported at this time.

Some devices support caching.  To use raw, uncached values, preface the path
with /uncached.

To find out what devices are actually attached, use this driver like this:

  cd /home/weewx
  sudo PYTHONPATH=bin python bin/weewx/drivers/owfs.py --sensors

To display the names of data fields for each sensor, as well as actual data:

  cd /home/weewx
  sudo PYTHONPATH=bin python bin/weewx/drivers/owfs.py --readings

Details about the python bindings are at the owfs project on sourceforge:

  http://owfs.sourceforge.net/owpython.html
"""

from __future__ import with_statement
import optparse
import syslog
import time

import ow

import weewx.abstractstation
from weeutil.weeutil import tobool

DRIVER_VERSION = '0.3'

def logmsg(dst, msg):
    syslog.syslog(dst, 'owfs: %s' % msg)

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

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

def logcrt(msg):
    logmsg(syslog.LOG_CRIT, msg)

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

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

class OWFS(weewx.abstractstation.AbstractStation):
    """Driver for one-wire sensors via owfs."""
    
    def __init__(self, **stn_dict) :
        """Initialize the driver.

        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]

        polling_interval: How often to poll for data, in seconds.
        [Optional. Default is 10]
        """
        self.sensor_map        = stn_dict['sensor_map']
        self.interface         = stn_dict.get('interface', 'u')
        self.polling_interval  = int(stn_dict.get('polling_interval', 10))
        self.max_tries         = int(stn_dict.get('max_tries', 3))
        self.retry_wait        = int(stn_dict.get('retry_wait', 5))

        loginf('driver version is %s' % DRIVER_VERSION)
        loginf('interface is %s' % self.interface)
        loginf('polling interval is %s' % str(self.polling_interval))
        loginf('sensor map is %s' % self.sensor_map)

        ow.init(self.interface)

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

    def genLoopPackets(self):
        ntries = 0
        while ntries < self.max_tries:
            ntries += 1
            try:
                packet = {}
                packet['usUnits'] = weewx.METRIC
                packet['dateTime'] = int(time.time() + 0.5)
                for s in self.sensor_map:
                    packet[s] = float(ow.owfs_get(self.sensor_map[s]))
                ntries = 0
                yield packet
                time.sleep(self.polling_interval)
            except ow.exError, e:
                logerr("Failed attempt %d of %d to get LOOP 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 LOOP data" % self.max_tries
            logerr(msg)
            raise weewx.RetriesExceeded(msg)


# define a main entry point for basic testing without weewx engine and service
# overhead.  invoke this as follows from the weewx root dir:
#
# PYTHONPATH=bin python bin/weewx/drivers/owfs.py

usage = """%prog [options] [--debug] [--help]"""

def main():
    syslog.openlog('wee_owfs', 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 "owfs driver version %s" % DRIVER_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)

# Provide human-readable labels for sensor types.
SENSOR_DICT = {
    'DS18B20':'Temperature Sensor',
    'DS2438':'Solar Sensor',
    'DS2423':'Lightning Counter',
    'Hobby_Boards_UVI':'Hobby Boards UV Sensor',
    }

def identify_sensor(s):
    label = ''
    if SENSOR_DICT.has_key(s._type):
        label = ' (%s)' % SENSOR_DICT[s._type]
    print '%s: %s %s%s' % (s.id, s._path, s._type, label)

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)
        else:
            if 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()
