#!/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"""

# pylint: disable=broad-except
# pylint: disable=invalid-name

import datetime
import math
import re
import threading
import time
import Pyro4
import serial
import warwick.observatory as observatory

# Set automatically when generating RPM package
SOFTWARE_VERSION = "1.13"

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+)(?P<wind_direction_status>[D\#]),' \
    b'Sm=(?P<wind_speed>\d+\.\d)(?P<wind_speed_status>[K\#]),' \
    b'Ta=(?P<temperature>-?\d+\.\d)(?P<temperature_status>[C\#]),' \
    b'Ua=(?P<relative_humidity>\d+\.\d)(?P<relative_humidity_status>[P\#]),' \
    b'Pa=(?P<pressure>\d+\.\d)(?P<pressure_status>[H\#]),' \
    b'Rc=(?P<accumulated_rain>\d+\.\d\d)(?P<accumulated_rain_status>[M\#]),' \
    b'Ri=(?P<rain_intensity>\d+\.\d)(?P<rain_intensity_status>[M\#]),' \
    b'Th=(?P<heater_temperature>-?\d+\.\d)(?P<heater_temperature_status>[C\#]),' \
    b'Vh=(?P<heater_voltage>\d+\.\d)(?P<heater_voltage_status>[NVWF])\r\n'
# pylint: enable=anomalous-backslash-in-string

DATA_FIELDS = {
    'wind_direction': b'D',
    'wind_speed': b'K',
    'temperature': b'C',
    'relative_humidity': b'P',
    'pressure': b'H',
    'accumulated_rain': b'M',
    'rain_intensity': b'M'
}

def dew_point(temperature, humidity):
    """Calculates the dew point using the equation defined by the Vaisala manual"""
    a = math.log(100 / humidity)
    b = 15 * a - 2.1 * temperature + 2711.5
    c = temperature + 273.16
    return c * b / (c * a / 2 + b) - 273.16

class VaisalaDaemon:
    """Daemon class that wraps the RS232 interface"""
    def __init__(self):
        self._lock = threading.Lock()
        self._running = True
        self._last_error = datetime.datetime.min
        self._regex = re.compile(DATA_REGEX)
        self._latest = None
        self._port = None
        self._send_rain_reset = False

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

    def run(self):
        """Main run loop"""
        while self._running:
            # Initial setup
            try:
                self._port = serial.Serial(SERIAL_PORT, SERIAL_BAUD, timeout=SERIAL_TIMEOUT)
            except Exception as exception:
                observatory.log.error('vaisalad', 'Failed to connect to serial port (' \
                                      + str(exception) + ')')
                print(exception)
                print('Will retry in 10 seconds...')
                time.sleep(10.)
                continue

            try:
                print('Connected to', 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:
                    if self._send_rain_reset:
                        self._port.write('0XZRU\r\n'.encode('ascii'))
                        self._send_rain_reset = False

                    data = self._port.readline()
                    match = self._regex.match(data)

                    if match:
                        latest = {
                            'date': datetime.datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%SZ'),
                            'software_version': SOFTWARE_VERSION
                        }

                        for field in DATA_FIELDS:
                            latest.update({
                                field: float(match.group(field)),
                                field+'_valid': match.group(field+'_status') == DATA_FIELDS[field]
                            })

                        dewpt = dew_point(latest['temperature'], latest['relative_humidity'])
                        latest.update({
                            'dew_point_delta': round(latest['temperature'] - dewpt, 2),
                            'dew_point_delta_valid': latest['temperature_valid'] and \
                                latest['relative_humidity_valid']
                        })

                        with self._lock:
                            self._latest = latest

            except Exception as exception:
                self._port.close()
                observatory.log.error('vaisalad', 'Failed to read serial port (' \
                                      + str(exception) + ')')
                print(exception)
                print('Will retry in 10 seconds...')
                time.sleep(10.)

    @Pyro4.expose
    def last_measurement(self):
        """Query the latest valid measurement."""
        with self._lock:
            return self._latest

    @Pyro4.expose
    def reset_rain_counter(self):
        """Reset the accumulated rain counter"""
        self._send_rain_reset = True

if __name__ == '__main__':
    observatory.daemons.onemetre_vaisala.launch(VaisalaDaemon())
