config.py 7.91 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
13
; base URL of the server
URL = http://dados.cta.if.ufrgs.br/emm

; board identification number (auto generated when a new board is created)
Nelso Jost's avatar
Nelso Jost committed
14
15
16
17
18
BOARD_ID =

; authentication token for the board's user
USER_HASH =

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

23
24
; time between logger cycles, in minutes
SLEEP_MINUTES = 5
Nelso Jost's avatar
Nelso Jost committed
25

26
27
; 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
28
29
30


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

34
35
; format of the datetime column
; %Y : years | %m : months | %d : days | %H : hours | %M : mins | %S : secs
Nelso Jost's avatar
Nelso Jost committed
36
37
38
39
DATETIME_FORMAT = %Y-%m-%d-%H-%M-%S


[arduino]
40
41
; 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
42
SERIAL_PORT =
43
 """
Nelso Jost's avatar
Nelso Jost committed
44

45
DEFAULT_INI_LINES = DEFAULT_INI.splitlines()
46

Nelso Jost's avatar
Nelso Jost committed
47
48
def make_path_here(filename):
    ''' Append filename to the current __file__ path. '''
49
50
    return os.path.realpath(os.path.join(
        os.path.abspath(os.path.dirname(__file__)), filename))
51
52
53


class Config:
Nelso Jost's avatar
Nelso Jost committed
54
    SETTINGS_FILENAME = make_path_here('../../settings.ini')
55
56
57
    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
58

59
60
    URL_API_POST_RAWSENSORDATA = '{base}api/post/rawsensordata/{bid}'
    SERIAL_CSV_SEP = ','
Nelso Jost's avatar
Nelso Jost committed
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
90
91
92
93
94
    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
95
    def __init__(self):
96
97
98
99
100
        self.default = configparser.ConfigParser()
        self.default.read_string(DEFAULT_INI)
        try:
            self.load_settings()
        except Exception as e:
101
102
            print('Exception: {}'.format(e))
            raise e
103
104
105
106

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

107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
    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
131
132

    def ask_restore_settings_file(self):
133
        a = input("\nRestore default file now and overwrite all"
Nelso Jost's avatar
Nelso Jost committed
134
                  "current values? [y/N] ")
135
        if 'y' in a.lowercase():
Nelso Jost's avatar
Nelso Jost committed
136
137
138
139
140
141
142
143
144
145
146
147
            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)


148
    def load_settings(self):
Nelso Jost's avatar
Nelso Jost committed
149
        '''
150
        Load the INI configuration file onto the self._parser attribute.
Nelso Jost's avatar
Nelso Jost committed
151
        '''
152
        self._parser = configparser.ConfigParser()
Nelso Jost's avatar
Nelso Jost committed
153
        try:
154
            self._parser.read(self.SETTINGS_FILENAME)
Nelso Jost's avatar
Nelso Jost committed
155
156
157
        except:
            print("Unable to open configuration file at\n    {}".format(
                  self.SETTINGS_FILENAME))
158
            return
Nelso Jost's avatar
Nelso Jost committed
159

160
        self.assert_config_keys()
Nelso Jost's avatar
Nelso Jost committed
161

162
163
164
165
        self.validate_server()
        self.validate_reading()
        self.validate_datalog()
        self.validate_arduino()
Nelso Jost's avatar
Nelso Jost committed
166

167
    def validate_server(self):
168
169
170
171
172
173
174
175
176
177
178
179
180
        try:
            bid = int(self['server']['board_id'])
        except:
            raise Exception(
                "Invalid numeric value for the server/BOARD_ID key.\n"
                "Expected to be of type integer.\n"
                "Was given:\n   {}".format(self['server']['board_id']))

        self['server']['api_post_url'] = self.URL_API_POST_RAWSENSORDATA\
            .format(base=self['server']['url'] +
                        ('' if self['server']['url'].endswith('/') else '/'),
                    bid=bid)

181
    def validate_reading(self):
182
183
        self['reading']['sensors'] = [x.strip() for x in
            self['reading']['sensors'].split(',') if x.strip() != '']
Nelso Jost's avatar
Nelso Jost committed
184

185
186
187
188
        if len(self['reading']['sensors']) == 0:
            raise Exception(
                    "At least one sensor must be present on the reading/"
                    "SENSORS key.")
Nelso Jost's avatar
Nelso Jost committed
189

190
        if 'true' in self['reading']['rtc_ds1307'].lower():
191
            self['reading']['sensors'].append(self.RTC_NAME)
Nelso Jost's avatar
Nelso Jost committed
192

193
        self['reading']['command'] = 'read,' + ','.join(
194
195
196
            self['reading']['sensors'])

        try:
197
198
            self['reading']['sleep_minutes'] =\
                float(self['reading']['sleep_minutes'].replace(',', '.'))
199
        except:
200
201
            raise self.ConfigValueError('reading', 'SLEEP_MINUTES',
                'TypeError: Number expected!', self)
202
203


204
    def validate_datalog(self):
205
206
207
208
209
210
211
212
213
        value = self['datalog']['csv_sep']
        self['datalog']['csv_sep'] = bytes(value, 'utf8').decode(
                                           'unicode_escape')
        if not value in (',', ';', r'\t'):
            raise Exception(
                "Invalid character value for the datalog/CSV_SEP key.\n"
                "Supported values:\n    ','   ';'   '\\t'\n"
                "Was given:\n    {}".format(value))

214
    def validate_arduino(self):
215
216
217
218
219
220
221
222
223
224

        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))