#!/usr/bin/python
# -*- encoding: utf-8; py-indent-offset: 4 -*-
# +------------------------------------------------------------------+
# |             ____ _               _        __  __ _  __           |
# |            / ___| |__   ___  ___| | __   |  \/  | |/ /           |
# |           | |   | '_ \ / _ \/ __| |/ /   | |\/| | ' /            |
# |           | |___| | | |  __/ (__|   <    | |  | | . \            |
# |            \____|_| |_|\___|\___|_|\_\___|_|  |_|_|\_\           |
# |                                                                  |
# | Copyright Mathias Kettner 2016             mk@mathias-kettner.de |
# +------------------------------------------------------------------+
#
# This file is part of Check_MK.
# The official homepage is at http://mathias-kettner.de/check_mk.
#
# check_mk 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 in version 2.  check_mk is  distributed
# in the hope that it will be useful, but WITHOUT ANY WARRANTY;  with-
# out even the implied warranty of  MERCHANTABILITY  or  FITNESS FOR A
# PARTICULAR PURPOSE. See the  GNU General Public License for more de-
# tails. You should have  received  a copy of the  GNU  General Public
# License along with GNU Make; see the file  COPYING.  If  not,  write
# to the Free Software Foundation, Inc., 51 Franklin St,  Fifth Floor,
# Boston, MA 02110-1301 USA.


###################################################
# plugin to retrieve data from tinkerforge devices.
#
# please note that for this plugin to work, the tinkerforge api has to be installed
#  (included in OMD, otherwise get it from http://download.tinkerforge.com/bindings/python/)
# Also, if the tinkerforge device is connected directly to the computer via usb,
# the brick deamon has to be installed and running: http://download.tinkerforge.com/tools/brickd/)
#
# This has been designed to also work as a special agent. In this case the following configuration
# settings have to be provided on the command line


#######################################################
# sample configuration (/etc/check_mk/tinkerforge.cfg):
#
# host = "localhost"
# port = 4223
# segment_display_uid = "abc"         # uid of the sensor to display on the 7-segment display
# segment_display_brightness = 2      # brightness of the 7-segment display (0-7)
#
# to find the uid of a sensor, either use brickv or run the plugin
# manually. plugin output looks like this:
#   temperature,Ab3d5F.a.xyz,2475
# xyz is the uid you're looking for. It's always the last of the dot-separated sensor path
# (Ab3d5F is the id of the master brick to which the sensor is connected, a is the port
#  to which the sensor is connected)


##################
# developer notes:
#
# Support for individual bricklets has to be added in init_device_handlers.
#  Currently the bricklets included in the Starter Kit: Server Room Monitoring are
#  implemented

# Don't have tinkerforge module during tests. So disable those checks
# pylint: disable=import-error

import sys
import os


def install():
    dest = os.path.dirname(os.path.realpath(__file__))
    sys.stdout.write("installing tinkerforge python api to %s\n" % dest)
    if os.path.exists(os.path.join(dest, "tinkerforge")):
        sys.stdout.write("already installed\n")
        return 1

    import urllib2
    import shutil
    from zipfile import ZipFile
    from cStringIO import StringIO
    url = "http://download.tinkerforge.com/bindings/python/tinkerforge_python_bindings_latest.zip"
    response = urllib2.urlopen(url)
    buf = StringIO(response.read())
    z = ZipFile(buf)

    extract_files = [f for f in z.namelist() if f.startswith("source/tinkerforge")]
    z.extractall(dest, extract_files)

    shutil.move(os.path.join(dest, "source", "tinkerforge"),
                os.path.join(dest, "tinkerforge"))
    shutil.rmtree(os.path.join(dest, "source"))

    return 0


DEFAULT_SETTINGS = {
    'host':                       "localhost",
    'port':                       4223,
    'segment_display_uid':        None,
    'segment_display_brightness': 2
}


from optparse import OptionParser
import time

# globals
segment_display_value = None
segment_display_unit  = ""
segment_display = None


def id_to_string(identifier):
    return "%s.%s.%s" % (identifier.connected_uid,
                         identifier.position,
                         identifier.uid)


