config.py 7.37 KB
Newer Older
Nelso Jost's avatar
Nelso Jost committed
1
2
import configparser
import sys
3
4
import os
from pprint import pprint
Nelso Jost's avatar
Nelso Jost committed
5

6
7

DEFAULT_INI=\
Nelso Jost's avatar
Nelso Jost committed
8
9
"""\
[server]
10
11
12
; base URL of the server
URL = http://dados.cta.if.ufrgs.br/emm

13
14
; user board authentication token (auto generated when a new board is created)
BOARD_HASH = 
Nelso Jost's avatar
Nelso Jost committed
15

16
17
[reading]
; CSV list of sensor names/nicknames (order reflect columns on datalog files)
Nelso Jost's avatar
Nelso Jost committed
18
19
SENSORS = DHT22_TEMP, DHT22_AH, BMP085_PRESSURE, LDR

20
21
; time between logger cycles, in minutes
SLEEP_MINUTES = 5
Nelso Jost's avatar
Nelso Jost committed
22

23
24
; true for read board clock; if fail, or false, system's time will be used
RTC_DS1307 = true
Nelso Jost's avatar
Nelso Jost committed
25
26

[datalog]
27
28
; CSV delimiter for local log files (valid options:  ,  or  ;  or  \t  )
CSV_SEP = \t
Nelso Jost's avatar
Nelso Jost committed
29

30
31
; format of the datetime column
; %Y : years | %m : months | %d : days | %H : hours | %M : mins | %S : secs
Nelso Jost's avatar
Nelso Jost committed
32
33
34
DATETIME_FORMAT = %Y-%m-%d-%H-%M-%S

[arduino]
35
36
; CSV list of ports to attempt connection or blank for automatic search on
; /dev/ttyACM* and /dev/ttyUSB* (where * varies from 0 to 4)
Nelso Jost's avatar
Nelso Jost committed
37
SERIAL_PORT =
38
 """
Nelso Jost's avatar
Nelso Jost committed
39

40
DEFAULT_INI_LINES = DEFAULT_INI.splitlines()
41

Nelso Jost's avatar
Nelso Jost committed
42
43
def make_path_here(filename):
    ''' Append filename to the current __file__ path. '''
44
45
    return os.path.realpath(os.path.join(
        os.path.abspath(os.path.dirname(__file__)), filename))
46
47
48


class Config:
Nelso Jost's avatar
Nelso Jost committed
49
    SETTINGS_FILENAME = make_path_here('../../settings.ini')
50
51
52
    DATALOG_PATH = make_path_here('../../data/')
    EXECUTION_LOG_PATH = make_path_here('../logs/')
    OUTGOING_FILENAME = make_path_here('../../data/outgoing.json')
Nelso Jost's avatar
Nelso Jost committed
53

54
    URL_API_POST_RAWSENSORDATA = '{base}api/post/rawsensordata'
55
    SERIAL_CSV_SEP = ','
Nelso Jost's avatar
Nelso Jost committed
56

57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
    RTC_NAME = 'RTC_DS1307'
    RTC_FORMAT = '%Y-%m-%d %H:%M:%S'

    class ConfigMissingSectionError(Exception):
        def __init__(self, section, parent):
            self.message = "\nConfigurationError: At file\n    {filename}\n\n"\
                           "[{section}]\n ^\n"\
                           "Missing section!".format(
                                filename=parent.SETTINGS_FILENAME,
                                section=section)


    class ConfigMissingKeyError(Exception):
        def __init__(self, section, key, parent):
            self.message = "\nConfigurationError: At file\n    {filename}\n\n"\
                           "[{section}]\n{comment}\n{key} = \n{indicator}\n"\
                           "Missing key!".format(
                                filename=parent.SETTINGS_FILENAME,
                                comment=parent.extract_comment(key),
                                section=section, key=key.upper(),
                                indicator=' '*len(key + ' = ') + '^')

    class ConfigValueError(Exception):
        def __init__(self, section, key, message, parent):
            self.message = "\nConfigurationError: At file\n    {filename}\n\n"\
                           "[{section}]\n{comment}\n{key} = {value}\n"\
                           "{indicator}\n{msg}".format(
                                filename=parent.SETTINGS_FILENAME,
                                section=section, key=key, msg=message,
                                comment=parent.extract_comment(key),
                                value=parent[section][key.lower()],
                                indicator=' '*len(key + ' = ') + '^')

Nelso Jost's avatar
Nelso Jost committed
90
    def __init__(self):
91
92
        self.default = configparser.ConfigParser()
        self.default.read_string(DEFAULT_INI)
Nelso Jost's avatar
Nelso Jost committed
93
        self.load_settings()
94
95
96
97

    def __getitem__(self, key):
        return self._parser._sections[key]

