Skip to content

Commit 483954e

Browse files
committed
rrd plugin: include changes from PR# 1018 and add webinterface
1 parent a630118 commit 483954e

File tree

16 files changed

+1115
-67
lines changed

16 files changed

+1115
-67
lines changed

rrd/__init__.py

Lines changed: 64 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,14 @@
22
# vim: set encoding=utf-8 tabstop=4 softtabstop=4 shiftwidth=4 expandtab
33
#########################################################################
44
# Copyright 2012-2013 Marcus Popp marcus@popp.mx
5-
# Copyright 2020 Bernd Meiners Bernd.Meiners@mail.de
5+
# Copyright 2020,2025 Bernd Meiners Bernd.Meiners@mail.de
6+
# Copyright 2025 ghciv6
67
#########################################################################
78
# This file is part of SmartHomeNG.
89
# https://www.smarthomeNG.de
910
# https://knx-user-forum.de/forum/supportforen/smarthome-py
1011
#
11-
# Sample plugin for new plugins to run with SmartHomeNG version 1.4 and
12-
# upwards.
12+
# rrd plugin to run with SmartHomeNG version 1.10 and upwards.
1313
#
1414
# SmartHomeNG is free software: you can redistribute it and/or modify
1515
# it under the terms of the GNU General Public License as published by
@@ -26,10 +26,11 @@
2626
#
2727
#########################################################################
2828

29-
from lib.module import Modules
30-
from lib.model.smartplugin import *
29+
from lib.model.smartplugin import SmartPlugin
3130
from lib.item import Items
3231

32+
from .webif import WebInterface
33+
3334
import datetime
3435
import functools
3536
import os
@@ -50,7 +51,7 @@ class RRD(SmartPlugin):
5051
Documentation can be found at `<https://pythonhosted.org/rrdtool/>`_
5152
"""
5253

53-
PLUGIN_VERSION = '1.6.2'
54+
PLUGIN_VERSION = '1.7.0'
5455

5556
def __init__(self, sh):
5657
"""
@@ -60,10 +61,6 @@ def __init__(self, sh):
6061
# Call init code of parent class (SmartPlugin)
6162
super().__init__()
6263

63-
from bin.smarthome import VERSION
64-
if '.'.join(VERSION.split('.', 2)[:2]) <= '1.5':
65-
self.logger = logging.getLogger(__name__)
66-
6764
# get the parameters for the plugin (as defined in metadata plugin.yaml):
6865
rrd_dir = self.get_parameter_value('rrd_dir')
6966
if not rrd_dir:
@@ -75,17 +72,21 @@ def __init__(self, sh):
7572
try:
7673
os.makedirs(self._rrd_dir)
7774
except:
78-
self.logger.error("Unable to create directory '{}'".format(self._rrd_dir))
75+
self.logger.error(f"Unable to create directory '{self._rrd_dir}'")
7976

8077
self._rrds = {}
8178
self.step = self.get_parameter_value('step')
8279

8380
# Initialization code goes here
8481
if not REQUIRED_PACKAGE_IMPORTED:
8582
self._init_complete = False
86-
self.logger.error("{}: Unable to import Python package 'rrdtool'".format(self.get_fullname()))
83+
self.logger.error(f"{self.get_fullname()}: Unable to import Python package 'rrdtool'")
8784
return
8885

86+
self.init_webinterface(WebInterface)
87+
# if plugin should not start without web interface
88+
# if not self.init_webinterface():
89+
# self._init_complete = False
8990
return
9091

9192
def run(self):
@@ -122,7 +123,7 @@ def parse_item(self, item):
122123

123124
# set database filename
124125
dbname = ''
125-
self.logger.debug("parse item: {}".format(item))
126+
self.logger.debug(f"parse item: {item}")
126127
if self.has_iattr(item.conf,'rrd_ds_name'):
127128
dbname = self.get_iattr_value(item.conf, 'rrd_ds_name')
128129
if not dbname:
@@ -149,7 +150,7 @@ def parse_item(self, item):
149150
no_series = self.get_iattr_value(item.conf,'rrd_no_series')
150151

