#!/usr/bin/env python3
#
# This file is part of vaisalad.
#
# vaisalad 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
# (at your option) any later version.
#
# vaisalad 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 the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with vaisalad.  If not, see <http://www.gnu.org/licenses/>.

"""Weather station daemon for the Warwick one-metre telescope"""

import datetime
import Pyro4
import re
import serial
import threading
import time

PYRO_HOST = 'localhost'
PYRO_PORT = 9001

# pylint: disable=too-many-instance-attributes

class VaisalaDaemon:
    """Daemon class that wraps the RS232 interface"""
    SERIAL_PORT = '/dev/vaisala'
    SERIAL_BAUD = 4800
    SERIAL_TIMEOUT = 5

    # pylint: disable=anomalous-backslash-in-string
    DATA_REGEX = b'0R0,' \
        b'Dm=(?P<wind_direction>\d+)D,' \
        b'Sm=(?P<wind_speed>\d+\.\d)K,' \
        b'Ta=(?P<temperature>\d+\.\d)C,' \
        b'Ua=(?P<relative_humidity>\d+\.\d)P,' \
        b'Pa=(?P<pressure>\d+\.\d)H,' \
        b'Rc=(?P<accumulated_rain>\d+\.\d\d)M,' \
        b'Th=(?P<heater_temperature>\d+\.\d)C,' \
        b'Vh=(?P<heater_voltage>\d+\.\d)N\r\n'
    # pylint: enable=anomalous-backslash-in-string

    def __init__(self):
        self._lock = threading.Lock()
        self._running = True
        self._last_error = datetime.datetime.min
        self._regex = re.compile(VaisalaDaemon.DATA_REGEX)
        self._latest = None
        self._last_error_time = None
        self._last_error_message = None
        self._port = None

        runloop = threading.Thread(target=self.run)
        runloop.daemon = True
        runloop.start()

    # pylint: disable=broad-except
    def run(self):
        """Main run loop"""
        while self._running:
            # Initial setup
            try:
                self._port = serial.Serial(VaisalaDaemon.SERIAL_PORT,
                                           VaisalaDaemon.SERIAL_BAUD,
                                           timeout=VaisalaDaemon.SERIAL_TIMEOUT)
            except Exception as exception:
                print(exception)
                print('Will retry in 10 seconds...')
                with self._lock:
                    self._last_error_message = str(exception)
                    self._last_error_time = datetime.datetime.utcnow()
                time.sleep(10.)
                continue

            try:
                print('Connected to', VaisalaDaemon.SERIAL_PORT)
                self._latest = None

                # Flush any stale state
                self._port.flushInput()
                self._port.flushOutput()

                # First line may have been only partially recieved
                self._port.readline()

                # Main run loop
                while self._running:
                    data = self._port.readline()
                    match = self._regex.match(data)

                    if match:
                        with self._lock:
                            self._latest = {k: float(v) for k, v
                                            in match.groupdict().items()}
                            self._latest['date'] = datetime.datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%SZ')
            except Exception as exception:
                self._port.close()
                with self._lock:
                    self._last_error_message = str(exception)
                    self._last_error_time = datetime.datetime.utcnow()

                print(exception)
                print('Will retry in 10 seconds...')
                time.sleep(10.)
    # pylint: enable=broad-except

    def running(self):
        """Returns false if the daemon should be terminated"""
        return self._running

    def stop(self):
        """Stop the daemon thread"""
        self._running = False

    def last_measurement(self):
        """Query the latest valid measurement.
        May return None if no data is available"""
        with self._lock:
            return self._latest

    def last_error(self):
        """Query the latest error time and message"""
        with self._lock:
            return (self._last_error_time, self._last_error_message)

def spawn_daemon():
    """Spawns the daemon and registers it with Pyro"""
    Pyro4.config.COMMTIMEOUT = 5

    pyro = Pyro4.Daemon(host=PYRO_HOST, port=PYRO_PORT)
    vaisala = VaisalaDaemon()
    uri = pyro.register(vaisala, objectId='vaisala_daemon')

    print('Starting vaisala daemon with Pyro ID:', uri)
    pyro.requestLoop(loopCondition=vaisala.running)
    print('Stopping vaisala daemon with Pyro ID:', uri)

if __name__ == '__main__':
    spawn_daemon()