def print_generic(settings, sensor_type, ident, factor, unit, *values):
    if ident.uid == settings['segment_display_uid']:
        global segment_display_value, segment_display_unit
        segment_display_value = int(values[0] * factor)
        segment_display_unit = unit
    sys.stdout.write("%s,%s,%s\n" %
        (sensor_type, id_to_string(ident), ",".join([str(val) for val in values])))


def print_ambient_light(conn, settings, uid):
    from tinkerforge.bricklet_ambient_light import BrickletAmbientLight
    br = BrickletAmbientLight(uid, conn)
    print_generic(settings, "ambient", br.get_identity(), 0.01, "L", br.get_illuminance())


def print_ambient_light_v2(conn, settings, uid):
    from tinkerforge.bricklet_ambient_light_v2 import BrickletAmbientLightV2
    br = BrickletAmbientLightV2(uid, conn)
    print_generic(settings, "ambient", br.get_identity(), 0.01, "L", br.get_illuminance())


def print_temperature(conn, settings, uid):
    from tinkerforge.bricklet_temperature import BrickletTemperature
    br = BrickletTemperature(uid, conn)
    print_generic(settings, "temperature", br.get_identity(), 0.01,
                  u"\N{DEGREE SIGN}C", br.get_temperature())


def print_temperature_ext(conn, settings, uid):
    from tinkerforge.bricklet_ptc import BrickletPTC
    br = BrickletPTC(uid, conn)
    print_generic(settings, "temperature.ext", br.get_identity(), 0.01,
                  u"\N{DEGREE SIGN}C", br.get_temperature())


def print_humidity(conn, settings, uid):
    from tinkerforge.bricklet_humidity import BrickletHumidity
    br = BrickletHumidity(uid, conn)
    print_generic(settings, "humidity", br.get_identity(), 0.1, "RH", br.get_humidity())


def print_master(conn, settings, uid):
    from tinkerforge.brick_master import BrickMaster
    br = BrickMaster(uid, conn)
    print_generic(settings, "master", br.get_identity(), 1.0, "",
                  br.get_stack_voltage(),
                  br.get_stack_current(),
                  br.get_chip_temperature(),
                  )

def print_motion_detector(conn, settings, uid):
    from tinkerforge.bricklet_motion_detector import BrickletMotionDetector
    br = BrickletMotionDetector(uid, conn)
    print_generic(settings, "motion", br.get_identity(), 1.0, "", br.get_motion_detected())


def display_on_segment(conn, settings, text):
    #        0x01
    #       ______
    #      |      |
    # 0x20 |      | 0x02
    #      |______|
    #      | 0x40 |
    # 0x10 |      | 0x04
    #      |______|
    #        0x08

    CHARACTERS = {
        "0" : 0x3f, "1" : 0x06, "2" : 0x5b, "3" : 0x4f, "4" : 0x66,
        "5" : 0x6d, "6" : 0x7d, "7" : 0x07, "8" : 0x7f, "9" : 0x6f,
        "C" : 0x39, "H" : 0x74, "L" : 0x38, "R" : 0x50,
        u"\N{DEGREE SIGN}" : 0x63,
    }

    from tinkerforge.bricklet_segment_display_4x7 import BrickletSegmentDisplay4x7
    br = BrickletSegmentDisplay4x7(segment_display, conn)
    segments = []
    for letter in text:
        if len(segments) >= 4:
            break
        if letter in CHARACTERS:
            segments.append(CHARACTERS[letter])

    # align to the right
    segments = [0] * (4 - len(segments)) + segments

    br.set_segments(segments, settings['segment_display_brightness'], False)


