Browse Source

add support for looking at the logs, getting specific entries, and

getting ranges
main
John-Mark Gurney 5 years ago
parent
commit
5dedc3d970
1 changed files with 145 additions and 3 deletions
  1. +145
    -3
      RainEagle/parse.py

+ 145
- 3
RainEagle/parse.py View File

@@ -1,9 +1,16 @@
from StringIO import StringIO
from bisect import bisect

import collections
import glob
import itertools
import os.path
import shutil
import tempfile
import unittest

# meterts is the timestamp of the read from the meter. readts is the
# timestamp of the local machine fetching the data
class MeterRead(collections.namedtuple('MeterRead', [ 'meterts', 'readts', 'status', 'load', 'maxload', 'loadunit', 'sink', 'source', 'ssunit' ])):
pass

@@ -45,6 +52,83 @@ def _pread(fp, off, sz):
# be a discontinuity, where it skips forward or backward an hour, and this
# is when the new timezone takes effect.

class LogDir(object):
def __init__(self, basename):
'''Pass in the base name for the logs. The pattern is
<basename>.<number>.log. Files w/ an extension of .idx
are indexes for those files and can be safely removed.'''

self._basename = basename

self.verifycreateindexes()

def verifycreateindexes(self):
self._files = {}
for i in glob.iglob(self._basename + '.*.log'):
with open(i) as fp:
self._files[i] = ParseLog.generateIndex(fp)

self._poses = [ (y[0], fname, y[1]) for fname, x in self._files.iteritems() for y in x['index'] ]
self._poses.sort()

def __getitem__(self, rng):
if not isinstance(rng, slice):
return self.get(rng)

if rng.stop < rng.start:
return []

ret = []
start, end = rng.start, rng.stop

pos = bisect(self._poses, (start, '')) - 1
while True:
posinfo = self._poses[pos]

with open(posinfo[1]) as fp:
for i in ParseLog(fp).iterfp(posinfo[2]):
#print `start, end, i`
if i.meterts < start:
# not to the start yet
continue
elif i.meterts >= end:
# past the end
break
else:
ret.append(i)

if i.meterts >= end:
break

pos += 1

return ret

def get(self, utc):
pos = bisect(self._poses, (utc, '')) - 1

posinfo = self._poses[pos]

previ = None
with open(posinfo[1]) as fp:
for i in ParseLog(fp).iterfp(posinfo[2]):
if utc == i.meterts:
break
elif utc < i.meterts:
break

previ = i

# decide which is closer, previ or i
if previ is None:
return i

middle = previ.meterts + (i.meterts - previ.meterts) / 2
if utc >= middle:
return i

return previ

class ParseLog(object):
def __init__(self, fp):
self._fp = fp
@@ -66,12 +150,15 @@ class ParseLog(object):
raise ValueError('unknown type: %s' % repr(data[0]))

def __iter__(self):
return self.iterfp()

def iterfp(self, pos=0):
'''Iterate the lines in the file starting as pos.'''
# this can be suspended/resumed between yields, so
# keep track of pointer.
# keep track of fp possition.

fp = self._fp

pos = 0
tz = None
done = False
while not done:
@@ -194,6 +281,18 @@ class MiscTests(unittest.TestCase):
self.assertEqual(sio.tell(), pos)

class Tests(unittest.TestCase):
def setUp(self):
d = os.path.realpath(tempfile.mkdtemp())
self.basetempdir = d
self.tempdir = os.path.join(d, 'subdir')

shutil.copytree(os.path.join('fixtures'), self.tempdir)

def tearDown(self):
shutil.rmtree(self.basetempdir)
self.tempdir = None
self.basetempdir = None

oldlines = '''l 1571848576 Connected 1.1260 9.155000 W 65375.946 0.000 Wh
z GMT+7 1571848569 1571873769 -0700
l 1571848585 Connected 1.0890 9.155000 kW 15.946 0.000 kWh
@@ -203,7 +302,7 @@ l 1571848593 Connected 1.0500 9.155000 kW 15.946 0.000 kWh
zi = ZoneInfo(tz='GMT+8', lcl='1577132476', utc='1577161276', offset='-0800')

self.assertEqual(zi.getOffset(), -8 * 60 * 60)
zi = ZoneInfo(tz='GMT+8', lcl='1577132476', utc='1577161276', offset='-0700')

self.assertEqual(zi.getOffset(), -7 * 60 * 60)
@@ -298,6 +397,49 @@ m 1577161298.96 1577132488 Connected 0.1450 1.992000 kW 90.404 1.660 kWh
self.assertEqual([ x.meterts for x in lines ], [ 1577161272, 1577161280, 1577161288 ])
self.assertEqual([ x.load for x in lines ], [ 0.2580, 0.3410, 0.1450 ])

def test_logdir(self):
ld = LogDir(os.path.join(self.tempdir, 'data'))

mr = ld.get(1577952703)
self.assertEqual(mr.load, .3596)

mr = ld.get(1577953233)
self.assertEqual(mr.load, .4799)

mr = ld.get(1577953252.71)
self.assertEqual(mr.load, .2738)
self.assertEqual(ld[mr.meterts].load, .2738)

# data.1.log
# XXX - fix, include first line of file
#mr = ld[1577952662]
#self.assertEqual(mr.load, .3967)

mr = ld.get(1577954523)
#print `mr`
self.assertEqual(mr.load, .1539)

rng = ld[1577953704:1577953541]
self.assertEqual(rng, [])

rng = ld[1577953541:1577953704]
tzline = 'z GMT+8 1577924671 1577953471 -0800'
tz = ParseLog.parseline(tzline)

# XXX - not including the first line of the file
#m 1577953674.37 1577924872 Connected 0.2909 1.483000 kW 1.000 1.000 kWh
reslines = '''m 1577953544.45 1577924742 Connected 0.4826 1.483000 kW 1.000 1.000 kWh
m 1577953553.54 1577924752 Connected 0.3117 1.483000 kW 1.000 1.000 kWh
m 1577953562.83 1577924762 Connected 0.4346 1.483000 kW 1.000 1.000 kWh
m 1577953683.88 1577924882 Connected 0.3879 1.483000 kW 1.000 1.000 kWh
m 1577953693.96 1577924892 Connected 0.1687 1.483000 kW 1.000 1.000 kWh
m 1577953704.06 1577924902 Connected 0.3323 1.483000 kW 1.000 1.000 kWh'''

rngres = [ ParseLog.parseline(x, tz) for x in reslines.split('\n') ]
self.assertEqual(rng, rngres)

# XXX - test out of range items as well

def test_close(self):
# test to make sure the file object is closed
pass

Loading…
Cancel
Save