source: hacks/munin/fritzboxdect.py@ 107

Last change on this file since 107 was 107, checked in by bird, 8 years ago

fritzboxdect.py: exec

  • Property svn:eol-style set to native
  • Property svn:executable set to *
  • Property svn:keywords set to Id Revision
File size: 13.3 KB
Line 
1#!/usr/bin/env python
2# -*- coding: utf-8 -*-
3# $Id: fritzboxdect.py 107 2017-12-11 11:59:42Z 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: 107 $"
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
66class FritzBoxConnection(object):
67 """
68 A HTTP(S) connection to a fritz!box.
69 """
70
71 ## The user agent string to use.
72 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"
73
74 def __init__(self, sServer, sPassword, iPort = 80):
75 self.fDebug = len(os.environ.get('debug', '')) > 0;
76
77 #
78 # Connect to the fritz!box.
79 #
80 oConn = httplib.HTTPConnection('%s:%s' % (sServer, iPort));
81 self.oConn = oConn;
82
83 #
84 # Login - gets a SID back.
85 #
86 dLoginHdrs = {
87 "Accept": "application/xml",
88 "Content-Type": "text/plain",
89 "User-Agent": self.g_UserAgent,
90 };
91 oConn.request("GET", '/login_sid.lua', '', dLoginHdrs)
92 oResp = oConn.getresponse();
93 sData = oResp.read();
94 if oResp.status != 200:
95 raise Exception('login_sid.lua response: %s %s' % (oResp.status, oResp.reason,));
96 oXmlData = minidom.parseString(sData);
97 aoElmSids = oXmlData.getElementsByTagName('SID');
98 sSid = aoElmSids[0].firstChild.data;
99 if sSid == '0000000000000000':
100 # Hash the password and compose reply text.
101 aoElmChallenges = oXmlData.getElementsByTagName('Challenge');
102 sChallange = aoElmChallenges[0].firstChild.data;
103 sReplyHashedText = ('%s-%s' % (sChallange, sPassword)).decode('iso-8859-1').encode('utf-16le');
104 oReplyHash = hashlib.md5();
105 oReplyHash.update(sReplyHashedText);
106 sReplyText = '%s-%s' % (sChallange, oReplyHash.hexdigest().lower());
107
108 # Sent it.
109 dReplyHdrs = {
110 "Accept": "text/html,application/xhtml+xml,application/xml",
111 "Content-Type": "application/x-www-form-urlencoded",
112 "User-Agent": self.g_UserAgent,
113 };
114 oConn.request("GET", '/login_sid.lua?' + urllib_urlencode({'response': sReplyText,}), '', dReplyHdrs)
115 oResp = oConn.getresponse();
116 sData = oResp.read();
117 if oResp.status != 200:
118 raise Exception('login_sid.lua response: %s %s' % (oResp.status, oResp.reason,));
119 oXmlData = minidom.parseString(sData);
120 aoElmSids = oXmlData.getElementsByTagName('SID');
121 sSid = aoElmSids[0].firstChild.data;
122 if sSid == '0000000000000000':
123 raise Exception('login failure');
124 # Remember the SID.
125 self.sSid = sSid;
126
127 def getPage(self, sUrl, asArgs):
128 """
129 Retrieves the given page.
130
131 We would like to use a directory for the arguments, but fritzy is picky
132 about the ordering.
133 """
134
135 dPageHdrs = {
136 "Accept": "application/xml,text/xml",
137 "Content-Type": "text/plain",
138 "User-Agent": self.g_UserAgent,
139 };
140
141 sUrl = sUrl + '?sid=' + self.sSid;
142 for sArg in asArgs:
143 sName, sValue = sArg.split('=')
144 sUrl += '&' + urllib_quote(sName) + '=' + urllib_quote(sValue);
145 if self.fDebug:
146 print('debug: sUrl: %s' % (sUrl,));
147
148 self.oConn.request("GET", sUrl, '', dPageHdrs)
149 oResp = self.oConn.getresponse();
150 sData = oResp.read();
151 if oResp.status != 200:
152 raise Exception('%s response: %s %s' % (sUrl, oResp.status, oResp.reason,));
153 return sData;
154
155
156 @staticmethod
157 def getPages(sUrl, asArgs):
158 """
159 Gets an array of pages from each of the frizboxes.
160 """
161 asRet = [];
162 asIps = os.environ.get('fritzboxdect_ip', '10.42.2.1 10.42.1.50').split();
163 asPasswords = os.environ.get('fritzboxdect_password', '').split();
164 for i, sIp in enumerate(asIps):
165 sPassword = asPasswords[i] if i < len(asPasswords) else asPasswords[-1];
166 oConn = FritzBoxConnection(sIp, sPassword);
167 asRet.append(oConn.getPage(sUrl, asArgs));
168 del oConn;
169 return asRet;
170
171
172
173# XML output example:
174# <devicelist version="1">
175# <device identifier="11657 0072338" id="16" functionbitmask="2944" fwversion="03.87" manufacturer="AVM"
176# productname="FRITZ!DECT 210">
177# <present>1</present>
178# <name>Socket #1</name>
179# <switch>
180# <state>1</state>
181# <mode>manuell</mode>
182# <lock>1</lock>
183# <devicelock>0</devicelock>
184# </switch>
185# <powermeter>
186# <power>2730</power>
187# <energy>3602</energy>
188# </powermeter>
189# <temperature>
190# <celsius>255</celsius>
191# <offset>0</offset>
192# </temperature>
193# </device>
194# </devicelist>
195
196g_sPage = '/webservices/homeautoswitch.lua'
197g_fBitEnergy = 1 << 7;
198g_fBitTemp = 1 << 8;
199
200def getAllDeviceElements():
201 """
202 Connects to the fritz!boxes and gets the devicelistinfos.
203 Returns array of device elements, sorted by name.
204 """
205 asData = FritzBoxConnection.getPages(g_sPage, ['switchcmd=getdevicelistinfos']);
206 aoRet = [];
207 for sData in asData:
208 oXmlRoot = ElementTree.fromstring(sData);
209 for oElmDevice in oXmlRoot.findall('device'):
210 aoRet.append(oElmDevice);
211
212 def getKey(oElmDevice):
213 oName = oElmDevice.find('name');
214 if oName is not None:
215 return oName.text;
216 return oElmDevice.get('identifier');
217
218 return sorted(aoRet, key = getKey);
219
220
221def getDeviceVarName(oElmDevice):
222 """
223 Gets the graph variable name for the device.
224 """
225 sAin = oElmDevice.get('identifier');
226 sAin = ''.join(sAin.split());
227 return 'ain_%s' % (sAin,);
228
229
230def getDeviceBitmask(oElmDevice):
231 """
232 Gets the device bitmask.
233 """
234 sBitmask = oElmDevice.get('functionbitmask');
235 try:
236 fBitmask = int(sBitmask);
237 except:
238 sProduct = oElmDevice.get('productname');
239 if sProduct == 'FRITZ!DECT 210' or sProduct == 'FRITZ!DECT 200':
240 fBitmask = 0xb80;
241 elif sProduct == 'FRITZ!DECT 100':
242 fBitmask = 0x500;
243 else:
244 fBitmask = 0;
245 return fBitmask;
246
247
248def printValues():
249 """
250 Prints the values.
251 """
252 aoElmDevices = getAllDeviceElements()
253
254 print('multigraph power_consumption')
255 uTotal = 0;
256 for oElmDevice in aoElmDevices:
257 sVarNm = getDeviceVarName(oElmDevice);
258 if getDeviceBitmask(oElmDevice) & g_fBitEnergy:
259 oElmPowerMeter = oElmDevice.find('powermeter');
260 if oElmPowerMeter is not None:
261 sValue = oElmPowerMeter.find('energy').text.strip();
262 if sValue:
263 print('%s_wh.value %s' % (sVarNm, sValue,));
264 try: uTotal += int(sValue)
265 except: pass;
266 print('total_wh.value %s' % (uTotal,));
267
268 print('multigraph power_usage')
269 for oElmDevice in aoElmDevices:
270 sVarNm = getDeviceVarName(oElmDevice);
271 if getDeviceBitmask(oElmDevice) & g_fBitEnergy:
272 oElmPowerMeter = oElmDevice.find('powermeter');
273 if oElmPowerMeter is not None:
274 sValue = oElmPowerMeter.find('power').text.strip();
275 if sValue:
276 try: uTotal += int(sValue)
277 except: pass;
278 while len(sValue) < 4:
279 sValue = '0' + sValue;
280 print('%s_w.value %s.%s' % (sVarNm, sValue[:-3] , sValue[-3:]));
281 print('total_wh.value %.3f' % (uTotal / 1000.0,));
282
283 print('multigraph temp')
284 dAvg = {};
285 for oElmDevice in aoElmDevices:
286 sVarNm = getDeviceVarName(oElmDevice);
287 if getDeviceBitmask(oElmDevice) & g_fBitTemp:
288 oElmTemp = oElmDevice.find('temperature');
289 if oElmTemp is not None:
290 sValue = oElmTemp.find('celsius').text.strip();
291 if sValue:
292 print('%s_c.value %s.%s' % (sVarNm, sValue[:-1] if len(sValue) > 0 else '0', sValue[-1:]));
293 if oElmDevice.find('name') is not None:
294 try: uValue = int(sValue);
295 except: pass;
296 else:
297 sFloor = oElmDevice.find('name').text[:2];
298 if sFloor not in dAvg:
299 dAvg[sFloor] = (1, uValue);
300 else:
301 dAvg[sFloor] = (dAvg[sFloor][0] + 1, dAvg[sFloor][1] + uValue);
302 for sVarNm in sorted(dAvg.keys()):
303 print('%s_avg_c.value %.1f' % (sVarNm, dAvg[sVarNm][1] / 10.0 / dAvg[sVarNm][0]));
304 # done
305
306
307def printConfig():
308 """
309 Prints the configuration.
310 """
311 aoElmDevices = getAllDeviceElements()
312
313 print('multigraph power_consumption')
314 print('graph_title Power consumption');
315 print('graph_vlabel Wh');
316 print('graph_args --base 1000');
317 print("graph_category house");
318 for oElmDevice in aoElmDevices:
319 if getDeviceBitmask(oElmDevice) & g_fBitEnergy:
320 sVarNm = getDeviceVarName(oElmDevice);
321 print('%s_wh.label %s' % (sVarNm, oElmDevice.find('name').text,));
322 print('%s_wh.type COUNTER' % (sVarNm,));
323 print('%s_wh.draw LINE1' % (sVarNm,));
324 print('total_wh.label total');
325 print('total_wh.type COUNTER');
326 print('total_wh.draw LINE1');
327
328 print('multigraph power_usage')
329 print('graph_title Power usage');
330 print('graph_vlabel W');
331 print('graph_args --base 1000');
332 print("graph_category house");
333 print('graph_info Current power usage around the house');
334 for oElmDevice in aoElmDevices:
335 if getDeviceBitmask(oElmDevice) & g_fBitEnergy:
336 sVarNm = getDeviceVarName(oElmDevice);
337 print('%s_w.label %s' % (sVarNm, oElmDevice.find('name').text,));
338 print('%s_w.type GAUGE' % (sVarNm,));
339 print('%s_w.draw LINE1' % (sVarNm,));
340 print('total_w.label total');
341 print('total_w.type COUNTER');
342 print('total_w.draw LINE1');
343
344 print('multigraph temp')
345 print('graph_title Temperature');
346 print('graph_args --base 1000');
347 print('graph_vlabel Degrees (C)');
348 print('graph_scale no');
349 print('graph_category house');
350 print('graph_info Temperatures around the house');
351 print('temperature.type GAUGE');
352 dAvg = {};
353 for oElmDevice in aoElmDevices:
354 if getDeviceBitmask(oElmDevice) & g_fBitTemp:
355 sVarNm = getDeviceVarName(oElmDevice);
356 sName = oElmDevice.find('name').text;
357 print('%s_c.label %s' % (sVarNm, sName,));
358 print('%s_c.type GAUGE' % (sVarNm,));
359 print('%s_c.draw LINE1' % (sVarNm,));
360 dAvg[sName[:2]] = 1;
361 for sVarNm in sorted(dAvg.keys()):
362 print('%s_avg_c.label %s' % (sVarNm, sName,));
363 print('%s_avg_c.type GAUGE' % (sVarNm,));
364 print('%s_avg_c.draw LINE1' % (sVarNm,));
365
366
367def main(asArgs):
368 """
369 C-like main.
370 """
371 if len(asArgs) == 2 and asArgs[1] == 'config':
372 try: printConfig();
373 except Exception as oXcpt:
374 sys.exit('Failed to retreive configuration (%s)' % (oXcpt,));
375 return 0;
376
377 if len(asArgs) == 2 and asArgs[1] == 'autoconfig':
378 print("yes");
379 return 0;
380
381 if len(asArgs) == 1 \
382 or (len(asArgs) == 2 and asArgs[1] == 'fetch'):
383 try: printValues();
384 except Exception as oXcpt:
385 sys.exit('Failed to retreive data (%s)' % (oXcpt,));
386 return 0;
387
388 sys.exit('Unknown request (%s)' % (asArgs,));
389
390
391if __name__ == '__main__':
392 sys.exit(main(sys.argv))
393
Note: See TracBrowser for help on using the repository browser.