#!/usr/bin/env python3

import asyncio
import time
import threading
import queue
import json
import logging
import logging.handlers
from websocket import create_connection
from pysnmp.hlapi.v3arch.asyncio import (
    get_cmd,
    SnmpEngine,
    CommunityData,
    UdpTransportTarget,
    ContextData,
    ObjectType,
    ObjectIdentity,
)


##########################
# Global parameters
string_device  = "nameOfProvider"
string_mmsi    = "123456789"
sigkServ_IP    = "127.0.0.1"
sigkServ_port  = "3000"
sigkServ_token = "ey.................."
takt           = 60
count_wsExcept = 1 # force initial connect


logger  = logging.getLogger(string_device)
logger.setLevel(logging.DEBUG)
handler = logging.handlers.SysLogHandler(address = '/dev/log')
logger.addHandler(handler)
logger.debug(str(string_device) + ': Starting websocket client ' + str(string_device))


# SNMP Configuration
snmpRouter_IP    = "192........"
community        = "public"
snmp_retries     = 1
snmp_retryDelay  = 1.0

sigk_basePath = 'sys.rut.'
initVal = None
factor  = 1 
summand = 0



snmp_list = [['signalLvl', '.1.3.6.1.4.1.48690.2.2.1.12.1', 'float', factor, summand, initVal],
             ['operator', '.1.3.6.1.4.1.48690.2.2.1.13.1', 'string', factor, summand, initVal],
             ['netRegState', '.1.3.6.1.4.1.48690.2.2.1.11.1', 'string', factor, summand, initVal],
             ['dataConnState', '.1.3.6.1.4.1.48690.2.2.1.15.1', 'string', factor, summand, initVal],
             ['dataConnType', '.1.3.6.1.4.1.48690.2.2.1.16.1', 'string', factor, summand, initVal],
             ['temperature', '.1.3.6.1.4.1.48690.2.2.1.17.1', 'float', 0.1, 273.15, initVal],
             ['SINR', '.1.3.6.1.4.1.48690.2.2.1.19.1', 'float', factor, summand, initVal],
             ['RSRP', '.1.3.6.1.4.1.48690.2.2.1.20.1', 'float', factor, summand, initVal],
             ['RSRQ', '.1.3.6.1.4.1.48690.2.2.1.21.1', 'float', factor, summand, initVal]]


# Queue for sending data to WebSocket thread
ws_queue = queue.Queue()




async def async_get_snmp(ip, community, oid, retries=snmp_retries):
    attempt = 0
    while(attempt < retries):
        attempt += 1
        try:
            snmp_engine = SnmpEngine()
            transport   = await UdpTransportTarget.create((ip, 161), timeout=0.2, retries=0)
            auth        = CommunityData(community, mpModel=1)
            ctx         = ContextData()
            oid_obj     = ObjectType(ObjectIdentity(oid))

            errorIndication, errorStatus, errorIndex, varBinds = await get_cmd(snmp_engine, auth, transport, ctx, oid_obj, lookupMib=False)
            snmp_engine.close_dispatcher()

            if(errorIndication):
                #raise RuntimeError(f"SNMP error for {oid}: {errorIndication}")
                continue
            elif(errorStatus):
                #raise RuntimeError(f"SNMP error {errorStatus.prettyPrint()} at index {errorIndex} for {oid}")
                continue

            return varBinds[0][1]

        except Exception as e:
            if(attempt < retries):
                #print(f"Warning: {e} -- retrying ({attempt}/{retries}) in {snmp_retryDelay}s")
                await asyncio.sleep(snmp_retryDelay)
            else:
                #print(f"Error: {e} -- giving up after {retries} retries")
                return None



async def poll_metrics(ip, community, takt):
    #print(f"Polling device {ip} every {takt}s for metrics.\n")
    while(True):
        time_startStep = time.time()

        count_snmpExcept = 0
        for item in range(0, len(snmp_list)):
           value = None
           value = await async_get_snmp(ip, community, snmp_list[item][1])
           if(value is None):
              count_snmpExcept += 1
              continue
           
           if(snmp_list[item][2] == 'string'):
              value = str(value)
           if(snmp_list[item][2] == 'float'):
              value = float(value)
           if(snmp_list[item][3] != 1):
              value = value * snmp_list[item][3]
           if(snmp_list[item][4] != 0):
              value = value + snmp_list[item][4]
           snmp_list[item][5] = value


        if(count_snmpExcept > 0):
           logString = str(string_device) + ': Could not read snmp for ' + str(count_snmpExcept) + ' keys'
           logger.debug(logString)

        # generate body of signalk-message
        msg = {
           "context": "vessels.urn:mrn:imo:mmsi:" + str(string_mmsi),
           "updates": [
              {
                "source": {
                  "device": string_device
                },
                "values": [
                ]
              },
            ]
          }

        # fill body of message with sensordata
        for item in range(0, len(snmp_list)):
           if(snmp_list[item][5] != initVal):
              msg['updates'][0]['values'].append(({"path": sigk_basePath + snmp_list[item][0], "value": snmp_list[item][5]}))
              snmp_list[item][5] = initVal # reset list to prevent to send old data, when router has disconnected

        # Send to WebSocket thread
        ws_queue.put(msg)

        await asyncio.sleep(max(0.0, takt - (time.time() - time_startStep)))



def websocket_worker():
    ws = None
    while(True):
        try:
            if(ws is None):
                logString = str(string_device) + ': Trying to establish websocket connection ...'
                logger.debug(logString)
                ws = create_connection("ws://" + str(sigkServ_IP) + ":" + str(sigkServ_port) + "/signalk/v1/stream?subscribe=none&token=" + str(sigkServ_token))
                logger.debug(str(string_device) + ': ' + ws.recv())

            # Get data from queue
            try:
                msg = ws_queue.get(timeout=1)
            except(queue.Empty):
                continue
            ws.send(json.dumps(msg))

        except:
            logString = str(string_device) + ': EXCEPTION: Could not send delta'
            logger.debug(logString)
            ws = None
            time.sleep(2)




def main():
    # Start WebSocket thread
    ws_thread = threading.Thread(target=websocket_worker, daemon=True)
    ws_thread.start()

    # Run async polling loop
    try:
        asyncio.run(poll_metrics(snmpRouter_IP, community, takt))
    except KeyboardInterrupt:
        print("\nStopped by user.")


if __name__ == "__main__":
    main()