def init_device_handlers():
    device_handlers = {}

    # storing the dev_id is not necessary but may save a little time as otherwise the module
    # needs to be imported just to find out this id. If the bricklet is present the module
    # gets imported anyway of course
    for dev_id, module_name,                 clazz,                    handler in [
        (13,    "brick_master",              "BrickMaster",            print_master),
        (21,    "bricklet_ambient_light",    "BrickletAmbientLight",   print_ambient_light),
        (259,   "bricklet_ambient_light_v2", "BrickletAmbientLightV2", print_ambient_light_v2),
        (216,   "bricklet_temperature",      "BrickletTemperature",    print_temperature),
        (226,   "bricklet_ptc",              "BrickletPTC",            print_temperature_ext),
        (27,    "bricklet_humidity",         "BrickletHumidity",       print_humidity),
        (233,   "bricklet_motion_detector",  "BrickletMotionDetector", print_motion_detector)
    ]:
        if dev_id is not None:
            device_handlers[dev_id] = handler
        else:
            module = __import__("tinkerforge." + module_name)
            sub_module = module.__dict__[module_name]
            device_handlers[sub_module.__dict__[clazz].DEVICE_IDENTIFIER] = handler

    return device_handlers


def enumerate_callback(conn, device_handlers, settings,
                       uid, connected_uid, position, hardware_version,
                       firmware_version, device_identifier, enumeration_type):
    if device_identifier == 237:
        global segment_display
        segment_display = uid
    elif device_identifier in device_handlers:
        device_handlers[device_identifier](conn, settings, uid)


def read_config(env):
    settings = DEFAULT_SETTINGS
    cfg_path = os.path.join(os.getenv("MK_CONFDIR", "/etc/check_mk"),
                            "tinkerforge.cfg")

    if os.path.isfile(cfg_path):
        execfile(cfg_path, settings, settings)
    return settings


def main():

# host = "localhost"
# port = 4223
# segment_display_uid = "abc"         # uid of the sensor to display on the 7-segment display
# segment_display_brightness = 2      # brightness of the 7-segment display (0-7)


    settings = read_config(os.environ)
    parser = OptionParser()
    parser.add_option("--host", dest="host", default=settings['host'],
                    help="host/ipaddress of the tinkerforge device",
                    metavar="ADDRESS")
    parser.add_option("--port", dest="port", default=settings['port'], type=int,
                    help="port of the tinkerforge device", metavar="PORT")
    parser.add_option("--segment_display_uid",
                        dest="uid", default=settings['segment_display_uid'],
                        help="uid of the bricklet which will be displayed in the 7-segment display",
                        metavar="UID")
    parser.add_option("--segment_display_brightness", type=int,
                        dest="brightness", default=settings['segment_display_brightness'],
                        help="brightness of the 7-segment display (0-7)")
    parser.add_option("--install", action="store_true",
                      help="install tinkerforge python api to same directory as the plugin")

    options = parser.parse_args()[0]

    settings = {
        'host': options.host,
        'port': options.port,
        'segment_display_uid': options.uid,
        'segment_display_brightness': options.brightness
    }

    if options.install:
        return install()

    try:
        from tinkerforge.ip_connection import IPConnection
    except ImportError:
        sys.stdout.write("<<<tinkerforge:sep(44)>>>\n")
        sys.stdout.write("master,0.0.0,tinkerforge api isn't installed\n")
        return 1

    conn = IPConnection()
    conn.connect(settings['host'], settings['port'])

    device_handlers = init_device_handlers()

    try:
        sys.stdout.write("<<<tinkerforge:sep(44)>>>\n")

        cb = lambda uid, connected_uid, position, hardware_version, firmware_version, \
                    device_identifier, enumeration_type: \
            enumerate_callback(conn, device_handlers, settings, \
                               uid, connected_uid, position, \
                               hardware_version, firmware_version, \
                               device_identifier, enumeration_type)

        conn.register_callback(IPConnection.CALLBACK_ENUMERATE, cb)
        conn.enumerate()

        # bricklets respond asynchronously in callbacks and we have no way of knowing
        # what bricklets to expect
        time.sleep(0.1)

        if segment_display is not None:
            if segment_display_value is not None:
                display_on_segment(conn, settings,
                                   "%d%s" % (segment_display_value, segment_display_unit))
            else:
                display_on_segment(conn, settings,
                                   "")
    finally:
        conn.disconnect()


if __name__ == "__main__":
    main()

