Heating controller with neural thermal model written in Python
Jacek Kowalski
2018-06-24 425bf71fc0b24b547006686d83404c54b983de0b
commit | author | age
425bf7 1 import abc
JK 2 import collections
3 import datetime
4 from math import floor
5
6 import math
7 import pytz
8 import solartime
9
10
11 solartime.basestring = str
12
13
14 class DataReader(abc.ABC):
15     def __init__(self):
16         self.lines = []
17
18     @abc.abstractmethod
19     def getNextLine(self):
20         pass
21
22     def peek(self, line=0):
23         while len(self.lines) <= line:
24             self.lines.append(self.getNextLine())
25         return self.lines[line]
26
27     def peekColumn(self, column, line=0):
28         return self.peek(line)[column]
29
30     def pop(self) -> dict:
31         self.peek()
32         return self.lines.pop(0)
33
34     def __iter__(self):
35         return self
36
37     def __next__(self):
38         return self.pop()
39
40
41 class CsvDataReader(DataReader):
42     def __init__(self, filename, **kwargs):
43         super().__init__()
44
45         import csv
46         self.filehandle = open(filename, 'r')
47         self.reader = csv.reader(self.filehandle, delimiter=' ', quoting=csv.QUOTE_NONE, **kwargs)
48
49     def transformLine(self, line):
50         return line
51
52     def getNextLine(self):
53         return self.transformLine(next(self.reader))
54
55
56 class HistoricalDataReader(DataReader):
57     def __init__(self, filename, **kwargs):
58         super().__init__()
59         
60         self.last_flow = 100
61         self.last_return = 100
62         self.last_mode = 0
63
64         import csv
65         filehandle = open(filename, 'r')
66         self.reader = csv.DictReader(
67             filehandle,
68             fieldnames=['time', 'temp_in', 'temp_out', 'temp_target', 'temp_flow', 'temp_return', 'mode'],
69             delimiter=' ', quoting=csv.QUOTE_NONE,
70             **kwargs
71         )
72
73     def transformLine(self, line):
74         return {
75             'time': int(line['time']),
76             'temp_in': float(line['temp_in']),
77             'temp_out': float(line['temp_out']),
78             'temp_target': float(line['temp_target']),
79             'temp_flow': float(line['temp_flow']),
80             'temp_return': float(line['temp_return']),
81             'mode': int(line['mode']),
82         }
83
84     def getNextLine(self):
85         line = self.transformLine(next(self.reader))
86         if ( not line['mode'] and self.last_flow < line['temp_flow'] ) or (line['mode'] and not self.last_mode):
87             line['temp_flow'] = self.last_flow
88         self.last_flow = line['temp_flow']
89         if not line['mode'] and self.last_return < line['temp_return']:
90             line['temp_return'] = self.last_return
91         self.last_return = line['temp_return']
92         self.last_mode = line['mode']
93         return line
94
95
96 class DataGenerator(DataReader):
97     def __init__(self):
98         super().__init__()
99
100         self.x = -60
101
102     def getNextLine(self):
103         self.x += 60
104         return {
105             'time': self.x,
106             'temp_in': 20.0,
107             'temp_out': 10 * math.sin(self.x / 24 / 500),  # around 20 hours
108             'mode': 0,
109         }
110
111
112 class WeatherDataReader(DataReader):
113     def __init__(self, filename, period=3, utc_offset=2):
114         super().__init__()
115
116         import json
117         with open(filename, 'r') as filehandle:
118             self.data = json.load(filehandle, object_pairs_hook=collections.OrderedDict)
119         self.iterator = iter(self.data)
120
121         self.period = period * 3600
122         self.utc_offset = utc_offset * 3600
123
124         self.start_time = int(next(iter(self.data)))
125         self.end_time = int(next(iter(reversed(self.data))))
126
127     def getNextLine(self):
128         i = next(self.iterator)
129         result = {k: float(v) for k, v in self.data[i].items()}
130         result['time'] = int(i) + self.utc_offset
131         return result
132
133     def getWeatherForTime(self, time):
134         time = int(time) - self.utc_offset
135         if time < self.start_time or time > self.end_time:
136             raise Exception('Weather for time {} is unavailable'.format(time))
137
138         period_no = (time - self.start_time) / self.period
139         first = floor(period_no) * self.period + self.start_time
140         second = first + self.period
141         if second > self.end_time:
142             second = first
143             first -= self.period
144
145         result = {}
146         for i in self.data[str(first)]:
147             result[i] = (float(self.data[str(first)][i]) * (second - time)
148                          + float(self.data[str(second)][i]) * (time - first)) / self.period
149         result['time'] = time
150
151         return result
152
153
154 class WeatherDataWrapper(DataReader):
155     def __init__(self, reader: DataReader, weather: WeatherDataReader):
156         super().__init__()
157
158         self.reader = reader
159         self.weather = weather
160
161     def getNextLine(self):
162         data = self.reader.getNextLine()
163         weather = self.weather.getWeatherForTime(data['time'])
164         return {**data, **weather}
165
166
167 class RadiationDataWrapper(DataReader):
168     def __init__(self, weather: WeatherDataWrapper, latitude=49.88, longitude=19.49, localtz=None):
169         super().__init__()
170
171         self.weather = weather
172         self.localtz = localtz if localtz else pytz.timezone('Europe/Warsaw')
173         self.latitude = latitude
174         self.longitude = longitude
175         self.solartime = solartime.SolarTime()
176
177     def getNextLine(self):
178         data = self.weather.getNextLine()
179
180         dtime = datetime.datetime.fromtimestamp(data['time']).replace(tzinfo=self.localtz).astimezone(pytz.utc)
181         sunrise = self.solartime.sunrise_utc(dtime, self.latitude, self.longitude) + datetime.timedelta(hours=1)
182         sunset = self.solartime.sunset_utc(dtime, self.latitude, self.longitude) - datetime.timedelta(hours=1)
183
184         data['day'] = int(sunrise < dtime < sunset)
185         data['radiation'] = (100 - data['cloudiness']) * data['day']
186         data['humid'] = 0 if data['humidity'] > 75 else int(75 - data['humidity'])
187         return data
188
189
190 class PeriodicReaderWrapper(DataReader):
191     def __init__(self, reader, period=60, max_difference=60 * 60):
192         super().__init__()
193
194         self.reader = reader
195         self.period = period
196         self.max_difference = max_difference
197
198         self.time = None
199         self.last = None
200
201     def getNextLine(self):
202         if self.last is None:
203             self.last = self.reader.pop()
204             self.time = self.last['time']
205         else:
206             self.time += self.period
207
208         while self.reader.peekColumn('time') < self.time:
209             self.last = self.reader.pop()
210
211         if abs(self.last['time'] - self.time) > self.max_difference:
212             self.last = None
213             self.time = None
214             raise StopIteration()
215
216         return {
217             **self.last,
218             'time': self.time,
219         }
220
221
222 class AggregatorReaderWrapper(DataReader):
223     def __init__(self, reader, period=60, aggregate=10):
224         super().__init__()
225
226         self.reader = reader
227         self.period = period
228         self.aggregate = aggregate
229
230         self.time = None
231
232     def getNextLine(self):
233         if self.time is None:
234             self.time = self.reader.peekColumn('time')
235
236         sums = {}
237         for i in range(self.aggregate):
238             line = self.reader.pop()
239             if self.time != line['time']:
240                 raise Exception('Invalid time series to aggregate, '
241                                 + 'expected time {}, got {}'.format(self.time, line['time']))
242
243             for key, value in line.items():
244                 if key not in sums:
245                     sums[key] = 0.0
246
247                 if key == 'mode':
248                     sums[key] = max(sums[key], value)
249                 else:
250                     sums[key] += value
251
252             self.time += self.period
253
254         for key, value in sums.items():
255             if key == 'mode':
256                 continue
257             sums[key] = value / self.aggregate
258
259         return sums