VirtualBox

source: kStuff/hacks/munin/fritzboxdect.py

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

frizboxdect.py: Don't try strip None.

  • Property svn:eol-style set to native
  • Property svn:executable set to *
  • Property svn:keywords set to Id Revision
File size: 13.9 KB
Line 
1#!/usr/bin/env python
2# -*- coding: utf-8 -*-
3# $Id: fritzboxdect.py 110 2017-12-18 23:48:49Z 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: 110 $"
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;
270 if sValue is not None:
271 sValue = oElmPowerMeter.find('energy').text.strip();
272 if sValue:
273 print('%s_wh.value %s' % (sVarNm, sValue,));
274 try: uTotal += int(sValue)
275 except: pass;
276 print('total_wh.value %s' % (uTotal,));
277
278 print('multigraph power_usage')
279 for oElmDevice in aoElmDevices:
280 sVarNm = getDeviceVarName(oElmDevice);
281 if getDeviceBitmask(oElmDevice) & g_fBitEnergy:
282 oElmPowerMeter = oElmDevice.find('powermeter');
283 if oElmPowerMeter is not None:
284 sValue = oElmPowerMeter.find('power').text;
285 if sValue is not None:
286 sValue = sValue.strip();
287 if sValue:
288 try: uTotal += int(sValue)
289 except: pass;
290 while len(sValue) < 4:
291 sValue = '0' + sValue;
292 print('%s_w.value %s.%s' % (sVarNm, sValue[:-3] , sValue[-3:]));
293 print('total_wh.value %.3f' % (uTotal / 1000.0,));
294
295 print('multigraph temp')
296 dAvg = {};
297 for oElmDevice in aoElmDevices:
298 sVarNm = getDeviceVarName(oElmDevice);
299 if getDeviceBitmask(oElmDevice) & g_fBitTemp:
300 oElmTemp = oElmDevice.find('temperature');
301 if oElmTemp is not None:
302 sValue = oElmTemp.find('celsius').text;
303 if sValue is not None:
304 sValue = sValue.strip();
305 if sValue:
306 print('%s_c.value %s.%s' % (sVarNm, sValue[:-1] if len(sValue) > 0 else '0', sValue[-1:]));
307 if oElmDevice.find('name') is not None:
308 try: uValue = int(sValue);
309 except: pass;
310 else:
311 sFloor = oElmDevice.find('name').text[:2];
312 if sFloor not in dAvg:
313 dAvg[sFloor] = (1, uValue);
314 else:
315 dAvg[sFloor] = (dAvg[sFloor][0] + 1, dAvg[sFloor][1] + uValue);
316 for sVarNm in sorted(dAvg.keys()):
317 print('avg_%s_c.value %.1f' % (sVarNm, dAvg[sVarNm][1] / 10.0 / dAvg[sVarNm][0]));
318 # done
319
320
321def printConfig():
322 """
323 Prints the configuration.
324 """
325 aoElmDevices = getAllDeviceElements()
326
327 print('multigraph power_consumption')
328 print('graph_title Power consumption');
329 print('graph_vlabel Wh');
330 print('graph_args --base 1000');
331 print("graph_category house");
332 for oElmDevice in aoElmDevices:
333 if getDeviceBitmask(oElmDevice) & g_fBitEnergy:
334 sVarNm = getDeviceVarName(oElmDevice);
335 print('%s_wh.label %s' % (sVarNm, oElmDevice.find('name').text,));
336 print('%s_wh.type COUNTER' % (sVarNm,));
337 print('%s_wh.draw LINE1' % (sVarNm,));
338 print('total_wh.label total');
339 print('total_wh.type COUNTER');
340 print('total_wh.draw LINE1');
341
342 print('multigraph power_usage')
343 print('graph_title Power usage');
344 print('graph_vlabel W');
345 print('graph_args --base 1000');
346 print("graph_category house");
347 print('graph_info Current power usage around the house');
348 for oElmDevice in aoElmDevices:
349 if getDeviceBitmask(oElmDevice) & g_fBitEnergy:
350 sVarNm = getDeviceVarName(oElmDevice);
351 print('%s_w.label %s' % (sVarNm, oElmDevice.find('name').text,));
352 print('%s_w.type GAUGE' % (sVarNm,));
353 print('%s_w.draw LINE1' % (sVarNm,));
354 print('total_w.label total');
355 print('total_w.type COUNTER');
356 print('total_w.draw LINE1');
357
358 print('multigraph temp')
359 print('graph_title Temperature');
360 print('graph_args --base 1000');
361 print('graph_vlabel Degrees (C)');
362 print('graph_scale no');
363 print('graph_category house');
364 print('graph_info Temperatures around the house');
365 print('temperature.type GAUGE');
366 dAvg = {};
367 for oElmDevice in aoElmDevices:
368 if getDeviceBitmask(oElmDevice) & g_fBitTemp:
369 sVarNm = getDeviceVarName(oElmDevice);
370 sName = oElmDevice.find('name').text;
371 print('%s_c.label %s' % (sVarNm, sName,));
372 print('%s_c.type GAUGE' % (sVarNm,));
373 print('%s_c.draw LINE1' % (sVarNm,));
374 dAvg[sName[:2]] = 1;
375 for sVarNm in sorted(dAvg.keys()):
376 print('avg_%s_c.label Average %s' % (sVarNm, sVarNm,));
377 print('avg_%s_c.type GAUGE' % (sVarNm,));
378 print('avg_%s_c.draw LINE1' % (sVarNm,));
379
380
381def main(asArgs):
382 """
383 C-like main.
384 """
385 if len(asArgs) == 2 and asArgs[1] == 'config':
386 try: printConfig();
387 except Exception as oXcpt:
388 sys.exit('Failed to retreive configuration (%s)' % (oXcpt,));
389 return 0;
390
391 if len(asArgs) == 2 and asArgs[1] == 'autoconfig':
392 print("yes");
393 return 0;
394
395 if len(asArgs) == 1 \
396 or (len(asArgs) == 2 and asArgs[1] == 'fetch'):
397 try: printValues();
398 except Exception as oXcpt:
399 sys.exit('Failed to retreive data (%s)' % (oXcpt,));
400 return 0;
401
402 sys.exit('Unknown request (%s)' % (asArgs,));
403
404
405if __name__ == '__main__':
406 sys.exit(main(sys.argv))
407
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