151152
if no_series:
152-
self.logger.warning("Attribute rrd_no_series is set to True, no data series will be provided for item {}".format(item.property.path))
153+
self.logger.warning("Attribute rrd_no_series is set to True, no data series will be provided for item {item.property.path}")
153154
else:
154155
item.series = functools.partial(self._series, item=item.property.path)
155156
item.db = functools.partial(self._single, item=item.property.path)
@@ -162,6 +163,11 @@ def parse_item(self, item):
162163
item.set(last, 'RRDtool')
163164

164165
def _update_cycle(self):
166+
"""
167+
this is called by scheduler to update all items at the same time
168+
it is currently fixed by parameter "step" from plugin.yaml
169+
thus any other step you would set for an item would not be served
170+
"""
165171
for itempath in self._rrds:
166172
rrd = self._rrds[itempath]
167173
if rrd['type'] == 'GAUGE':
@@ -174,7 +180,7 @@ def _update_cycle(self):
174180
value
175181
)
176182
except Exception as e:
177-
self.logger.warning("RRD: error updating {}: {}".format(itempath, e))
183+
self.logger.warning(f"RRD: error updating {itempath}: {e}")
178184
continue
179185

180186

@@ -193,50 +199,50 @@ def _series(self, func, start='1d', end='now', count=100, ratio=1, update=False,
193199
if item in self._rrds:
194200
rrd = self._rrds[item]
195201
else:
196-
self.logger.warning("RRDtool: not enabled for {}".format(item))
202+
self.logger.warning(f"RRDtool: not enabled for {item}")
197203
return
198-
query = ["{}".format(rrd['rrdb'])]
204+
query = [f"{rrd['rrdb']}"]
199205
# prepare consolidation function
200-
if func == 'avg':
206+
if func == 'avg' or func == 'raw':
201207
query.append('AVERAGE')
202208
elif func == 'max':
203209
if not rrd['max']:
204-
self.logger.warning("RRDtool: unsupported consolidation function {} for {}".format(func, item))
210+
self.logger.warning(f"RRDtool: unsupported consolidation function {func} for {item}")
205211
return
206212
query.append('MAX')
207213
elif func == 'min':
208214
if not rrd['min']:
209-
self.logger.warning("RRDtool: unsupported consolidation function {} for {}".format(func, item))
215+
self.logger.warning(f"RRDtool: unsupported consolidation function {func} for {item}")
210216
return
211217
query.append('MIN')
212218
elif func == 'raw':
213219
query.append('AVERAGE')
214-
self.logger.warning("RRDtool: unsupported consolidation function {} for {}. Using average instead".format(func, item))
220+
self.logger.warning(f"RRDtool: unsupported consolidation function {func} for {item}. Using average instead")
215221
else:
216-
self.logger.warning("RRDtool: unsupported consolidation function {} for {}".format(func, item))
222+
self.logger.warning(f"RRDtool: unsupported consolidation function {func} for {item}")
217223
return
218224
# set time frame for query
219225
if start.isdigit():
220-
query.extend(['--start', "{}".format(start)])
226+
query.extend(['--start', f"{start}"])
221227
else:
222-
query.extend(['--start', "now-{}".format(start)])
228+
query.extend(['--start', f"now-{start}"])
223229
if end != 'now':
224230
if end.isdigit():
225-
query.extend(['--end', "{}".format(end)])
231+
query.extend(['--end', f"{end}"])
226232
else:
227-
query.extend(['--end', "now-{}".format(end)])
233+
query.extend(['--end', f"now-{end}"])
228234
if step is not None:
229235
query.extend(['--resolution', step])
230236
# run query
231237
try:
232238
meta, name, data = rrdtool.fetch(*query)
233239
except Exception as e:
234-
self.logger.warning("error reading {0} data: {1}".format(item, e))
240+
self.logger.warning(f"error reading {item} data: {e}")
235241
return None
236242

237243
# postprocess values
238244
if sid is None:
239-
sid = "{}|{}|{}|{}|{}".format(item,func,start,end,count)
245+
sid = f"{item}|{func}|{start}|{end}|{count}"
240246
reply = {'cmd': 'series', 'series': None, 'sid': sid}
241247
istart, iend, istep = meta
242248
mstart = istart * 1000
@@ -247,29 +253,32 @@ def _series(self, func, start='1d', end='now', count=100, ratio=1, update=False,
247253
for i, v in enumerate(data):
248254
if v[0] is not None:
249255
tuples.append((mstart + i * mstep, v[0]))
256+
iend = int( ( mstart + i * mstep ) / 1000 ) # added by ghciv6
250257
reply['series'] = sorted(tuples)
251-
reply['params'] = {'update': True, 'item': item, 'func': func, 'start': str(iend), 'end': str(iend + istep), 'step': str(istep), 'sid': sid}
258+
#reply['params'] = {'update': True, 'item': item, 'func': func, 'start': str(iend), 'end': str(iend + istep), 'step': str(istep), 'sid': sid} # old
259+
reply['params'] = {'update': True, 'item': item, 'func': func, 'start': str(iend + istep), 'end': str(iend + 2 * istep), 'step': str(istep), 'sid': sid} # changed by ghciv6
252260
reply['update'] = self.get_sh().now() + datetime.timedelta(seconds=istep)
253-
self.logger.warning("Returning series for {} from {} to {} with {} values".format(sid, iend, iend+istep, len(tuples) ))
261+
#self.logger.warning(f"Returning series for {sid} from {iend} to {iend+istep} with {len(tuples)} values") # old
262+
self.logger.info(f"Returning series for {sid} from {istart} to {iend} with {len(tuples)} values") # changed by ghciv6
254263
return reply
255264

256265
def _single(self, func, start='1d', end='now', item=None):
257266
"""
258267
Reads a single value from rrd.
259268
260-
:param func: String with consolidating function 'avg', 'min', 'max', 'last' to use
269+
:param func: String with consolidating function 'avg' (or 'raw'), 'min', 'max', 'last' to use
261270
:param start: String containing a start time
262271
:param end: String containing a start time
263272
"""
264273
if item in self._rrds:
265274
rrd = self._rrds[item]
266275
else:
267-
self.logger.warning("RRDtool: not enabled for {}".format(item))
276+
self.logger.warning(f"RRDtool: not enabled for {item}")
268277
return
269278

270279
# prepare consolidation function
271-
query = ["{}".format(rrd['rrdb'])]
272-
if func == 'avg':
280+
query = [f"{rrd['rrdb']}"]
281+
if func == 'avg' or func == 'raw':
273282
query.append('AVERAGE')
274283
elif func == 'max':
275284
if rrd['max']:
@@ -284,35 +293,35 @@ def _single(self, func, start='1d', end='now', item=None):
284293
elif func == 'last':
285294
query.append('AVERAGE')
286295
elif func == 'raw':
287-
self.logger.warning("RRDtool: unsupported consolidation function {} for {}. Using average instead".format(func, item))
296+
self.logger.warning(f"RRDtool: unsupported consolidation function {func} for {item}. Using average instead")
288297
query.append('AVERAGE')
289298
else:
290-
self.logger.warning("RRDtool: unsupported consolidation function {} for {}".format(func, item))
299+
self.logger.warning(f"RRDtool: unsupported consolidation function {func} for {item}")
291300
return
292301

293302
# set time frame for query
294303
if start.isdigit():
295-
query.extend(['--start', "{}".format(start)])
304+
query.extend(['--start', f"{start}"])
296305
else:
297-
query.extend(['--start', "now-{}".format(start)])
306+
query.extend(['--start', f"now-{start}"])
298307
if end != 'now':
299308
if end.isdigit():
300-
query.extend(['--end', "{}".format(end)])
309+
query.extend(['--end', f"{end}"])
301310
else:
302-
query.extend(['--end', "now-{}".format(end)])
311+
query.extend(['--end', f"now-{end}"])
303312

304313
# execute query
305314
try:
306315
meta, name, data = rrdtool.fetch(*query)
307316
except Exception as e:
308-
self.logger.warning("error reading {0} data: {1}".format(item, e))
317+
self.logger.warning(f"error reading {item} data: {e}")
309318
return None
310319

311320
# unpack returned values
312321
values = [v[0] for v in data if v[0] is not None]
313322

314323
# postprocess for consolidation
315-
if func == 'avg':
324+
if func == 'avg' or func == 'raw':
316325
if len(values) > 0:
317326
return sum(values) / len(values)
318327
elif func == 'min':
@@ -325,7 +334,7 @@ def _single(self, func, start='1d', end='now', item=None):
325334
if len(values) > 0:
326335
return values[-1]
327336
elif func == 'raw':
328-
self.logger.warning("Unsupported consolidation function {0} for {1}. Using last instead".format(func, item))
337+
self.logger.warning(f"Unsupported consolidation function {func} for {item}. Using last instead")
329338
if len(values) > 0:
330339
return values[-1]
331340

@@ -337,24 +346,24 @@ def _create(self, rrd):
337346
args = [rrd['rrdb']]
338347
item_id = rrd['id'].rpartition('.')[2][:19]
339348

340-
args.append("DS:{}:{}:{}:U:U".format(item_id, rrd['type'], str(2 * rrd['step'])))
349+
args.append(f"DS:{item_id}:{rrd['type']}:{str(2 * rrd['step'])}:U:U")
341350
if rrd['min']:
342-
args.append('RRA:MIN:0.5:{}:1825'.format(int(86400 / rrd['step']))) # 24h/5y
351+
args.append(f"RRA:MIN:0.5:{int(86400 / rrd['step'])}:1825") # 24h/5y
343352
if rrd['max']:
344-
args.append('RRA:MAX:0.5:{}:1825'.format(int(86400 / rrd['step']))) # 24h/5y
353+
args.append(f"RRA:MAX:0.5:{int(86400 / rrd['step'])}:1825") # 24h/5y
345354
args.extend(['--step', str(rrd['step'])])
346355

347356
if rrd['type'] == 'GAUGE':
348-
args.append('RRA:AVERAGE:0.5:1:{}'.format(int(86400 / rrd['step']) * 7 + 8)) # 7 days
349-
args.append('RRA:AVERAGE:0.5:{}:1536'.format(int(1800 / rrd['step']))) # 0.5h/32 days
350-
args.append('RRA:AVERAGE:0.5:{}:1600'.format(int(21600 / rrd['step']))) # 6h/400 days
351-
args.append('RRA:AVERAGE:0.5:{}:1826'.format(int(86400 / rrd['step']))) # 24h/5y
352-
args.append('RRA:AVERAGE:0.5:{}:1300'.format(int(604800 / rrd['step']))) # 7d/25y
357+
args.append(f"RRA:AVERAGE:0.5:1:{int(86400 / rrd['step']) * 7 + 8}") # 7 days
358+
args.append(f"RRA:AVERAGE:0.5:{int(1800 / rrd['step'])}:1536") # 0.5h/32 days
359+
args.append(f"RRA:AVERAGE:0.5:{int(21600 / rrd['step'])}:1600") # 6h/400 days
360+
args.append(f"RRA:AVERAGE:0.5:{int(86400 / rrd['step'])}:1826") # 24h/5y
361+
args.append(f"RRA:AVERAGE:0.5:{int(604800 / rrd['step'])}:1300") # 7d/25y
353362
elif rrd['type'] == 'COUNTER':
354-
args.append('RRA:AVERAGE:0.5:{}:1826'.format(int(86400 / rrd['step']))) # 24h/5y
355-
args.append('RRA:AVERAGE:0.5:{}:1300'.format(int(604800 / rrd['step']))) # 7d/25y
363+
args.append(f"RRA:AVERAGE:0.5:{int(86400 / rrd['step'])}:1826") # 24h/5y
364+
args.append(f"RRA:AVERAGE:0.5:{int(604800 / rrd['step'])}:1300") # 7d/25y
356365
try:
357366
rrdtool.create(*args)
358-
self.logger.debug("Creating rrd ({0}) for {1}.".format(rrd['rrdb'], rrd['item']))
367+
self.logger.debug(f"Creating rrd ({rrd['rrdb']}) for {rrd['item']}.")
359368
except Exception as e:
360-
self.logger.warning("Error creating rrd ({0}) for {1}: {2}".format(rrd['rrdb'], rrd['item'], e))
369+
self.logger.warning(f"Error creating rrd ({rrd['rrdb']}) for {rrd['item']}: {e}")

0 commit comments

Comments
 (0)