98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
    def __contains__(self, key):
        return key in self._parser._sections

    def extract_comment(self, key):
        key, comment_lines = key.upper() + ' =', []
        for i, line in enumerate(DEFAULT_INI_LINES):
            if key in line:
                j = i - 1
                while j >= 0:
                    if not DEFAULT_INI_LINES[j].startswith(';'):
                        return '\n'.join(DEFAULT_INI_LINES[k]
                                         for k in range(j+1, i))
                    j -= 1

    def assert_config_keys(self):
        for section in self.default:
            if section == 'DEFAULT':
                continue
            if section in self:
                for key in self.default[section]:
                    if not key in self[section]:
                        raise self.ConfigMissingKeyError(section, key, self)
            else:
                raise self.ConfigMissingSectionError(section, self)
Nelso Jost's avatar
Nelso Jost committed
122
123

    def ask_restore_settings_file(self):
124
        a = input("\nRestore default file now and overwrite all"
Nelso Jost's avatar
Nelso Jost committed
125
                  "current values? [y/N] ")
126
        if 'y' in a.lowercase():
Nelso Jost's avatar
Nelso Jost committed
127
128
129
130
131
132
133
134
135
136
137
138
            try:
                with open(self.SETTINGS_FILENAME, 'w') as f:
                    f.write(DEFAULT_SETTINGS)
                print("Done! Restart the application!")
                sys.exit(0)
            except:
                print("Unable to write on file\n    {}\n\nTry to run this "
                      "program from another location!".format(
                          self.SETTINGS_FILENAME))
                sys.exit(1)


139
    def load_settings(self):
Nelso Jost's avatar
Nelso Jost committed
140
        '''
141
        Load the INI configuration file onto the self._parser attribute.
Nelso Jost's avatar
Nelso Jost committed
142
        '''
143
        self._parser = configparser.ConfigParser()
Nelso Jost's avatar
Nelso Jost committed
144
        try:
145
            self._parser.read(self.SETTINGS_FILENAME)
Nelso Jost's avatar
Nelso Jost committed
146
147
148
        except:
            print("Unable to open configuration file at\n    {}".format(
                  self.SETTINGS_FILENAME))
149
            return
Nelso Jost's avatar
Nelso Jost committed
150

151
        self.assert_config_keys()
Nelso Jost's avatar
Nelso Jost committed
152

153
154
155
156
        self.validate_server()
        self.validate_reading()
        self.validate_datalog()
        self.validate_arduino()
Nelso Jost's avatar
Nelso Jost committed
157

158
    def validate_server(self):
159
160
        self['server']['api_post_url'] = self.URL_API_POST_RAWSENSORDATA\
            .format(base=self['server']['url'] +
161
                        ('' if self['server']['url'].endswith('/') else '/'))
162

163
    def validate_reading(self):
164
165
        self['reading']['sensors'] = [x.strip() for x in
            self['reading']['sensors'].split(',') if x.strip() != '']
Nelso Jost's avatar
Nelso Jost committed
166

167
        if len(self['reading']['sensors']) == 0:
Nelso Jost's avatar
Nelso Jost committed
168
169
            raise self.ConfigValueError('reading', 'SENSORS',
                    "EmptyListError: At least one sensor must be present!", self)
Nelso Jost's avatar
Nelso Jost committed
170

171
        if 'true' in self['reading']['rtc_ds1307'].lower():
172
            self['reading']['sensors'].append(self.RTC_NAME)
Nelso Jost's avatar
Nelso Jost committed
173

174
        self['reading']['command'] = 'read,' + ','.join(
175
176
177
            self['reading']['sensors'])

        try:
178
179
            self['reading']['sleep_minutes'] =\
                float(self['reading']['sleep_minutes'].replace(',', '.'))
180
        except:
181
182
            raise self.ConfigValueError('reading', 'SLEEP_MINUTES',
                'TypeError: Number expected!', self)
183
184


185
    def validate_datalog(self):
186
187
188
189
        value = self['datalog']['csv_sep']
        self['datalog']['csv_sep'] = bytes(value, 'utf8').decode(
                                           'unicode_escape')
        if not value in (',', ';', r'\t'):
Nelso Jost's avatar
Nelso Jost committed
190
191
192
            raise self.ConfigValueError('datalog', 'CSV_SEP',
                "InvalidCharacter: Supported values are\n"
                "','   ';'   '\\t'\n", self)
193

194
    def validate_arduino(self):
195
196
197
198
199
200
201
202
203
204

        self['arduino']['serial_port'] = [x.strip() for x in
            self['arduino']['serial_port'].split(',') if x.strip() != '']

        if len(self['arduino']['serial_port']) == 0:
            self['arduino']['serial_port'] = []

            for i in range(5):
                self['arduino']['serial_port'].append('/dev/ttyACM{}'.format(i))
                self['arduino']['serial_port'].append('/dev/ttyUSB{}'.format(i))