VirtualBox

source: kStuff/hacks/munin/fritzboxdect.py@ 109

Last change on this file since 109 was 109, checked in by bird, 7 years ago

fritzboxdect.py: average fixes

  • Property svn:eol-style set to native
  • Property svn:executable set to *
  • Property svn:keywords set to Id Revision
File size: 13.7 KB
Line 
1#!/usr/bin/env python
2# -*- coding: utf-8 -*-
3# $Id: fritzboxdect.py 109 2017-12-11 14:10:52Z bird $
4
5"""
6Fritz!box DECT sockets.
7
8Generates power consumption, current power usage, and temperature graphs.
9
10
11Configuration:
12
13[fritzboxdect]
14env.fritzboxdect_ip [ip addresses of the fritzboxes]
15env.fritzboxdect_password [passwords of the frizboxes]
16
17#%# family=auto contrib
18#%# capabilities=autoconf
19"""
20
21__copyright__ = \
22"""
23Copyright (c) 2017 Knut St. Osmundsen <bird-kStuff-spamix@anduin.net>
24
25Permission is hereby granted, free of charge, to any person
26obtaining a copy of this software and associated documentation
27files (the "Software"), to deal in the Software without
28restriction, including without limitation the rights to use,
29copy, modify, merge, publish, distribute, sublicense, and/or sell
30copies of the Software, and to permit persons to whom the
31Software is furnished to do so, subject to the following
32conditions:
33
34The above copyright notice and this permission notice shall be
35included in all copies or substantial portions of the Software.
36
37THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
38EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
39OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
40NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
41HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
42WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
43FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
44OTHER DEALINGS IN THE SOFTWARE.
45"""
46__version__ = "$Revision: 109 $"
47
48
49# Standard Python imports.
50import hashlib
51import httplib
52import os
53import sys
54from xml.etree import ElementTree
55from xml.dom import minidom
56
57if sys.version_info[0] < 3:
58 from urllib2 import quote as urllib_quote;
59 from urllib import urlencode as urllib_urlencode;
60else:
61 from urllib.parse import quote as urllib_quote; # pylint: disable=F0401,E0611
62 from urllib.parse import urlencode as urllib_urlencode; # pylint: disable=F0401,E0611
63
64
65## Wheter to display debug messages.
66g_fDebug = len(os.environ.get('debug', '')) > 0;
67
68
69class FritzBoxConnection(object):
70 """
71 A HTTP(S) connection to a fritz!box.
72 """
73
74 ## The user agent string to use.
75 g_UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36"
76
77 def __init__(self, sServer, sPassword, iPort = 80):
78 #
79 # Connect to the fritz!box.
80 #
81 oConn = httplib.HTTPConnection('%s:%s' % (sServer, iPort));
82 self.oConn = oConn;
83
84 #
85 # Login - gets a SID back.
86 #
87 dLoginHdrs = {
88 "Accept": "application/xml",
89 "Content-Type": "text/plain",
90 "User-Agent": self.g_UserAgent,
91 };
92 oConn.request("GET", '/login_sid.lua', '', dLoginHdrs)
93 oResp = oConn.getresponse();
94 sData = oResp.read();
95 if oResp.status != 200:
96 raise Exception('login_sid.lua response: %s %s' % (oResp.status, oResp.reason,));
97 oXmlData = minidom.parseString(sData);
98 aoElmSids = oXmlData.getElementsByTagName('SID');
99 sSid = aoElmSids[0].firstChild.data;
100 if sSid == '0000000000000000':
101 # Hash the password and compose reply text.
102 aoElmChallenges = oXmlData.getElementsByTagName('Challenge');
103 sChallange = aoElmChallenges[0].firstChild.data;
104 sReplyHashedText = ('%s-%s' % (sChallange, sPassword)).decode('iso-8859-1').encode('utf-16le');
105 oReplyHash = hashlib.md5();
106 oReplyHash.update(sReplyHashedText);
107 sReplyText = '%s-%s' % (sChallange, oReplyHash.hexdigest().lower());
108
109 # Sent it.
110 dReplyHdrs = {
111 "Accept": "text/html,application/xhtml+xml,application/xml",
112 "Content-Type": "application/x-www-form-urlencoded",
113 "User-Agent": self.g_UserAgent,
114 };
115 oConn.request("GET", '/login_sid.lua?' + urllib_urlencode({'response': sReplyText,}), '', dReplyHdrs)
116 oResp = oConn.getresponse();
117 sData = oResp.read();
118 if oResp.status != 200:
119 raise Exception('login_sid.lua response: %s %s' % (oResp.status, oResp.reason,));
120 oXmlData = minidom.parseString(sData);
121 aoElmSids = oXmlData.getElementsByTagName('SID');
122 sSid = aoElmSids[0].firstChild.data;
123 if sSid == '0000000000000000':
124 raise Exception('login failure');
125 # Remember the SID.
126 self.sSid = sSid;
127
128 def getPage(self, sUrl, asArgs):
129 """
130 Retrieves the given page.
131
132 We would like to use a directory for the arguments, but fritzy is picky
133 about the ordering.
134 """
135
136 dPageHdrs = {
137 "Accept": "application/xml,text/xml",
138 "Content-Type": "text/plain",
139 "User-Agent": self.g_UserAgent,
140 };
141
142 sUrl = sUrl + '?sid=' + self.sSid;
143 for sArg in asArgs:
144 sName, sValue = sArg.split('=')
145 sUrl += '&' + urllib_quote(sName) + '=' + urllib_quote(sValue);
146 if g_fDebug:
147 print('debug: sUrl: %s' % (sUrl,));
148
149 self.oConn.request("GET", sUrl, '', dPageHdrs)
150 oResp = self.oConn.getresponse();
151 sData = oResp.read();
152 if oResp.status != 200:
153 raise Exception('%s response: %s %s' % (sUrl, oResp.status, oResp.reason,));
154 return sData;
155
156
157 @staticmethod
158 def getPages(sUrl, asArgs):
159 """
160 Gets an array of pages from each of the frizboxes.
161 """
162 asRet = [];
163 asIps = os.environ.get('fritzboxdect_ip', '10.42.2.1 10.42.1.50').split();
164 if len(asIps) == 0:
165 raise Exception('environment variable fritzboxdect_ip is empty')
166 asPasswords = os.environ.get('fritzboxdect_password', '').split();
167 if len(asPasswords) == 0:
168 raise Exception('environment variable fritzboxdect_password is empty')
169 if g_fDebug:
170 print('debug: asIps=%s asPasswords=%s' % (asIps, asPasswords,));
171
172 for i, sIp in enumerate(asIps):
173 sPassword = asPasswords[i] if i < len(asPasswords) else asPasswords[-1];
174 oConn = FritzBoxConnection(sIp, sPassword);
175 asRet.append(oConn.getPage(sUrl, asArgs));
176 del oConn;
177 return asRet;
178
179
180
181# XML output example:
182# <devicelist version="1">
183# <device identifier="11657 0072338" id="16" functionbitmask="2944" fwversion="03.87" manufacturer="AVM"
184# productname="FRITZ!DECT 210">
185# <present>1</present>
186# <name>Socket #1</name>
187# <switch>
188# <state>1</state>
189# <mode>manuell</mode>
190# <lock>1</lock>
191# <devicelock>0</devicelock>
192# </switch>
193# <powermeter>
194# <power>2730</power>
195# <energy>3602</energy>
196# </powermeter>
197# <temperature>
198# <celsius>255</celsius>
199# <offset>0</offset>
200# </temperature>
201# </device>
202# </devicelist>
203
204g_sPage = '/webservices/homeautoswitch.lua'
205g_fBitEnergy = 1 << 7;
206g_fBitTemp = 1 << 8;
207
208def getAllDeviceElements():
209 """
210 Connects to the fritz!boxes and gets the devicelistinfos.
211 Returns array of device elements, sorted by name.
212 """
213 asData = FritzBoxConnection.getPages(g_sPage, ['switchcmd=getdevicelistinfos']);
214 aoRet = [];
215 for sData in asData:
216 oXmlRoot = ElementTree.fromstring(sData);
217 for oElmDevice in oXmlRoot.findall('device'):
218 aoRet.append(oElmDevice);
219
220 def getKey(oElmDevice):
221 oName = oElmDevice.find('name');
222 if oName is not None:
223 return oName.text;
224 return oElmDevice.get('identifier');
225
226 return sorted(aoRet, key = getKey);
227
228
229def getDeviceVarName(oElmDevice):
230 """
231 Gets the graph variable name for the device.
232 """
233 sAin = oElmDevice.get('identifier');
234 sAin = ''.join(sAin.split());
235 return 'ain_%s' % (sAin,);
236
237
238def getDeviceBitmask(oElmDevice):
239 """
240 Gets the device bitmask.
241 """
242 sBitmask = oElmDevice.get('functionbitmask');
243 try:
244 fBitmask = int(sBitmask);
245 except:
246 sProduct = oElmDevice.get('productname');
247 if sProduct == 'FRITZ!DECT 210' or sProduct == 'FRITZ!DECT 200':
248 fBitmask = 0xb80;
249 elif sProduct == 'FRITZ!DECT 100':
250 fBitmask = 0x500;
251 else:
252 fBitmask = 0;
253 return fBitmask;
254
255
256def printValues():
257 """
258 Prints the values.
259 """
260 aoElmDevices = getAllDeviceElements()
261
262 print('multigraph power_consumption')
263 uTotal = 0;
264 for oElmDevice in aoElmDevices:
265 sVarNm = getDeviceVarName(oElmDevice);
266 if getDeviceBitmask(oElmDevice) & g_fBitEnergy:
267 oElmPowerMeter = oElmDevice.find('powermeter');
268 if oElmPowerMeter is not None:
269 sValue = oElmPowerMeter.find('energy').text.strip();
270 if sValue:
271 print('%s_wh.value %s' % (sVarNm, sValue,));
272 try: uTotal += int(sValue)
273 except: pass;
274 print('total_wh.value %s' % (uTotal,));
275
276 print('multigraph power_usage')
277 for oElmDevice in aoElmDevices:
278 sVarNm = getDeviceVarName(oElmDevice);
279 if getDeviceBitmask(oElmDevice) & g_fBitEnergy:
280 oElmPowerMeter = oElmDevice.find('powermeter');
281 if oElmPowerMeter is not None:
282 sValue = oElmPowerMeter.find('power').text.strip();
283 if sValue:
284 try: uTotal += int(sValue)
285 except: pass;
286 while len(sValue) < 4:
287 sValue = '0' + sValue;
288 print('%s_w.value %s.%s' % (sVarNm, sValue[:-3] , sValue[-3:]));
289 print('total_wh.value %.3f' % (uTotal / 1000.0,));
290
291 print('multigraph temp')
292 dAvg = {};
293 for oElmDevice in aoElmDevices:
294 sVarNm = getDeviceVarName(oElmDevice);
295 if getDeviceBitmask(oElmDevice) & g_fBitTemp:
296 oElmTemp = oElmDevice.find('temperature');
297 if oElmTemp is not None:
298 sValue = oElmTemp.find('celsius').text.strip();
299 if sValue:
300 print('%s_c.value %s.%s' % (sVarNm, sValue[:-1] if len(sValue) > 0 else '0', sValue[-1:]));
301 if oElmDevice.find('name') is not None:
302 try: uValue = int(sValue);
303 except: pass;
304 else:
305 sFloor = oElmDevice.find('name').text[:2];
306 if sFloor not in dAvg:
307 dAvg[sFloor] = (1, uValue);
308 else:
309 dAvg[sFloor] = (dAvg[sFloor][0] + 1, dAvg[sFloor][1] + uValue);
310 for sVarNm in sorted(dAvg.keys()):
311 print('avg_%s_c.value %.1f' % (sVarNm, dAvg[sVarNm][1] / 10.0 / dAvg[sVarNm][0]));
312 # done
313
314
315def printConfig():
316 """
317 Prints the configuration.
318 """
319 aoElmDevices = getAllDeviceElements()
320
321 print('multigraph power_consumption')
322 print('graph_title Power consumption');
323 print('graph_vlabel Wh');
324 print('graph_args --base 1000');
325 print("graph_category house");
326 for oElmDevice in aoElmDevices:
327 if getDeviceBitmask(oElmDevice) & g_fBitEnergy:
328 sVarNm = getDeviceVarName(oElmDevice);
329 print('%s_wh.label %s' % (sVarNm, oElmDevice.find('name').text,));
330 print('%s_wh.type COUNTER' % (sVarNm,));
331 print('%s_wh.draw LINE1' % (sVarNm,));
332 print('total_wh.label total');
333 print('total_wh.type COUNTER');
334 print('total_wh.draw LINE1');
335
336 print('multigraph power_usage')
337 print('graph_title Power usage');
338 print('graph_vlabel W');
339 print('graph_args --base 1000');
340 print("graph_category house");
341 print('graph_info Current power usage around the house');
342 for oElmDevice in aoElmDevices:
343 if getDeviceBitmask(oElmDevice) & g_fBitEnergy:
344 sVarNm = getDeviceVarName(oElmDevice);
345 print('%s_w.label %s' % (sVarNm, oElmDevice.find('name').text,));
346 print('%s_w.type GAUGE' % (sVarNm,));
347 print('%s_w.draw LINE1' % (sVarNm,));
348 print('total_w.label total');
349 print('total_w.type COUNTER');
350 print('total_w.draw LINE1');
351
352 print('multigraph temp')
353 print('graph_title Temperature');
354 print('graph_args --base 1000');
355 print('graph_vlabel Degrees (C)');
356 print('graph_scale no');
357 print('graph_category house');
358 print('graph_info Temperatures around the house');
359 print('temperature.type GAUGE');
360 dAvg = {};
361 for oElmDevice in aoElmDevices:
362 if getDeviceBitmask(oElmDevice) & g_fBitTemp:
363 sVarNm = getDeviceVarName(oElmDevice);
364 sName = oElmDevice.find('name').text;
365 print('%s_c.label %s' % (sVarNm, sName,));
366 print('%s_c.type GAUGE' % (sVarNm,));
367 print('%s_c.draw LINE1' % (sVarNm,));
368 dAvg[sName[:2]] = 1;
369 for sVarNm in sorted(dAvg.keys()):
370 print('avg_%s_c.label Average %s' % (sVarNm, sVarNm,));
371 print('avg_%s_c.type GAUGE' % (sVarNm,));
372 print('avg_%s_c.draw LINE1' % (sVarNm,));
373
374
375def main(asArgs):
376 """
377 C-like main.
378 """
379 if len(asArgs) == 2 and asArgs[1] == 'config':
380 try: printConfig();
381 except Exception as oXcpt:
382 sys.exit('Failed to retreive configuration (%s)' % (oXcpt,));
383 return 0;
384
385 if len(asArgs) == 2 and asArgs[1] == 'autoconfig':
386 print("yes");
387 return 0;
388
389 if len(asArgs) == 1 \
390 or (len(asArgs) == 2 and asArgs[1] == 'fetch'):
391 try: printValues();
392 except Exception as oXcpt:
393 sys.exit('Failed to retreive data (%s)' % (oXcpt,));
394 return 0;
395
396 sys.exit('Unknown request (%s)' % (asArgs,));
397
398
399if __name__ == '__main__':
400 sys.exit(main(sys.argv))
401
Note: See TracBrowser for help on using the repository browser.

© 2024 Oracle Support Privacy / Do Not Sell My Info Terms of Use Trademark Policy Automated Access Etiquette