├── .gitignore ├── .travis.yml ├── Example └── PyDect200_Demo.py ├── LICENSE ├── MANIFEST ├── PyDect200 ├── PyDect200.py └── __init__.py ├── README.md ├── setup.cfg ├── setup.py └── test_PyDect200.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | dist 10 | build 11 | eggs 12 | parts 13 | bin 14 | var 15 | sdist 16 | develop-eggs 17 | .installed.cfg 18 | lib 19 | lib64 20 | __pycache__ 21 | 22 | # Installer logs 23 | pip-log.txt 24 | 25 | # Unit test / coverage reports 26 | .coverage 27 | .tox 28 | nosetests.xml 29 | 30 | # Translations 31 | *.mo 32 | 33 | # Mr Developer 34 | .mr.developer.cfg 35 | .project 36 | .pydevproject 37 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.6" 4 | - "2.7" 5 | - "3.3" 6 | - "3.4" 7 | - "3.5" 8 | - "3.6" 9 | - "nightly" 10 | - "pypy" 11 | - "pypy3" 12 | matrix: 13 | include: 14 | - python: 3.7 15 | dist: xenial 16 | sudo: true 17 | script: python test_PyDect200.py -------------------------------------------------------------------------------- /Example/PyDect200_Demo.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -- coding: utf-8 -- 3 | from __future__ import (absolute_import, division, 4 | print_function, unicode_literals) 5 | 6 | try: 7 | from PyDect200 import PyDect200 8 | except: 9 | print(u'PyDect200 is not installed!') 10 | print(u'run: pip install PyDect200') 11 | exit() 12 | import getpass 13 | 14 | try: 15 | PyDect200.__version__ 16 | except: 17 | PyDect200 = PyDect200.PyDect200 18 | 19 | print(u"Welcome to PyDect200 v%s, the Python AVM-DECT200 API" % PyDect200.__version__) 20 | fritzbox_username = getpass.getpass(prompt='Please insert your fritzbox username (press enter to skip): ', stream=None) 21 | fritzbox_pw = getpass.getpass(prompt='Please insert your fritzbox-password: ', stream=None) 22 | print(u'Thank you, please wait few seconds...') 23 | f = PyDect200(fritzbox_pw, username=fritzbox_username) 24 | 25 | if not f.login_ok(): 26 | print("Login Not Successful, Wrong Password?") 27 | exit(1) 28 | 29 | try: 30 | info = f.get_info() 31 | power = f.get_power_all() 32 | names = f.get_device_names() 33 | except Exception: 34 | print(u'HTTP-Error, wrong password?') 35 | exit() 36 | 37 | print(u'') 38 | for dev_id in info.keys(): 39 | print(u"Device ID: %s" % dev_id) 40 | dev_name = names.get(dev_id) 41 | try: 42 | print(u"Device Name: %s" % dev_name) 43 | except: 44 | print(u"Device Name: %s" % dev_name.encode('utf-8').decode('utf-8', 'ignore')) 45 | 46 | 47 | print(u"Device State: %s" % ('ON' if info.get(dev_id) == '1' else 'OFF')) 48 | dev_power = power.get(dev_id) 49 | if dev_power.isdigit(): 50 | dev_power = float(dev_power) / 1000 51 | print(u"Device Power: %sW" % dev_power) 52 | print(u"Device Energy: %sWh" % f.get_energy_single(dev_id)) 53 | print(u"Device Temperature: %s degree Celsius " % (f.get_temperature_single(dev_id))) 54 | print(u'') 55 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Mathias P. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /MANIFEST: -------------------------------------------------------------------------------- 1 | # file GENERATED by distutils, do NOT edit 2 | setup.cfg 3 | setup.py 4 | PyDect200/PyDect200.py 5 | PyDect200/__init__.py 6 | -------------------------------------------------------------------------------- /PyDect200/PyDect200.py: -------------------------------------------------------------------------------- 1 | # -- coding: utf-8 -- 2 | 3 | """ 4 | Module to Control the AVM DECT200 Socket 5 | """ 6 | 7 | from __future__ import (absolute_import, division, 8 | print_function, unicode_literals) 9 | import hashlib, sys 10 | try: 11 | import urllib.request as urllib2 12 | except ImportError: 13 | import urllib2 14 | 15 | class PyDect200(object): 16 | """ 17 | Class to Control the AVM DECT200 Socket 18 | """ 19 | __version__ = u'0.0.16' 20 | __author__ = u'Mathias Perlet' 21 | __author_email__ = u'mathias@mperlet.de' 22 | __description__ = u'Control Fritz AVM DECT200' 23 | 24 | __fritz_url = u'http://fritz.box' 25 | __homeswitch = u'/webservices/homeautoswitch.lua' 26 | __username_query = u'' 27 | 28 | __debug = False 29 | 30 | def __init__(self, fritz_password, username = u''): 31 | """The constructor""" 32 | self.__password = fritz_password 33 | if username != u'': 34 | self.__username_query = u'username=%s&' % username 35 | self.get_sid() 36 | 37 | def set_url(self, url): 38 | """Set alternative url""" 39 | self.__fritz_url = url 40 | 41 | def login_ok(self): 42 | """Returns True for a valid session id""" 43 | return self.sid is not None and self.sid != u'0000000000000000' 44 | 45 | def set_debug(self, enable=True): 46 | """Enables some debug prints""" 47 | self.__debug = enable 48 | 49 | def __homeauto_url_with_sid(self): 50 | """Returns formatted uri""" 51 | return u'%s%s?sid=%s' % (self.__fritz_url, 52 | self.__homeswitch, 53 | self.sid) 54 | 55 | @classmethod 56 | def __query(cls, url): 57 | """Reads a URL""" 58 | try: 59 | return urllib2.urlopen(url).read().decode('utf-8').replace('\n', '') 60 | except urllib2.HTTPError: 61 | _, exception, _ = sys.exc_info() 62 | if cls.__debug: 63 | print('HTTPError = ' + str(exception.code)) 64 | except urllib2.URLError: 65 | _, exception, _ = sys.exc_info() 66 | if cls.__debug: 67 | print('URLError = ' + str(exception.reason)) 68 | except Exception: 69 | _, exception, _ = sys.exc_info() 70 | if cls.__debug: 71 | print('generic exception: ' + str(exception)) 72 | raise 73 | pass 74 | return "inval" 75 | 76 | 77 | 78 | def __query_cmd(self, command, device=None): 79 | """Calls a command""" 80 | base_url = u'%s&switchcmd=%s' % (self.__homeauto_url_with_sid(), command) 81 | 82 | if device is None: 83 | url = base_url 84 | else: 85 | url = '%s&ain=%s' % (base_url, device) 86 | 87 | if self.__debug: 88 | print(u'Query Command URI: ' + url) 89 | 90 | return self.__query(url) 91 | 92 | def get_sid(self): 93 | """Returns a valid SID""" 94 | base_url = u'%s/login_sid.lua' % self.__fritz_url 95 | get_challenge = None 96 | try: 97 | get_challenge = urllib2.urlopen(base_url).read().decode('ascii') 98 | except urllib2.HTTPError as exception: 99 | print('HTTPError = ' + str(exception.code)) 100 | except urllib2.URLError as exception: 101 | print('URLError = ' + str(exception.reason)) 102 | except Exception as exception: 103 | print('generic exception: ' + str(exception)) 104 | raise 105 | 106 | 107 | challenge = get_challenge.split( 108 | '')[1].split('')[0] 109 | challenge_b = ( 110 | challenge + '-' + self.__password).encode().decode('iso-8859-1').encode('utf-16le') 111 | 112 | md5hash = hashlib.md5() 113 | md5hash.update(challenge_b) 114 | 115 | response_b = challenge + '-' + md5hash.hexdigest().lower() 116 | get_sid = urllib2.urlopen('%s?%sresponse=%s' % (base_url, self.__username_query, response_b)).read().decode('utf-8') 117 | self.sid = get_sid.split('')[1].split('')[0] 118 | 119 | def get_info(self): 120 | """Returns device info""" 121 | return self.get_state_all() 122 | 123 | def switch_onoff(self, device, status): 124 | """Switch a Socket""" 125 | if status == 1 or status == True or status == '1': 126 | return self.switch_on(device) 127 | else: 128 | return self.switch_off(device) 129 | 130 | def switch_toggle(self, device): 131 | """Toggles the current state of the given device""" 132 | state = self.get_state(device) 133 | if(state == '1'): 134 | return self.switch_off(device) 135 | 136 | elif(state == '0'): 137 | return self.switch_on(device) 138 | else: 139 | return state 140 | 141 | def get_power(self): 142 | """Returns the Power in Watt""" 143 | power_dict = self.get_power_all() 144 | for device in power_dict.keys(): 145 | power_dict[device] = float(power_dict[device]) / 1000.0 146 | return power_dict 147 | 148 | def get_device_ids(self): 149 | """Returns a list of device id strings""" 150 | return self.__query_cmd('getswitchlist').split(',') 151 | 152 | def get_device_names(self): 153 | """Returns a Dict with devicenames""" 154 | dev_names = {} 155 | for device in self.get_device_ids(): 156 | dev_names[device] = self.get_device_name(device) 157 | return dev_names 158 | 159 | def get_device_name(self, device): 160 | """Returns the name for a single device""" 161 | return self.__query_cmd('getswitchname', device) 162 | 163 | def get_power_single(self, device): 164 | """Returns the power in mW for a single device""" 165 | return self.__query_cmd('getswitchpower', device) 166 | 167 | def get_energy_single(self, device): 168 | """Returns the energy in Wh for a single device""" 169 | return self.__query_cmd('getswitchenergy', device) 170 | 171 | def get_temperature_single(self, device): 172 | """Returns the temperature in 0.1 °C for a single device""" 173 | temp_str = self.__query_cmd('gettemperature', device) 174 | if temp_str.lstrip('-').isdigit(): 175 | return float(temp_str) / 10.0 176 | return 'inval' 177 | 178 | def get_power_all(self): 179 | """Returns the power in mW for all devices""" 180 | power_dict = {} 181 | for device in self.get_device_names().keys(): 182 | power_dict[device] = self.get_power_single(device) 183 | return power_dict 184 | 185 | def switch_on(self, device): 186 | """Switch device on""" 187 | return self.__query_cmd('setswitchon', device) 188 | 189 | 190 | def switch_off(self, device): 191 | """Switch device off""" 192 | return self.__query_cmd('setswitchoff', device) 193 | 194 | 195 | def get_state(self, device): 196 | """Returns the device state""" 197 | return self.__query_cmd('getswitchstate', device) 198 | 199 | def get_state_all(self): 200 | """Returns all device states""" 201 | state_dict = {} 202 | for device in self.get_device_names().keys(): 203 | state_dict[device] = self.get_state(device) 204 | return state_dict 205 | -------------------------------------------------------------------------------- /PyDect200/__init__.py: -------------------------------------------------------------------------------- 1 | from PyDect200 import * 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | PyDect200 2 | ====== 3 | [![Build Status](https://travis-ci.org/mperlet/PyDect200.svg?branch=master)](https://travis-ci.org/mperlet/PyDect200) 4 | ![pylint Score](https://mperlet.github.io/pybadge/badges/9.12.svg) 5 | [![Download format](http://img.shields.io/pypi/format/PyDect200.svg)](https://pypi.python.org/pypi/PY_DECT200/) 6 | [![Downloads](http://img.shields.io/pypi/dm/PyDect200.svg)](https://pypi.python.org/pypi/PY_DECT200/) 7 | [![License](http://img.shields.io/pypi/l/PyDect200.svg)](https://pypi.python.org/pypi/PY_DECT200/) 8 | [![Latest Version](http://img.shields.io/pypi/v/PyDect200.svg)](https://pypi.python.org/pypi/PY_DECT200/) 9 | 10 | 11 | Control the Fritz-AVM DECT200 (switch a electric socket) 12 | and Fritz-AVM PowerLine 546E 13 | 14 | ### Install 15 | 16 | ``` 17 | pip install PyDect200 18 | ``` 19 | 20 | ### Demo 21 | 22 | #### Demo (Github Style) 23 | 24 | ``` 25 | curl https://raw.githubusercontent.com/mperlet/PyDect200/master/Example/PyDect200_Demo.py | python 26 | ``` 27 | 28 | #### Demo (git clone) 29 | 30 | ``` 31 | git clone git@github.com:mperlet/PyDect200.git 32 | 33 | ./PyDect200/Example/PyDect200_Demo.py 34 | ``` 35 | 36 | ### Example Code 37 | 38 | ``` 39 | from PyDect200 import PyDect200 40 | f = PyDect200('fitzbox_password') 41 | # or with username PyDect200('fritzbox_password', username='fritzbox_username') 42 | 43 | f.get_device_names() 44 | # {'16': 'Beleuchtung', '17': 'Fernseher'} 45 | 46 | f.get_info() 47 | # {u'16': u'0', u'17': u'0'} 48 | 49 | f.switch_onoff(16,1) 50 | # {u'DeviceID': u'16', 51 | # u'RequestResult': u'1', 52 | # u'Value': u'0', 53 | # u'ValueToSet': u'1'} 54 | 55 | f.get_power() 56 | # {u'16': 68.95, u'17': 0.0} 57 | ``` 58 | 59 | ### Tested with 60 | 61 | * Python2.7 / Python3.4 62 | * Fritzbox 7270 63 | * FRITZ!OS: 06.05 64 | * AVM Dect200 65 | 66 | ****************** 67 | 68 | * Python2.7 69 | * Fritzbox 7490 70 | * FRITZ!OS: 6.36 Labor 71 | * Dect200 72 | * PowerLine 546E 73 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.md 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from distutils.core import setup 4 | from PyDect200 import PyDect200 5 | try: 6 | PyDect200.__version__ 7 | except: 8 | PyDect200 = PyDect200.PyDect200 9 | 10 | setup(name='PyDect200', 11 | version=PyDect200.__version__, 12 | description=PyDect200.__description__, 13 | author=PyDect200.__author__, 14 | author_email=PyDect200.__author_email__, 15 | license='MIT', 16 | url='https://github.com/mperlet/PyDect200', 17 | packages=['PyDect200'], 18 | keywords = ['avm', 'dect200', 'fritzbox', 'dect', 'switch', 'smart home', 'PowerLine 546E'], 19 | ) 20 | -------------------------------------------------------------------------------- /test_PyDect200.py: -------------------------------------------------------------------------------- 1 | from __future__ import (absolute_import, division, 2 | print_function, unicode_literals) 3 | 4 | import sys 5 | import unittest 6 | 7 | from PyDect200.PyDect200 import PyDect200 8 | 9 | try: 10 | from unittest.mock import patch, MagicMock # Py3 11 | except Exception: 12 | from mock import patch, Mock # Py2 13 | 14 | PY2 = 2 15 | PY3 = 3 16 | 17 | 18 | def run(python_version): 19 | def run_decorator(func): 20 | def function_wrapper(x): 21 | if (sys.version_info > (3, 0)) and python_version == PY3: 22 | func(x) 23 | elif (sys.version_info < (3, 0)) and python_version == PY2: 24 | func(x) 25 | else: 26 | return True 27 | 28 | return function_wrapper 29 | 30 | return run_decorator 31 | 32 | 33 | class TestPyDect200(unittest.TestCase): 34 | 35 | @run(PY2) 36 | @patch('PyDect200.urllib2.urlopen') 37 | def test_basic_commands_py2(self, mock_urlopen): 38 | cm = Mock() 39 | cm.read.side_effect = ['1234', 40 | 'caffeaffe1234', 41 | 'device1,device2,device3', 'true'] 42 | mock_urlopen.return_value = cm 43 | 44 | instance = PyDect200("testtest") 45 | 46 | self.assertEqual(instance.sid, 'caffeaffe1234') 47 | 48 | self.assertEqual(instance.get_device_ids(), 49 | ['device1', 'device2', 'device3']) 50 | self.assertEqual(instance.switch_onoff('device1', True), 'true') 51 | 52 | @run(PY2) 53 | @patch('PyDect200.PyDect200.get_sid') 54 | def test_session_id_py2(self, mock_sid): 55 | mock_sid.return_value = "hey" 56 | 57 | instance = PyDect200("testtest") 58 | instance.sid = 'caffeaffe1234' 59 | self.assertEqual(instance.sid, 'caffeaffe1234') 60 | 61 | @run(PY3) 62 | @patch('urllib.request.urlopen') 63 | def test_basic_commands_py3(self, mock_urlopen): 64 | cm = MagicMock() 65 | cm.read.side_effect = ['1234'.encode(), 66 | 'caffeaffe1234'.encode(), 67 | 'device1,device2,device3'.encode(), 68 | 'true'.encode()] 69 | mock_urlopen.return_value = cm 70 | 71 | instance = PyDect200("testtest") 72 | 73 | self.assertEqual(instance.sid, 'caffeaffe1234') 74 | 75 | self.assertEqual(instance.get_device_ids(), 76 | ['device1', 'device2', 'device3']) 77 | self.assertEqual(instance.switch_onoff('device1', True), 'true') 78 | 79 | 80 | if __name__ == '__main__': 81 | unittest.main() 82 | --------------------------------------------------------------------------------