import abc
|
import collections
|
import datetime
|
from math import floor
|
|
import math
|
import pytz
|
import solartime
|
|
|
solartime.basestring = str
|
|
|
class DataReader(abc.ABC):
|
def __init__(self):
|
self.lines = []
|
|
@abc.abstractmethod
|
def getNextLine(self):
|
pass
|
|
def peek(self, line=0):
|
while len(self.lines) <= line:
|
self.lines.append(self.getNextLine())
|
return self.lines[line]
|
|
def peekColumn(self, column, line=0):
|
return self.peek(line)[column]
|
|
def pop(self) -> dict:
|
self.peek()
|
return self.lines.pop(0)
|
|
def __iter__(self):
|
return self
|
|
def __next__(self):
|
return self.pop()
|
|
|
class CsvDataReader(DataReader):
|
def __init__(self, filename, **kwargs):
|
super().__init__()
|
|
import csv
|
self.filehandle = open(filename, 'r')
|
self.reader = csv.reader(self.filehandle, delimiter=' ', quoting=csv.QUOTE_NONE, **kwargs)
|
|
def transformLine(self, line):
|
return line
|
|
def getNextLine(self):
|
return self.transformLine(next(self.reader))
|
|
|
class HistoricalDataReader(DataReader):
|
def __init__(self, filename, **kwargs):
|
super().__init__()
|
|
self.last_flow = 100
|
self.last_return = 100
|
self.last_mode = 0
|
|
import csv
|
filehandle = open(filename, 'r')
|
self.reader = csv.DictReader(
|
filehandle,
|
fieldnames=['time', 'temp_in', 'temp_out', 'temp_target', 'temp_flow', 'temp_return', 'mode'],
|
delimiter=' ', quoting=csv.QUOTE_NONE,
|
**kwargs
|
)
|
|
def transformLine(self, line):
|
return {
|
'time': int(line['time']),
|
'temp_in': float(line['temp_in']),
|
'temp_out': float(line['temp_out']),
|
'temp_target': float(line['temp_target']),
|
'temp_flow': float(line['temp_flow']),
|
'temp_return': float(line['temp_return']),
|
'mode': int(line['mode']),
|
}
|
|
def getNextLine(self):
|
line = self.transformLine(next(self.reader))
|
if ( not line['mode'] and self.last_flow < line['temp_flow'] ) or (line['mode'] and not self.last_mode):
|
line['temp_flow'] = self.last_flow
|
self.last_flow = line['temp_flow']
|
if not line['mode'] and self.last_return < line['temp_return']:
|
line['temp_return'] = self.last_return
|
self.last_return = line['temp_return']
|
self.last_mode = line['mode']
|
return line
|
|
|
class DataGenerator(DataReader):
|
def __init__(self):
|
super().__init__()
|
|
self.x = -60
|
|
def getNextLine(self):
|
self.x += 60
|
return {
|
'time': self.x,
|
'temp_in': 20.0,
|
'temp_out': 10 * math.sin(self.x / 24 / 500), # around 20 hours
|
'mode': 0,
|
}
|
|
|
class WeatherDataReader(DataReader):
|
def __init__(self, filename, period=3, utc_offset=2):
|
super().__init__()
|
|
import json
|
with open(filename, 'r') as filehandle:
|
self.data = json.load(filehandle, object_pairs_hook=collections.OrderedDict)
|
self.iterator = iter(self.data)
|
|
self.period = period * 3600
|
self.utc_offset = utc_offset * 3600
|
|
self.start_time = int(next(iter(self.data)))
|
self.end_time = int(next(iter(reversed(self.data))))
|
|
def getNextLine(self):
|
i = next(self.iterator)
|
result = {k: float(v) for k, v in self.data[i].items()}
|
result['time'] = int(i) + self.utc_offset
|
return result
|
|
def getWeatherForTime(self, time):
|
time = int(time) - self.utc_offset
|
if time < self.start_time or time > self.end_time:
|
raise Exception('Weather for time {} is unavailable'.format(time))
|
|
period_no = (time - self.start_time) / self.period
|
first = floor(period_no) * self.period + self.start_time
|
second = first + self.period
|
if second > self.end_time:
|
second = first
|
first -= self.period
|
|
result = {}
|
for i in self.data[str(first)]:
|
result[i] = (float(self.data[str(first)][i]) * (second - time)
|
+ float(self.data[str(second)][i]) * (time - first)) / self.period
|
result['time'] = time
|
|
return result
|
|
|
class WeatherDataWrapper(DataReader):
|
def __init__(self, reader: DataReader, weather: WeatherDataReader):
|
super().__init__()
|
|
self.reader = reader
|
self.weather = weather
|
|
def getNextLine(self):
|
data = self.reader.getNextLine()
|
weather = self.weather.getWeatherForTime(data['time'])
|
return {**data, **weather}
|
|
|
class RadiationDataWrapper(DataReader):
|
def __init__(self, weather: WeatherDataWrapper, latitude=49.88, longitude=19.49, localtz=None):
|
super().__init__()
|
|
self.weather = weather
|
self.localtz = localtz if localtz else pytz.timezone('Europe/Warsaw')
|
self.latitude = latitude
|
self.longitude = longitude
|
self.solartime = solartime.SolarTime()
|
|
def getNextLine(self):
|
data = self.weather.getNextLine()
|
|
dtime = datetime.datetime.fromtimestamp(data['time']).replace(tzinfo=self.localtz).astimezone(pytz.utc)
|
sunrise = self.solartime.sunrise_utc(dtime, self.latitude, self.longitude) + datetime.timedelta(hours=1)
|
sunset = self.solartime.sunset_utc(dtime, self.latitude, self.longitude) - datetime.timedelta(hours=1)
|
|
data['day'] = int(sunrise < dtime < sunset)
|
data['radiation'] = (100 - data['cloudiness']) * data['day']
|
data['humid'] = 0 if data['humidity'] > 75 else int(75 - data['humidity'])
|
return data
|
|
|
class PeriodicReaderWrapper(DataReader):
|
def __init__(self, reader, period=60, max_difference=60 * 60):
|
super().__init__()
|
|
self.reader = reader
|
self.period = period
|
self.max_difference = max_difference
|
|
self.time = None
|
self.last = None
|
|
def getNextLine(self):
|
if self.last is None:
|
self.last = self.reader.pop()
|
self.time = self.last['time']
|
else:
|
self.time += self.period
|
|
while self.reader.peekColumn('time') < self.time:
|
self.last = self.reader.pop()
|
|
if abs(self.last['time'] - self.time) > self.max_difference:
|
self.last = None
|
self.time = None
|
raise StopIteration()
|
|
return {
|
**self.last,
|
'time': self.time,
|
}
|
|
|
class AggregatorReaderWrapper(DataReader):
|
def __init__(self, reader, period=60, aggregate=10):
|
super().__init__()
|
|
self.reader = reader
|
self.period = period
|
self.aggregate = aggregate
|
|
self.time = None
|
|
def getNextLine(self):
|
if self.time is None:
|
self.time = self.reader.peekColumn('time')
|
|
sums = {}
|
for i in range(self.aggregate):
|
line = self.reader.pop()
|
if self.time != line['time']:
|
raise Exception('Invalid time series to aggregate, '
|
+ 'expected time {}, got {}'.format(self.time, line['time']))
|
|
for key, value in line.items():
|
if key not in sums:
|
sums[key] = 0.0
|
|
if key == 'mode':
|
sums[key] = max(sums[key], value)
|
else:
|
sums[key] += value
|
|
self.time += self.period
|
|
for key, value in sums.items():
|
if key == 'mode':
|
continue
|
sums[key] = value / self.aggregate
|
|
return sums
|