├── .hound.yml ├── pywemo ├── ouimeaux_device │ ├── api │ │ ├── xsd │ │ │ ├── __init__.py │ │ │ ├── service.xsd │ │ │ ├── device.xsd │ │ │ └── service.py │ │ ├── __init__.py │ │ └── service.py │ ├── motion.py │ ├── lightswitch.py │ ├── switch.py │ ├── LICENSE │ ├── dimmer.py │ ├── maker.py │ ├── insight.py │ ├── coffeemaker.py │ ├── __init__.py │ ├── humidifier.py │ └── bridge.py ├── __init__.py ├── util.py ├── color.py ├── discovery.py ├── subscribe.py └── ssdp.py ├── requirements.txt ├── setup.cfg ├── .github └── PULL_REQUEST_TEMPLATE.md ├── script └── check_dirty ├── .gitattributes ├── requirements_test.txt ├── .travis.yml ├── setup.py ├── tox.ini ├── pylintrc ├── .gitignore └── README.rst /.hound.yml: -------------------------------------------------------------------------------- 1 | python: 2 | enabled: true 3 | -------------------------------------------------------------------------------- /pywemo/ouimeaux_device/api/xsd/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pywemo/ouimeaux_device/api/__init__.py: -------------------------------------------------------------------------------- 1 | """WeMo device API.""" 2 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | netifaces>=0.10.0 2 | requests>=2.0 3 | six>=1.10.0 4 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal = 1 3 | 4 | [flake8] 5 | exclude = .venv,.git,.tox,docs,venv,bin,lib,deps,build 6 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Description: 2 | 3 | 4 | **Related issue (if applicable):** fixes # 5 | 6 | ## Checklist: 7 | - [ ] The code change is tested and works locally. 8 | - [ ] There is no commented out code in this PR. -------------------------------------------------------------------------------- /script/check_dirty: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | [[ -z $(git ls-files --others --exclude-standard) ]] && exit 0 3 | 4 | echo -e '\n***** ERROR\nTests are leaving files behind. Please update the tests to avoid writing any files:' 5 | git ls-files --others --exclude-standard 6 | echo 7 | exit 1 8 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Ensure Docker script files uses LF to support Docker for Windows. 2 | # Ensure "git config --global core.autocrlf input" before you clone 3 | * text eol=lf 4 | *.py whitespace=error 5 | 6 | *.ico binary 7 | *.jpg binary 8 | *.png binary 9 | *.zip binary 10 | *.mp3 binary 11 | -------------------------------------------------------------------------------- /requirements_test.txt: -------------------------------------------------------------------------------- 1 | # linters such as flake8 and pylint should be pinned, as new releases 2 | # make new things fail. Manually update these pins when pulling in a 3 | # new version 4 | flake8-docstrings==1.3.0 5 | flake8==3.6.0 6 | mypy==0.650 7 | pydocstyle==3.0.0 8 | pylint==2.2.2 9 | pytest==4.0.2 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | addons: 3 | apt: 4 | packages: 5 | - libudev-dev 6 | matrix: 7 | fast_finish: true 8 | include: 9 | - python: "3.5.3" 10 | env: TOXENV=lint 11 | - python: "3.5.3" 12 | env: TOXENV=pylint 13 | 14 | cache: 15 | directories: 16 | - $HOME/.cache/pip 17 | install: pip install -U tox 18 | language: python 19 | script: travis_wait 30 tox --develop 20 | -------------------------------------------------------------------------------- /pywemo/ouimeaux_device/motion.py: -------------------------------------------------------------------------------- 1 | """Representation of a WeMo Motion device.""" 2 | from . import Device 3 | 4 | 5 | class Motion(Device): 6 | """Representation of a WeMo Motion device.""" 7 | 8 | def __repr__(self): 9 | """Return a string representation of the device.""" 10 | return ''.format(name=self.name) 11 | 12 | @property 13 | def device_type(self): 14 | """Return what kind of WeMo this device is.""" 15 | return "Motion" 16 | -------------------------------------------------------------------------------- /pywemo/ouimeaux_device/lightswitch.py: -------------------------------------------------------------------------------- 1 | """Representation of a WeMo Motion device.""" 2 | from .switch import Switch 3 | 4 | 5 | class LightSwitch(Switch): 6 | """Representation of a WeMo Motion device.""" 7 | 8 | def __repr__(self): 9 | """Return a string representation of the device.""" 10 | return ''.format(name=self.name) 11 | 12 | @property 13 | def device_type(self): 14 | """Return what kind of WeMo this device is.""" 15 | return "LightSwitch" 16 | -------------------------------------------------------------------------------- /pywemo/__init__.py: -------------------------------------------------------------------------------- 1 | """Lightweight Python module to discover and control WeMo devices.""" 2 | 3 | from .ouimeaux_device import Device as WeMoDevice # noqa F401 4 | from .ouimeaux_device.insight import Insight # noqa F401 5 | from .ouimeaux_device.lightswitch import LightSwitch # noqa F401 6 | from .ouimeaux_device.dimmer import Dimmer # noqa F401 7 | from .ouimeaux_device.motion import Motion # noqa F401 8 | from .ouimeaux_device.switch import Switch # noqa F401 9 | from .ouimeaux_device.maker import Maker # noqa F401 10 | from .ouimeaux_device.coffeemaker import CoffeeMaker # noqa F401 11 | from .ouimeaux_device.bridge import Bridge # noqa F401 12 | from .ouimeaux_device.humidifier import Humidifier # noqa F401 13 | 14 | from .discovery import discover_devices # noqa F401 15 | from .subscribe import SubscriptionRegistry # noqa F401 16 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """pyWeMo setup script.""" 2 | 3 | from setuptools import setup, find_packages 4 | 5 | CONST_DESC = 'Lightweight Python module to discover and control WeMo devices' 6 | 7 | 8 | setup(name='pywemo', 9 | version='0.4.39', 10 | description=CONST_DESC, 11 | long_description=open('README.rst').read(), 12 | url='http://github.com/pavoni/pywemo', 13 | author='Greg Dowling', 14 | author_email='mail@gregdowling.com', 15 | license='MIT', 16 | install_requires=['netifaces>=0.10.0', 'requests>=2.0', 'six>=1.10.0'], 17 | packages=find_packages(), 18 | zip_safe=True, 19 | classifiers=[ 20 | "Programming Language :: Python :: 2", 21 | "Programming Language :: Python :: 3", 22 | "License :: OSI Approved :: MIT License", 23 | "Operating System :: OS Independent", 24 | "Topic :: Home Automation"]) 25 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = lint, pylint 3 | skip_missing_interpreters = True 4 | 5 | [testenv] 6 | setenv = 7 | PYTHONPATH = {toxinidir}:{toxinidir}/pywemo 8 | whitelist_externals = /usr/bin/env 9 | install_command = /usr/bin/env LANG=C.UTF-8 pip install {opts} {packages} 10 | commands = 11 | pytest {posargs} 12 | {toxinidir}/script/check_dirty 13 | deps = 14 | -r{toxinidir}/requirements_test.txt 15 | 16 | [testenv:pylint] 17 | basepython = {env:PYTHON3_PATH:python3} 18 | ignore_errors = True 19 | deps = 20 | -r{toxinidir}/requirements.txt 21 | -r{toxinidir}/requirements_test.txt 22 | commands = 23 | pylint {posargs} pywemo 24 | 25 | [testenv:lint] 26 | basepython = {env:PYTHON3_PATH:python3} 27 | deps = 28 | -r{toxinidir}/requirements_test.txt 29 | commands = 30 | flake8 {posargs} 31 | 32 | [flake8] 33 | exclude = 34 | .tox, 35 | .git, 36 | __pycache__, 37 | pywemo/ouimeaux_device/api/xsd/*, 38 | *.pyc, 39 | *.egg-info, 40 | .cache, 41 | .eggs 42 | -------------------------------------------------------------------------------- /pylintrc: -------------------------------------------------------------------------------- 1 | [MASTER] 2 | ignore=ouimeaux_device 3 | reports=no 4 | 5 | # A comma-separated list of package or module names from where C extensions may 6 | # be loaded. Extensions are loading into the active Python interpreter and may 7 | # run arbitrary code 8 | extension-pkg-whitelist=netifaces 9 | 10 | disable= 11 | abstract-class-little-used, 12 | abstract-method, 13 | cyclic-import, 14 | duplicate-code, 15 | global-statement, 16 | inconsistent-return-statements, 17 | locally-disabled, 18 | not-an-iterable, 19 | not-context-manager, 20 | redefined-variable-type, 21 | too-few-public-methods, 22 | too-many-arguments, 23 | too-many-branches, 24 | too-many-instance-attributes, 25 | too-many-lines, 26 | too-many-locals, 27 | too-many-public-methods, 28 | too-many-return-statements, 29 | too-many-statements, 30 | unnecessary-pass, 31 | unused-argument 32 | 33 | [REPORTS] 34 | reports=no 35 | 36 | [FORMAT] 37 | expected-line-ending-format=LF 38 | 39 | [EXCEPTIONS] 40 | overgeneral-exceptions=Exception 41 | -------------------------------------------------------------------------------- /pywemo/ouimeaux_device/switch.py: -------------------------------------------------------------------------------- 1 | """Representation of a WeMo Switch device.""" 2 | from . import Device 3 | 4 | 5 | class Switch(Device): 6 | """Representation of a WeMo Switch device.""" 7 | 8 | def set_state(self, state): 9 | """Set the state of this device to on or off.""" 10 | # pylint: disable=maybe-no-member 11 | self.basicevent.SetBinaryState(BinaryState=int(state)) 12 | self._state = int(state) 13 | 14 | def off(self): 15 | """Turn this device off. If already off, will return "Error".""" 16 | return self.set_state(0) 17 | 18 | # pylint: disable=invalid-name 19 | def on(self): 20 | """Turn this device on. If already on, will return "Error".""" 21 | return self.set_state(1) 22 | 23 | def toggle(self): 24 | """Toggle the switch's state.""" 25 | return self.set_state(not self.get_state()) 26 | 27 | def __repr__(self): 28 | """Return a string representation of the device.""" 29 | return ''.format(name=self.name) 30 | 31 | @property 32 | def device_type(self): 33 | """Return what kind of WeMo this device is.""" 34 | return "Humidifier" 35 | -------------------------------------------------------------------------------- /pywemo/ouimeaux_device/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014, Ian McCracken 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 5 | 6 | * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 7 | 8 | * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 9 | 10 | * Neither the name of ouimeaux nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 11 | 12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Hide sublime text stuff 2 | *.sublime-project 3 | *.sublime-workspace 4 | 5 | # Hide some OS X stuff 6 | .DS_Store 7 | .AppleDouble 8 | .LSOverride 9 | Icon 10 | 11 | # Thumbnails 12 | ._* 13 | 14 | # IntelliJ IDEA 15 | .idea 16 | *.iml 17 | 18 | # pytest 19 | .pytest_cache 20 | .cache 21 | 22 | # GITHUB Proposed Python stuff: 23 | *.py[cod] 24 | 25 | # C extensions 26 | *.so 27 | 28 | # Packages 29 | *.egg 30 | *.egg-info 31 | dist 32 | build 33 | eggs 34 | .eggs 35 | parts 36 | bin 37 | var 38 | sdist 39 | develop-eggs 40 | .installed.cfg 41 | lib 42 | lib64 43 | 44 | # Logs 45 | *.log 46 | pip-log.txt 47 | 48 | # Unit test / coverage reports 49 | .coverage 50 | .tox 51 | nosetests.xml 52 | htmlcov/ 53 | 54 | # Translations 55 | *.mo 56 | 57 | # Mr Developer 58 | .mr.developer.cfg 59 | .project 60 | .pydevproject 61 | 62 | .python-version 63 | 64 | # emacs auto backups 65 | *~ 66 | *# 67 | *.orig 68 | 69 | # venv stuff 70 | pyvenv.cfg 71 | pip-selfcheck.json 72 | venv 73 | .venv 74 | Pipfile* 75 | share/* 76 | 77 | # vimmy stuff 78 | *.swp 79 | *.swo 80 | 81 | ctags.tmp 82 | 83 | # vagrant stuff 84 | virtualization/vagrant/setup_done 85 | virtualization/vagrant/.vagrant 86 | virtualization/vagrant/config 87 | 88 | # Visual Studio Code 89 | .vscode 90 | 91 | # Built docs 92 | docs/build 93 | 94 | # Windows Explorer 95 | desktop.ini 96 | /home-assistant.pyproj 97 | /home-assistant.sln 98 | /.vs/* 99 | 100 | # mypy 101 | /.mypy_cache/* 102 | 103 | # Secrets 104 | .lokalise_token 105 | 106 | # monkeytype 107 | monkeytype.sqlite3 108 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | pyWeMo |Build Status| 2 | ===================== 3 | Lightweight Python 2 and Python 3 module to discover and control WeMo devices. 4 | 5 | This is a stripped down version of the Python API for WeMo devices [ouimeaux](https://github.com/iancmcc/ouimeaux) with simpler dependencies. 6 | 7 | Dependencies 8 | ------------ 9 | pyWeMo depends on Python packages requests, netifaces and six. 10 | 11 | How to use 12 | ---------- 13 | 14 | .. code:: python 15 | 16 | >> import pywemo 17 | 18 | >> devices = pywemo.discover_devices() 19 | >> print(devices) 20 | [] 21 | 22 | >> devices[0].toggle() 23 | 24 | 25 | If discovery doesn't work on your network 26 | ----------------------------------------- 27 | On some networks discovery doesn't work reliably, in that case if you can find the ip address of your Wemo device you can use the following code. 28 | 29 | .. code:: python 30 | 31 | >> import pywemo 32 | 33 | >> address = "192.168.100.193" 34 | >> port = pywemo.ouimeaux_device.probe_wemo(address) 35 | >> url = 'http://%s:%i/setup.xml' % (address, port) 36 | >> device = pywemo.discovery.device_from_description(url, None) 37 | >> print(device) 38 | 39 | 40 | Please note that you need to use ip addresses as shown above, rather than hostnames, otherwise the subscription update logic won't work. 41 | 42 | License 43 | ------- 44 | The code in pywemo/ouimeaux_device is written and copyright by Ian McCracken and released under the BSD license. The rest is released under the MIT license. 45 | 46 | .. |Build Status| image:: https://travis-ci.org/pavoni/pywemo.svg?branch=master 47 | :target: https://travis-ci.org/pavoni/pywemo 48 | -------------------------------------------------------------------------------- /pywemo/util.py: -------------------------------------------------------------------------------- 1 | """Miscellaneous utility functions.""" 2 | from collections import defaultdict 3 | import netifaces 4 | 5 | 6 | # Taken from http://stackoverflow.com/a/10077069 7 | def etree_to_dict(tree): 8 | """Split a tree into a dict.""" 9 | # strip namespace 10 | tag_name = tree.tag[tree.tag.find("}")+1:] 11 | 12 | tree_dict = {tag_name: {} if tree.attrib else None} 13 | children = list(tree) 14 | if children: 15 | default_dict = defaultdict(list) 16 | for dict_children in map(etree_to_dict, children): 17 | for key, value in dict_children.items(): 18 | default_dict[key].append(value) 19 | tree_dict = { 20 | tag_name: { 21 | key: value[0] if len(value) == 1 else value 22 | for key, value in 23 | default_dict.items()}} 24 | if tree.attrib: 25 | tree_dict[tag_name].update(('@' + key, value) 26 | for key, value in tree.attrib.items()) 27 | if tree.text: 28 | text = tree.text.strip() 29 | if children or tree.attrib: 30 | if text: 31 | tree_dict[tag_name]['#text'] = text 32 | else: 33 | tree_dict[tag_name] = text 34 | return tree_dict 35 | 36 | 37 | def interface_addresses(family=netifaces.AF_INET): 38 | """ 39 | Return local address for broadcast/multicast. 40 | 41 | Return local address of any network associated with a local interface 42 | that has broadcast (and probably multicast) capability. 43 | """ 44 | return [addr['addr'] 45 | for i in netifaces.interfaces() 46 | for addr in netifaces.ifaddresses(i).get(family) or [] 47 | if 'broadcast' in addr] 48 | -------------------------------------------------------------------------------- /pywemo/ouimeaux_device/dimmer.py: -------------------------------------------------------------------------------- 1 | """Representation of a WeMo Dimmer device.""" 2 | from .switch import Switch 3 | 4 | 5 | class Dimmer(Switch): 6 | """Representation of a WeMo Dimmer device.""" 7 | 8 | def __init__(self, *args, **kwargs): 9 | """Create a WeMo Dimmer device.""" 10 | Switch.__init__(self, *args, **kwargs) 11 | self._brightness = None 12 | 13 | def get_brightness(self, force_update=False): 14 | """Get brightness from device.""" 15 | if force_update or self._brightness is None: 16 | try: 17 | # pylint: disable=maybe-no-member 18 | brightness = self.basicevent.GetBinaryState().get('brightness') 19 | except ValueError: 20 | brightness = 0 21 | self._brightness = brightness 22 | 23 | return self._brightness 24 | 25 | def set_brightness(self, brightness): 26 | """ 27 | Set the brightness of this device to an integer between 1-100. 28 | 29 | Setting the brightness does not turn the light on, so we need 30 | to check the state of the switch. 31 | """ 32 | if brightness == 0: 33 | if self.get_state() != 0: 34 | self.off() 35 | else: 36 | if self.get_state() == 0: 37 | self.on() 38 | 39 | # pylint: disable=maybe-no-member 40 | self.basicevent.SetBinaryState(brightness=int(brightness)) 41 | self._brightness = int(brightness) 42 | 43 | def subscription_update(self, _type, _param): 44 | """Disable subscription updates.""" 45 | return False 46 | 47 | def __repr__(self): 48 | """Return a string representation of the device.""" 49 | return ''.format(name=self.name) 50 | 51 | @property 52 | def device_type(self): 53 | """Return what kind of WeMo this device is.""" 54 | return "Dimmer" 55 | -------------------------------------------------------------------------------- /pywemo/color.py: -------------------------------------------------------------------------------- 1 | """Various utilities for handling colors.""" 2 | 3 | # Define usable ranges as bulbs either ignore or behave unexpectedly 4 | # when it is sent a value is outside of the range. 5 | TEMPERATURE_PROFILES = dict((model, temp) for models, temp in ( 6 | # Lightify RGBW, 1900-6500K 7 | (["LIGHTIFY A19 RGBW"], (151, 555)), 8 | ) for model in models) 9 | 10 | COLOR_PROFILES = dict((model, gamut) for models, gamut in ( 11 | # Lightify RGBW, 1900-6500K 12 | # http://flow-morewithless.blogspot.com/2015/01/osram-lightify-color-gamut-and-spectrum.html 13 | (["LIGHTIFY A19 RGBW"], 14 | ((0.683924, 0.315904), (0.391678, 0.501414), (0.136990, 0.051035))), 15 | ) for model in models) 16 | 17 | 18 | def get_profiles(model): 19 | """Return the temperature and color profiles for a given model.""" 20 | return (TEMPERATURE_PROFILES.get(model, (150, 600)), 21 | COLOR_PROFILES.get(model, ((1., 0.), (0., 1.), (0., 0.)))) 22 | 23 | 24 | # pylint: disable=invalid-name 25 | def is_same_side(p1, p2, a, b): 26 | """Test if points p1 and p2 lie on the same side of line a-b.""" 27 | vector_ab = [y - x for x, y in zip(a, b)] 28 | vector_ap1 = [y - x for x, y in zip(a, p1)] 29 | vector_ap2 = [y - x for x, y in zip(a, p2)] 30 | cross_vab_ap1 = vector_ab[0] * vector_ap1[1] - vector_ab[1] * vector_ap1[0] 31 | cross_vab_ap2 = vector_ab[0] * vector_ap2[1] - vector_ab[1] * vector_ap2[0] 32 | return (cross_vab_ap1 * cross_vab_ap2) >= 0 33 | 34 | 35 | # pylint: disable=invalid-name 36 | def closest_point(p, a, b): 37 | """Test if points p1 and p2 lie on the same side of line a-b.""" 38 | vector_ab = [y - x for x, y in zip(a, b)] 39 | vector_ap = [y - x for x, y in zip(a, p)] 40 | dot_ap_ab = sum(x * y for x, y in zip(vector_ap, vector_ab)) 41 | dot_ab_ab = sum(x * y for x, y in zip(vector_ab, vector_ab)) 42 | t = max(0.0, min(dot_ap_ab / dot_ab_ab, 1.0)) 43 | return a[0] + vector_ab[0] * t, a[1] + vector_ab[1] * t 44 | 45 | 46 | # pylint: disable=invalid-name 47 | def limit_to_gamut(xy, gamut): 48 | """Return the closest point within the gamut triangle for colorxy.""" 49 | r, g, b = gamut 50 | 51 | # http://www.blackpawn.com/texts/pointinpoly/ 52 | if not is_same_side(xy, r, g, b): 53 | xy = closest_point(xy, g, b) 54 | 55 | if not is_same_side(xy, g, b, r): 56 | xy = closest_point(xy, b, r) 57 | 58 | if not is_same_side(xy, b, r, g): 59 | xy = closest_point(xy, r, g) 60 | 61 | return xy 62 | -------------------------------------------------------------------------------- /pywemo/ouimeaux_device/maker.py: -------------------------------------------------------------------------------- 1 | """Representation of a WeMo Maker device.""" 2 | from xml.etree import cElementTree as et 3 | from .switch import Switch 4 | 5 | 6 | class Maker(Switch): 7 | """Representation of a WeMo Maker device.""" 8 | 9 | def __repr__(self): 10 | """Return a string representation of the device.""" 11 | return ''.format(name=self.name) 12 | 13 | @property 14 | def maker_params(self): 15 | """Get and parse the device attributes.""" 16 | # pylint: disable=maybe-no-member 17 | makerresp = self.deviceevent.GetAttributes().get('attributeList') 18 | makerresp = "" + makerresp + "" 19 | makerresp = makerresp.replace(">", ">") 20 | makerresp = makerresp.replace("<", "<") 21 | attributes = et.fromstring(makerresp) 22 | for attribute in attributes: 23 | if attribute[0].text == "Switch": 24 | switchstate = attribute[1].text 25 | elif attribute[0].text == "Sensor": 26 | sensorstate = attribute[1].text 27 | elif attribute[0].text == "SwitchMode": 28 | switchmode = attribute[1].text 29 | elif attribute[0].text == "SensorPresent": 30 | hassensor = attribute[1].text 31 | return { 32 | 'switchstate': int(switchstate), 33 | 'sensorstate': int(sensorstate), 34 | 'switchmode': int(switchmode), 35 | 'hassensor': int(hassensor)} 36 | 37 | def get_state(self, force_update=False): 38 | """Return 0 if off and 1 if on.""" 39 | # The base implementation using GetBinaryState doesn't 40 | # work for the Maker (always returns 0), 41 | # so pull the switch state from the atrributes instead 42 | if force_update or self._state is None: 43 | params = self.maker_params or {} 44 | try: 45 | self._state = int(params.get('switchstate', 0)) 46 | except ValueError: 47 | self._state = 0 48 | 49 | return self._state 50 | 51 | def set_state(self, state): 52 | """Set the state of this device to on or off.""" 53 | # The Maker has a momentary mode - so it's not safe to assume 54 | # the state is what you just set, so re-read it from the device 55 | 56 | # pylint: disable=maybe-no-member 57 | self.basicevent.SetBinaryState(BinaryState=int(state)) 58 | self.get_state(True) 59 | 60 | @property 61 | def device_type(self): 62 | """Return what kind of WeMo this device is.""" 63 | return "Maker" 64 | 65 | @property 66 | def sensor_state(self): 67 | """Return the state of the sensor.""" 68 | return self.maker_params['sensorstate'] 69 | 70 | @property 71 | def switch_mode(self): 72 | """Return the switch mode of the sensor.""" 73 | return self.maker_params['switchmode'] 74 | 75 | @property 76 | def has_sensor(self): 77 | """Return whether the device has a sensor.""" 78 | return self.maker_params['hassensor'] 79 | -------------------------------------------------------------------------------- /pywemo/ouimeaux_device/api/xsd/service.xsd: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | XML Schema for UPnP service descriptions in real XSD format 9 | (not like the XDR one from Microsoft) 10 | Created by Michael Weinrich 2007 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /pywemo/discovery.py: -------------------------------------------------------------------------------- 1 | """Module to discover WeMo devices.""" 2 | import logging 3 | import requests 4 | 5 | from . import ssdp 6 | from .ouimeaux_device.bridge import Bridge 7 | from .ouimeaux_device.insight import Insight 8 | from .ouimeaux_device.lightswitch import LightSwitch 9 | from .ouimeaux_device.dimmer import Dimmer 10 | from .ouimeaux_device.motion import Motion 11 | from .ouimeaux_device.switch import Switch 12 | from .ouimeaux_device.maker import Maker 13 | from .ouimeaux_device.coffeemaker import CoffeeMaker 14 | from .ouimeaux_device.humidifier import Humidifier 15 | from .ouimeaux_device.api.xsd import device as deviceParser 16 | 17 | LOG = logging.getLogger(__name__) 18 | 19 | 20 | def discover_devices(ssdp_st=None, max_devices=None, 21 | match_mac=None, match_serial=None, 22 | rediscovery_enabled=True): 23 | """Find WeMo devices on the local network.""" 24 | ssdp_st = ssdp_st or ssdp.ST 25 | ssdp_entries = ssdp.scan(ssdp_st, max_entries=max_devices, 26 | match_mac=match_mac, match_serial=match_serial) 27 | 28 | wemos = [] 29 | 30 | for entry in ssdp_entries: 31 | if entry.match_device_description( 32 | {'manufacturer': 'Belkin International Inc.'}): 33 | mac = entry.description.get('device').get('macAddress') 34 | device = device_from_description( 35 | description_url=entry.location, mac=mac, 36 | rediscovery_enabled=rediscovery_enabled) 37 | 38 | if device is not None: 39 | wemos.append(device) 40 | 41 | return wemos 42 | 43 | 44 | def device_from_description(description_url, mac, rediscovery_enabled=True): 45 | """Return object representing WeMo device running at host, else None.""" 46 | xml = requests.get(description_url, timeout=10) 47 | uuid = deviceParser.parseString(xml.content).device.UDN 48 | device_mac = mac or deviceParser.parseString(xml.content).device.macAddress 49 | 50 | if device_mac is None: 51 | LOG.debug( 52 | 'No MAC address was supplied or found in setup xml at: %s.', 53 | description_url) 54 | 55 | return device_from_uuid_and_location( 56 | uuid, device_mac, description_url, 57 | rediscovery_enabled=rediscovery_enabled) 58 | 59 | 60 | def device_from_uuid_and_location(uuid, mac, location, 61 | rediscovery_enabled=True): 62 | """Determine device class based on the device uuid.""" 63 | if uuid is None: 64 | return None 65 | if uuid.startswith('uuid:Socket'): 66 | return Switch(url=location, mac=mac, 67 | rediscovery_enabled=rediscovery_enabled) 68 | if uuid.startswith('uuid:Lightswitch'): 69 | return LightSwitch(url=location, mac=mac, 70 | rediscovery_enabled=rediscovery_enabled) 71 | if uuid.startswith('uuid:Dimmer'): 72 | return Dimmer(url=location, mac=mac, 73 | rediscovery_enabled=rediscovery_enabled) 74 | if uuid.startswith('uuid:Insight'): 75 | return Insight(url=location, mac=mac, 76 | rediscovery_enabled=rediscovery_enabled) 77 | if uuid.startswith('uuid:Sensor'): 78 | return Motion(url=location, mac=mac, 79 | rediscovery_enabled=rediscovery_enabled) 80 | if uuid.startswith('uuid:Maker'): 81 | return Maker(url=location, mac=mac, 82 | rediscovery_enabled=rediscovery_enabled) 83 | if uuid.startswith('uuid:Bridge'): 84 | return Bridge(url=location, mac=mac, 85 | rediscovery_enabled=rediscovery_enabled) 86 | if uuid.startswith('uuid:CoffeeMaker'): 87 | return CoffeeMaker(url=location, mac=mac, 88 | rediscovery_enabled=rediscovery_enabled) 89 | if uuid.startswith('uuid:Humidifier'): 90 | return Humidifier(url=location, mac=mac, 91 | rediscovery_enabled=rediscovery_enabled) 92 | 93 | return None 94 | -------------------------------------------------------------------------------- /pywemo/ouimeaux_device/insight.py: -------------------------------------------------------------------------------- 1 | """Representation of a WeMo Insight device.""" 2 | import logging 3 | from datetime import datetime 4 | from .switch import Switch 5 | 6 | LOG = logging.getLogger(__name__) 7 | 8 | 9 | class Insight(Switch): 10 | """Representation of a WeMo Insight device.""" 11 | 12 | def __init__(self, *args, **kwargs): 13 | """Create a WeMo Switch device.""" 14 | Switch.__init__(self, *args, **kwargs) 15 | self.insight_params = {} 16 | 17 | self.update_insight_params() 18 | 19 | def __repr__(self): 20 | """Return a string representation of the device.""" 21 | return ''.format(name=self.name) 22 | 23 | def update_insight_params(self): 24 | """Get and parse the device attributes.""" 25 | # pylint: disable=maybe-no-member 26 | params = self.insight.GetInsightParams().get('InsightParams') 27 | self.insight_params = self.parse_insight_params(params) 28 | 29 | def subscription_update(self, _type, _params): 30 | """Update the device attributes due to a subscription update event.""" 31 | LOG.debug("subscription_update %s %s", _type, _params) 32 | if _type == "InsightParams": 33 | self.insight_params = self.parse_insight_params(_params) 34 | return True 35 | return Switch.subscription_update(self, _type, _params) 36 | 37 | def parse_insight_params(self, params): 38 | """Parse the Insight parameters.""" 39 | ( 40 | state, # 0 if off, 1 if on, 8 if on but load is off 41 | lastchange, 42 | onfor, # seconds 43 | ontoday, # seconds 44 | ontotal, # seconds 45 | timeperiod, # pylint: disable=unused-variable 46 | _x, # This one is always 19 for me; what is it? 47 | currentmw, 48 | todaymw, 49 | totalmw, 50 | powerthreshold 51 | ) = params.split('|') 52 | return {'state': state, 53 | 'lastchange': datetime.fromtimestamp(int(lastchange)), 54 | 'onfor': int(onfor), 55 | 'ontoday': int(ontoday), 56 | 'ontotal': int(ontotal), 57 | 'todaymw': int(float(todaymw)), 58 | 'totalmw': int(float(totalmw)), 59 | 'currentpower': int(float(currentmw)), 60 | 'powerthreshold': int(float(powerthreshold))} 61 | 62 | def get_state(self, force_update=False): 63 | """Return the device state.""" 64 | if force_update or self._state is None: 65 | self.update_insight_params() 66 | 67 | return Switch.get_state(self, force_update) 68 | 69 | @property 70 | def device_type(self): 71 | """Return what kind of WeMo this device is.""" 72 | return "Insight" 73 | 74 | @property 75 | def today_kwh(self): 76 | """Return the kwh used today.""" 77 | return self.insight_params['todaymw'] * 1.6666667e-8 78 | 79 | @property 80 | def current_power(self): 81 | """Return the current power usage in mW.""" 82 | return self.insight_params['currentpower'] 83 | 84 | @property 85 | def threshold_power(self): 86 | """ 87 | Return the threshold power. 88 | 89 | Above this the device is on, below it is standby. 90 | """ 91 | return self.insight_params['powerthreshold'] 92 | 93 | @property 94 | def today_on_time(self): 95 | """Return how long the device has been on today.""" 96 | return self.insight_params['ontoday'] 97 | 98 | @property 99 | def on_for(self): 100 | """Return how long the device has been on.""" 101 | return self.insight_params['onfor'] 102 | 103 | @property 104 | def last_change(self): 105 | """Return the last change datetime.""" 106 | return self.insight_params['lastchange'] 107 | 108 | @property 109 | def today_standby_time(self): 110 | """Return how long the device has been in standby today.""" 111 | return self.insight_params['ontoday'] 112 | 113 | @property 114 | def get_standby_state(self): 115 | """Return the standby state of the device.""" 116 | state = self.insight_params['state'] 117 | 118 | if state == '0': 119 | return 'off' 120 | 121 | if state == '1': 122 | return 'on' 123 | 124 | return 'standby' 125 | -------------------------------------------------------------------------------- /pywemo/ouimeaux_device/api/xsd/device.xsd: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 11 | XML Schema for UPnP device descriptions in real XSD format 12 | (not like the XDR one from Microsoft) 13 | Created by Michael Weinrich 2007 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | -------------------------------------------------------------------------------- /pywemo/ouimeaux_device/api/service.py: -------------------------------------------------------------------------------- 1 | """Representation of Services and Actions for WeMo devices.""" 2 | # flake8: noqa E501 3 | import logging 4 | from xml.etree import cElementTree as et 5 | 6 | import requests 7 | 8 | from .xsd import service as serviceParser 9 | 10 | 11 | LOG = logging.getLogger(__name__) 12 | MAX_RETRIES = 3 13 | 14 | REQUEST_TEMPLATE = """ 15 | 16 | 17 | 18 | 19 | {args} 20 | 21 | 22 | 23 | """ 24 | 25 | 26 | class ActionException(Exception): 27 | """Generic exceptions when dealing with Actions.""" 28 | 29 | pass 30 | 31 | 32 | class Action: 33 | """Representation of an Action for a WeMo device.""" 34 | 35 | def __init__(self, device, service, action_config): 36 | """Create an instance of an Action.""" 37 | self._device = device 38 | self._action_config = action_config 39 | self.name = action_config.get_name() 40 | # pylint: disable=invalid-name 41 | self.serviceType = service.serviceType 42 | self.controlURL = service.controlURL 43 | self.args = {} 44 | self.headers = { 45 | 'Content-Type': 'text/xml', 46 | 'SOAPACTION': '"%s#%s"' % (self.serviceType, self.name) 47 | } 48 | 49 | arglist = action_config.get_argumentList() 50 | if arglist is not None: 51 | for arg in arglist.get_argument(): 52 | self.args[arg.get_name()] = 0 53 | 54 | def __call__(self, **kwargs): 55 | """Representations a method or function call.""" 56 | arglist = '\n'.join('<{0}>{1}'.format(arg, value) 57 | for arg, value in kwargs.items()) 58 | body = REQUEST_TEMPLATE.format( 59 | action=self.name, 60 | service=self.serviceType, 61 | args=arglist 62 | ) 63 | for attempt in range(3): 64 | try: 65 | response = requests.post( 66 | self.controlURL, body.strip(), 67 | headers=self.headers, timeout=10) 68 | response_dict = {} 69 | # pylint: disable=deprecated-method 70 | for response_item in et.fromstring( 71 | response.content 72 | ).getchildren()[0].getchildren()[0].getchildren(): 73 | response_dict[response_item.tag] = response_item.text 74 | return response_dict 75 | except requests.exceptions.RequestException: 76 | LOG.warning("Error communicating with %s at %s:%i, retry %i", 77 | self._device.name, self._device.host, 78 | self._device.port, attempt) 79 | 80 | if self._device.rediscovery_enabled: 81 | self._device.reconnect_with_device() 82 | 83 | LOG.error("Error communicating with %s after %i attempts. Giving up.", 84 | self._device.name, MAX_RETRIES) 85 | 86 | raise ActionException( 87 | "Error communicating with {0} after {1} attempts." 88 | "Giving up.".format(self._device.name, MAX_RETRIES)) 89 | 90 | def __repr__(self): 91 | """Return a string representation of the Action.""" 92 | return "" % (self.name, ", ".join(self.args)) 93 | 94 | 95 | class Service: 96 | """Representation of a service for a WeMo device.""" 97 | 98 | def __init__(self, device, service, base_url): 99 | """Create an instance of a Service.""" 100 | self._base_url = base_url.rstrip('/') 101 | self._config = service 102 | self.name = self._config.get_serviceType().split(':')[-2] 103 | self.actions = {} 104 | 105 | url = '%s/%s' % (base_url, service.get_SCPDURL().strip('/')) 106 | xml = requests.get(url, timeout=10) 107 | if xml.status_code != 200: 108 | return 109 | 110 | self._svc_config = serviceParser.parseString(xml.content).actionList 111 | for action in self._svc_config.get_action(): 112 | act = Action(device, self, action) 113 | name = action.get_name() 114 | self.actions[name] = act 115 | setattr(self, name, act) 116 | 117 | @property 118 | def hostname(self): 119 | """Get the hostname from the base URL.""" 120 | return self._base_url.split('/')[-1] 121 | 122 | # pylint: disable=invalid-name 123 | @property 124 | def controlURL(self): 125 | """Get the controlURL for interacting with this Service.""" 126 | return '%s/%s' % (self._base_url, 127 | self._config.get_controlURL().strip('/')) 128 | 129 | @property 130 | def serviceType(self): 131 | """Get the type of this Service.""" 132 | return self._config.get_serviceType() 133 | 134 | def __repr__(self): 135 | """Return a string representation of the Service.""" 136 | return "" % (self.name, ", ".join(self.actions)) 137 | -------------------------------------------------------------------------------- /pywemo/ouimeaux_device/coffeemaker.py: -------------------------------------------------------------------------------- 1 | """Representation of a WeMo CofeeMaker device.""" 2 | from xml.etree import cElementTree as et 3 | import sys 4 | from pywemo.ouimeaux_device.api.xsd.device import quote_xml 5 | from .switch import Switch 6 | 7 | if sys.version_info[0] < 3: 8 | class IntEnum: 9 | """Enum class.""" 10 | 11 | pass 12 | else: 13 | from enum import IntEnum 14 | 15 | 16 | # These enums were derived from the 17 | # CoffeeMaker.deviceevent.GetAttributeList() service call 18 | # Thus these names/values were not chosen randomly 19 | # and the numbers have meaning. 20 | class CoffeeMakerMode(IntEnum): 21 | """Enum to map WeMo modes to human-readable strings.""" 22 | 23 | Refill = 0 # reservoir empty and carafe not in place 24 | PlaceCarafe = 1 # reservoir has water but carafe not present 25 | RefillWater = 2 # carafe present but reservoir is empty 26 | Ready = 3 27 | Brewing = 4 28 | Brewed = 5 29 | CleaningBrewing = 6 30 | CleaningSoaking = 7 31 | BrewFailCarafeRemoved = 8 32 | 33 | 34 | MODE_NAMES = { 35 | CoffeeMakerMode.Refill: "Refill", 36 | CoffeeMakerMode.PlaceCarafe: "PlaceCarafe", 37 | CoffeeMakerMode.RefillWater: "RefillWater", 38 | CoffeeMakerMode.Ready: "Ready", 39 | CoffeeMakerMode.Brewing: "Brewing", 40 | CoffeeMakerMode.Brewed: "Brewed", 41 | CoffeeMakerMode.CleaningBrewing: "CleaningBrewing", 42 | CoffeeMakerMode.CleaningSoaking: "CleaningSoaking", 43 | CoffeeMakerMode.BrewFailCarafeRemoved: "BrewFailCarafeRemoved", 44 | } 45 | 46 | 47 | def attribute_xml_to_dict(xml_blob): 48 | """Return integer value of Mode from an attributesList blob, if present.""" 49 | xml_blob = "" + xml_blob + "" 50 | xml_blob = xml_blob.replace(">", ">") 51 | xml_blob = xml_blob.replace("<", "<") 52 | result = {} 53 | attributes = et.fromstring(xml_blob) 54 | for attribute in attributes: 55 | # The coffee maker might also send unrelated xml blobs, e.g.: 56 | # coffee-brewed 57 | # 58 | # so be sure to check the length of attribute 59 | if len(attribute) >= 2: 60 | try: 61 | result[attribute[0].text] = int(attribute[1].text) 62 | except ValueError: 63 | pass 64 | return result 65 | 66 | 67 | class CoffeeMaker(Switch): 68 | """Representation of a WeMo CofeeMaker device.""" 69 | 70 | def __init__(self, *args, **kwargs): 71 | """Create a WeMo CoffeeMaker device.""" 72 | Switch.__init__(self, *args, **kwargs) 73 | self._attributes = {} 74 | 75 | def __repr__(self): 76 | """Return a string representation of the device.""" 77 | return ''.format(name=self.name) 78 | 79 | def update_attributes(self): 80 | """Request state from device.""" 81 | # pylint: disable=maybe-no-member 82 | resp = self.deviceevent.GetAttributes().get('attributeList') 83 | self._attributes = attribute_xml_to_dict(resp) 84 | self._state = self.mode 85 | 86 | def subscription_update(self, _type, _params): 87 | """Handle reports from device.""" 88 | if _type == "attributeList": 89 | self._attributes.update(attribute_xml_to_dict(_params)) 90 | self._state = self.mode 91 | return True 92 | 93 | return Switch.subscription_update(self, _type, _params) 94 | 95 | @property 96 | def device_type(self): 97 | """Return what kind of WeMo this device is.""" 98 | return "CoffeeMaker" 99 | 100 | @property 101 | def mode(self): 102 | """Return the mode of the device.""" 103 | return self._attributes.get('Mode') 104 | 105 | @property 106 | def mode_string(self): 107 | """Return the mode of the device as a string.""" 108 | return MODE_NAMES.get(self.mode, "Unknown") 109 | 110 | def get_state(self, force_update=False): 111 | """Return 0 if off and 1 if on.""" 112 | # The base implementation using GetBinaryState doesn't 113 | # work for CoffeeMaker (always returns 0), so use mode instead. 114 | if force_update or self._state is None: 115 | self.update_attributes() 116 | 117 | # Consider the Coffee Maker to be "on" if it's currently brewing. 118 | return int(self._state == CoffeeMakerMode.Brewing) 119 | 120 | def set_state(self, state): 121 | """Set the state of this device to on or off.""" 122 | # CoffeeMaker cannot be turned off remotely, 123 | # so ignore the request if state is "falsey" 124 | if state: 125 | # Coffee Maker always responds with an error if 126 | # SetBinaryState is called. Use SetAttributes 127 | # to change the Mode to "Brewing" 128 | 129 | # pylint: disable=maybe-no-member 130 | self.deviceevent.SetAttributes(attributeList=quote_xml( 131 | "Mode4")) 132 | 133 | # The Coffee Maker might not be ready - so it's not safe 134 | # to assume the state is what you just set, 135 | # so re-read it from the device 136 | self.get_state(True) 137 | -------------------------------------------------------------------------------- /pywemo/ouimeaux_device/__init__.py: -------------------------------------------------------------------------------- 1 | """Base WeMo Device class.""" 2 | 3 | import logging 4 | import time 5 | 6 | try: 7 | from urllib.parse import urlparse 8 | except ImportError: 9 | from urlparse import urlparse 10 | 11 | import requests 12 | 13 | from .api.service import Service 14 | from .api.xsd import device as deviceParser 15 | 16 | LOG = logging.getLogger(__name__) 17 | 18 | # Start with the most commonly used port 19 | PROBE_PORTS = (80,49153, 49152, 49154, 49151, 49155, 49156, 49157, 49158, 49159) 20 | 21 | 22 | def probe_wemo(host, ports=PROBE_PORTS, probe_timeout=10): 23 | """ 24 | Probe a host for the current port. 25 | 26 | This probes a host for known-to-be-possible ports and 27 | returns the one currently in use. If no port is discovered 28 | then it returns None. 29 | """ 30 | for port in ports: 31 | try: 32 | response = requests.get('http://%s:%i/setup.xml' % (host, port), 33 | timeout=probe_timeout) 34 | if ('WeMo' in response.text) or ('Belkin' in response.text): 35 | return port 36 | except requests.ConnectTimeout: 37 | # If we timed out connecting, then the wemo is gone, 38 | # no point in trying further. 39 | LOG.debug('Timed out connecting to %s on port %i, ' 40 | 'wemo is offline', host, port) 41 | break 42 | except requests.Timeout: 43 | # Apparently sometimes wemos get into a wedged state where 44 | # they still accept connections on an old port, but do not 45 | # respond. If that happens, we should keep searching. 46 | LOG.debug('No response from %s on port %i, continuing', 47 | host, port) 48 | continue 49 | except requests.ConnectionError: 50 | pass 51 | return None 52 | 53 | 54 | def probe_device(device): 55 | """Probe a device for available port. 56 | 57 | This is an extension for probe_wemo, also probing current port. 58 | """ 59 | ports = list(PROBE_PORTS) 60 | if device.port in ports: 61 | ports.remove(device.port) 62 | ports.insert(0, device.port) 63 | 64 | return probe_wemo(device.host, ports) 65 | 66 | 67 | class UnknownService(Exception): 68 | """Exception raised when a non-existent service is called.""" 69 | 70 | pass 71 | 72 | 73 | class Device(object): 74 | """Base object for WeMo devices.""" 75 | 76 | def __init__(self, url, mac, rediscovery_enabled=True): 77 | """Create a WeMo device.""" 78 | self._state = None 79 | self.basic_state_params = {} 80 | base_url = url.rsplit('/', 1)[0] 81 | parsed_url = urlparse(url) 82 | self.host = parsed_url.hostname 83 | self.port = parsed_url.port 84 | self.retrying = False 85 | self.mac = mac 86 | self.rediscovery_enabled = rediscovery_enabled 87 | xml = requests.get(url, timeout=10) 88 | self._config = deviceParser.parseString(xml.content).device 89 | service_list = self._config.serviceList 90 | self.services = {} 91 | for svc in service_list.service: 92 | svcname = svc.get_serviceType().split(':')[-2] 93 | service = Service(self, svc, base_url) 94 | service.eventSubURL = base_url + svc.get_eventSubURL() 95 | self.services[svcname] = service 96 | setattr(self, svcname, service) 97 | 98 | def _reconnect_with_device_by_discovery(self): 99 | """ 100 | Scan network to find the device again. 101 | 102 | Wemos tend to change their port number from time to time. 103 | Whenever requests throws an error, we will try to find the device again 104 | on the network and update this device. 105 | """ 106 | # Put here to avoid circular dependency 107 | from ..discovery import discover_devices 108 | 109 | # Avoid retrying from multiple threads 110 | if self.retrying: 111 | return 112 | 113 | self.retrying = True 114 | LOG.info("Trying to reconnect with %s", self.name) 115 | # We will try to find it 5 times, each time we wait a bigger interval 116 | try_no = 0 117 | 118 | while True: 119 | found = discover_devices(ssdp_st=None, max_devices=1, 120 | match_mac=self.mac, 121 | match_serial=self.serialnumber) 122 | 123 | if found: 124 | LOG.info("Found %s again, updating local values", self.name) 125 | 126 | # pylint: disable=attribute-defined-outside-init 127 | self.__dict__ = found[0].__dict__ 128 | self.retrying = False 129 | 130 | return 131 | 132 | wait_time = try_no * 5 133 | 134 | LOG.info( 135 | "%s Not found in try %i. Trying again in %i seconds", 136 | self.name, try_no, wait_time) 137 | 138 | if try_no == 5: 139 | LOG.error( 140 | "Unable to reconnect with %s in 5 tries. Stopping.", 141 | self.name) 142 | self.retrying = False 143 | 144 | return 145 | 146 | time.sleep(wait_time) 147 | 148 | try_no += 1 149 | 150 | def _reconnect_with_device_by_probing(self): 151 | """Attempt to reconnect to the device on the existing port.""" 152 | port = probe_device(self) 153 | 154 | if port is None: 155 | LOG.error('Unable to re-probe wemo at %s', self.host) 156 | return False 157 | 158 | LOG.info('Reconnected to wemo at %s on port %i', 159 | self.host, port) 160 | 161 | self.port = port 162 | url = 'http://{}:{}/setup.xml'.format(self.host, self.port) 163 | 164 | # pylint: disable=attribute-defined-outside-init 165 | self.__dict__ = self.__class__(url, None).__dict__ 166 | 167 | return True 168 | 169 | def reconnect_with_device(self): 170 | """Re-probe & scan network to rediscover a disconnected device.""" 171 | if self.rediscovery_enabled: 172 | if (not self._reconnect_with_device_by_probing() and 173 | (self.mac or self.serialnumber)): 174 | self._reconnect_with_device_by_discovery() 175 | else: 176 | LOG.warning("Rediscovery was requested for device %s, " 177 | "but rediscovery is disabled. Ignoring request.", 178 | self.name) 179 | 180 | def parse_basic_state(self, params): 181 | """Parse the basic state response from the device.""" 182 | # BinaryState 183 | # 1|1492338954|0|922|14195|1209600|0|940670|15213709|227088884 184 | ( 185 | state, # 0 if off, 1 if on, 186 | _x1, 187 | _x2, 188 | _x3, 189 | _x4, 190 | _x5, 191 | _x6, 192 | _x7, 193 | _x8, 194 | _x9 195 | ) = params.split('|') 196 | 197 | return {'state': state} 198 | 199 | def update_binary_state(self): 200 | """Update the cached copy of the basic state response.""" 201 | # pylint: disable=maybe-no-member 202 | self.basic_state_params = self.basicevent.GetBinaryState() 203 | 204 | def subscription_update(self, _type, _params): 205 | """Update device state based on subscription event.""" 206 | LOG.debug("subscription_update %s %s", _type, _params) 207 | if _type == "BinaryState": 208 | try: 209 | self._state = int(self.parse_basic_state(_params).get("state")) 210 | except ValueError: 211 | self._state = 0 212 | return True 213 | return False 214 | 215 | def get_state(self, force_update=False): 216 | """Return 0 if off and 1 if on.""" 217 | if force_update or self._state is None: 218 | # pylint: disable=maybe-no-member 219 | state = self.basicevent.GetBinaryState() or {} 220 | 221 | try: 222 | self._state = int(state.get('BinaryState', 0)) 223 | except ValueError: 224 | self._state = 0 225 | 226 | return self._state 227 | 228 | def get_service(self, name): 229 | """Get service object by name.""" 230 | try: 231 | return self.services[name] 232 | except KeyError: 233 | raise UnknownService(name) 234 | 235 | def list_services(self): 236 | """Return list of services.""" 237 | return list(self.services.keys()) 238 | 239 | def explain(self): 240 | """Print information about the device and its actions.""" 241 | for name, svc in self.services.items(): 242 | print(name) 243 | print('-' * len(name)) 244 | for aname, action in svc.actions.items(): 245 | print(" %s(%s)" % (aname, ', '.join(action.args))) 246 | print() 247 | 248 | @property 249 | def model(self): 250 | """Return the model description of the device.""" 251 | return self._config.get_modelDescription() 252 | 253 | @property 254 | def model_name(self): 255 | """Return the model name of the device.""" 256 | return self._config.get_modelName() 257 | 258 | @property 259 | def name(self): 260 | """Return the name of the device.""" 261 | return self._config.get_friendlyName() 262 | 263 | @property 264 | def serialnumber(self): 265 | """Return the serial number of the device.""" 266 | return self._config.get_serialNumber() 267 | -------------------------------------------------------------------------------- /pywemo/ouimeaux_device/humidifier.py: -------------------------------------------------------------------------------- 1 | """Representation of a WeMo Humidifier device.""" 2 | 3 | from xml.etree import cElementTree as et 4 | import sys 5 | from pywemo.ouimeaux_device.api.xsd.device import quote_xml 6 | from .switch import Switch 7 | 8 | 9 | if sys.version_info[0] < 3: 10 | class IntEnum: 11 | """Enum class.""" 12 | 13 | pass 14 | else: 15 | from enum import IntEnum 16 | 17 | 18 | # These enums were derived from the 19 | # Humidifier.deviceevent.GetAttributeList() 20 | # service call. 21 | # Thus these names/values were not chosen randomly 22 | # and the numbers have meaning. 23 | class FanMode(IntEnum): 24 | """Enum to map WeMo FanModes to human-readable strings.""" 25 | 26 | Off = 0 # Fan and device turned off 27 | Minimum = 1 28 | Low = 2 29 | Medium = 3 30 | High = 4 31 | Maximum = 5 32 | 33 | 34 | FAN_MODE_NAMES = { 35 | FanMode.Off: "Off", 36 | FanMode.Minimum: "Minimum", 37 | FanMode.Low: "Low", 38 | FanMode.Medium: "Medium", 39 | FanMode.High: "High", 40 | FanMode.Maximum: "Maximum" 41 | } 42 | 43 | 44 | class DesiredHumidity(IntEnum): 45 | """Enum to map WeMo DesiredHumidity to human-readable strings.""" 46 | 47 | FortyFivePercent = 0 48 | FiftyPercent = 1 49 | FiftyFivePercent = 2 50 | SixtyPercent = 3 51 | OneHundredPercent = 4 # "Always On" Mode 52 | 53 | 54 | DESIRED_HUMIDITY_NAMES = { 55 | DesiredHumidity.FortyFivePercent: "45", 56 | DesiredHumidity.FiftyPercent: "50", 57 | DesiredHumidity.FiftyFivePercent: "55", 58 | DesiredHumidity.SixtyPercent: "60", 59 | DesiredHumidity.OneHundredPercent: "100" 60 | } 61 | 62 | 63 | class WaterLevel(IntEnum): 64 | """Enum to map WeMo WaterLevel to human-readable strings.""" 65 | 66 | Empty = 0 67 | Low = 1 68 | Good = 2 69 | 70 | 71 | WATER_LEVEL_NAMES = { 72 | WaterLevel.Empty: "Empty", 73 | WaterLevel.Low: "Low", 74 | WaterLevel.Good: "Good", 75 | } 76 | 77 | FILTER_LIFE_MAX = 60480 78 | 79 | 80 | def attribute_xml_to_dict(xml_blob): 81 | """Return attribute values as a dict of key value pairs.""" 82 | xml_blob = "" + xml_blob + "" 83 | xml_blob = xml_blob.replace(">", ">") 84 | xml_blob = xml_blob.replace("<", "<") 85 | 86 | result = {} 87 | 88 | attributes = et.fromstring(xml_blob) 89 | 90 | result["water_level"] = int(2) 91 | 92 | for attribute in attributes: 93 | if attribute[0].text == "FanMode": 94 | try: 95 | result["fan_mode"] = int(attribute[1].text) 96 | except ValueError: 97 | pass 98 | elif attribute[0].text == "DesiredHumidity": 99 | try: 100 | result["desired_humidity"] = int(attribute[1].text) 101 | except ValueError: 102 | pass 103 | elif attribute[0].text == "CurrentHumidity": 104 | try: 105 | result["current_humidity"] = float(attribute[1].text) 106 | except ValueError: 107 | pass 108 | elif attribute[0].text == "NoWater" and attribute[1].text == "1": 109 | try: 110 | result["water_level"] = int(0) 111 | except ValueError: 112 | pass 113 | elif attribute[0].text == "WaterAdvise" and attribute[1].text == "1": 114 | try: 115 | result["water_level"] = int(1) 116 | except ValueError: 117 | pass 118 | elif attribute[0].text == "FilterLife": 119 | try: 120 | result["filter_life"] = float(round((float(attribute[1].text) 121 | / float(60480)) 122 | * float(100), 2)) 123 | except ValueError: 124 | pass 125 | elif attribute[0].text == "ExpiredFilterTime": 126 | try: 127 | result["filter_expired"] = bool(int(attribute[1].text)) 128 | except ValueError: 129 | pass 130 | 131 | return result 132 | 133 | 134 | class Humidifier(Switch): 135 | """Representation of a WeMo Humidifier device.""" 136 | 137 | def __init__(self, *args, **kwargs): 138 | """Create a WeMo Humidifier device.""" 139 | Switch.__init__(self, *args, **kwargs) 140 | self._attributes = {} 141 | self.update_attributes() 142 | 143 | def __repr__(self): 144 | """Return a string representation of the device.""" 145 | return ''.format(name=self.name) 146 | 147 | def update_attributes(self): 148 | """Request state from device.""" 149 | # pylint: disable=maybe-no-member 150 | resp = self.deviceevent.GetAttributes().get('attributeList') 151 | self._attributes = attribute_xml_to_dict(resp) 152 | self._state = self.fan_mode 153 | 154 | def subscription_update(self, _type, _params): 155 | """Handle reports from device.""" 156 | if _type == "attributeList": 157 | self._attributes.update(attribute_xml_to_dict(_params)) 158 | self._state = self.fan_mode 159 | 160 | return True 161 | 162 | return Switch.subscription_update(self, _type, _params) 163 | 164 | @property 165 | def device_type(self): 166 | """Return what kind of WeMo this device is.""" 167 | return "Humidifier" 168 | 169 | @property 170 | def fan_mode(self): 171 | """Return the FanMode setting (as an int index of the IntEnum).""" 172 | return self._attributes.get('fan_mode') 173 | 174 | @property 175 | def fan_mode_string(self): 176 | """ 177 | Return the FanMode setting as a string. 178 | 179 | (Off, Low, Medium, High, Maximum). 180 | """ 181 | return FAN_MODE_NAMES.get(self.fan_mode, "Unknown") 182 | 183 | @property 184 | def desired_humidity(self): 185 | """Return the desired humidity (as an int index of the IntEnum).""" 186 | return self._attributes.get('desired_humidity') 187 | 188 | @property 189 | def desired_humidity_percent(self): 190 | """Return the desired humidity in percent (string).""" 191 | return DESIRED_HUMIDITY_NAMES.get(self.desired_humidity, "Unknown") 192 | 193 | @property 194 | def current_humidity_percent(self): 195 | """Return the observed relative humidity in percent (float).""" 196 | return self._attributes.get('current_humidity') 197 | 198 | @property 199 | def water_level(self): 200 | """Return 0 if water level is Empty, 1 if Low, and 2 if Good.""" 201 | return self._attributes.get('water_level') 202 | 203 | @property 204 | def water_level_string(self): 205 | """Return Empty, Low, or Good depending on the water level.""" 206 | return WATER_LEVEL_NAMES.get(self.water_level, "Unknown") 207 | 208 | @property 209 | def filter_life_percent(self): 210 | """Return the percentage (float) of filter life remaining.""" 211 | return self._attributes.get('filter_life') 212 | 213 | @property 214 | def filter_expired(self): 215 | """Return 0 if filter is OK, and 1 if it needs to be changed.""" 216 | return self._attributes.get('filter_expired') 217 | 218 | def get_state(self, force_update=False): 219 | """Return 0 if off and 1 if on.""" 220 | # The base implementation using GetBinaryState 221 | # doesn't work for Humidifier (always returns 0) 222 | # so use fan mode instead. 223 | if force_update or self._state is None: 224 | self.update_attributes() 225 | 226 | # Consider the Humidifier to be "on" if it's not off. 227 | return int(self._state != FanMode.Off) 228 | 229 | def set_state(self, state): 230 | """ 231 | Set the fan mode of this device (as int index of the FanMode IntEnum). 232 | 233 | Provided for compatibility with the Switch base class. 234 | """ 235 | self.set_fan_mode(state) 236 | 237 | def set_fan_mode(self, fan_mode): 238 | """ 239 | Set the fan mode of this device (as int index of the FanMode IntEnum). 240 | 241 | Provided for compatibility with the Switch base class. 242 | """ 243 | # Send the attribute list to the device 244 | # pylint: disable=maybe-no-member 245 | self.deviceevent.SetAttributes(attributeList=quote_xml( 246 | "FanMode" + 247 | str(int(fan_mode)) + "")) 248 | 249 | # Refresh the device state 250 | self.get_state(True) 251 | 252 | def set_humidity(self, desired_humidity): 253 | """Set the desired humidity (as int index of the IntEnum).""" 254 | # Send the attribute list to the device 255 | # pylint: disable=maybe-no-member 256 | self.deviceevent.SetAttributes(attributeList=quote_xml( 257 | "DesiredHumidity" + 258 | str(int(desired_humidity)) + "")) 259 | 260 | # Refresh the device state 261 | self.get_state(True) 262 | 263 | def set_fan_mode_and_humidity(self, fan_mode, desired_humidity): 264 | """ 265 | Set the desired humidity and fan mode. 266 | 267 | (as int index of their respective IntEnums) 268 | """ 269 | # Send the attribute list to the device 270 | # pylint: disable=maybe-no-member 271 | self.deviceevent.SetAttributes(attributeList=quote_xml( 272 | "FanMode" + 273 | str(int(fan_mode)) + "" + 274 | "DesiredHumidity" + 275 | str(int(desired_humidity)) + "")) 276 | 277 | # Refresh the device state 278 | self.get_state(True) 279 | 280 | def reset_filter_life(self): 281 | """Reset the filter life (call this when you install a new filter).""" 282 | # Send the attribute list to the device 283 | # pylint: disable=maybe-no-member 284 | self.deviceevent.SetAttributes(attributeList=quote_xml( 285 | "FilterLife" + 286 | str(FILTER_LIFE_MAX) + "")) 287 | 288 | # Refresh the device state 289 | self.get_state(True) 290 | -------------------------------------------------------------------------------- /pywemo/subscribe.py: -------------------------------------------------------------------------------- 1 | """Module to listen for wemo events.""" 2 | import collections 3 | import logging 4 | import sched 5 | import socket 6 | import time 7 | import threading 8 | 9 | from xml.etree import cElementTree 10 | 11 | try: 12 | import BaseHTTPServer 13 | except ImportError: 14 | import http.server as BaseHTTPServer 15 | 16 | import requests 17 | 18 | LOG = logging.getLogger(__name__) 19 | NS = "{urn:schemas-upnp-org:event-1-0}" 20 | SUCCESS = '

200 OK

' 21 | SUBSCRIPTION_RETRY = 60 22 | 23 | 24 | class SubscriptionRegistryFailed(Exception): 25 | """General exceptions related to the subscription registry.""" 26 | 27 | pass 28 | 29 | 30 | def get_ip_address(host='1.2.3.4'): 31 | """Return IP from hostname or IP.""" 32 | sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 33 | try: 34 | sock.connect((host, 9)) 35 | return sock.getsockname()[0] 36 | except socket.error: 37 | return None 38 | finally: 39 | del sock 40 | 41 | 42 | class RequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): 43 | """Handles subscription responses received from devices.""" 44 | 45 | # pylint: disable=invalid-name 46 | def do_NOTIFY(self): 47 | """Handle subscription responses received from devices.""" 48 | sender_ip, _ = self.client_address 49 | outer = self.server.outer 50 | device = outer.devices.get(sender_ip) 51 | content_len = int(self.headers.get('content-length', 0)) 52 | data = self.rfile.read(content_len) 53 | if device is None: 54 | LOG.warning('Received event for unregistered device %s', sender_ip) 55 | else: 56 | # trim garbage from end, if any 57 | data = data.decode("UTF-8").split("\n\n")[0] 58 | doc = cElementTree.fromstring(data) 59 | for propnode in doc.findall('./{0}property'.format(NS)): 60 | for property_ in propnode.getchildren(): 61 | text = property_.text 62 | outer.event(device, property_.tag, text) 63 | 64 | self.send_response(200) 65 | self.send_header('Content-Type', 'text/html') 66 | self.send_header('Content-Length', len(SUCCESS)) 67 | self.send_header('Connection', 'close') 68 | self.end_headers() 69 | self.wfile.write(SUCCESS.encode("UTF-8")) 70 | 71 | # pylint: disable=redefined-builtin 72 | def log_message(self, format, *args): 73 | """Disable error logging.""" 74 | return 75 | 76 | 77 | class SubscriptionRegistry: 78 | """Class for subscribing to wemo events.""" 79 | 80 | def __init__(self): 81 | """Create the subscription registry object.""" 82 | self.devices = {} 83 | self._callbacks = collections.defaultdict(list) 84 | self._exiting = False 85 | 86 | self._event_thread = None 87 | self._event_thread_cond = threading.Condition() 88 | self._events = {} 89 | 90 | def sleep(secs): 91 | with self._event_thread_cond: 92 | self._event_thread_cond.wait(secs) 93 | self._sched = sched.scheduler(time.time, sleep) 94 | 95 | self._http_thread = None 96 | self._httpd = None 97 | self._port = None 98 | 99 | def register(self, device): 100 | """Register a device for subscription updates.""" 101 | if not device: 102 | LOG.error("Called with an invalid device: %r", device) 103 | return 104 | 105 | LOG.info("Subscribing to events from %r", device) 106 | self.devices[device.host] = device 107 | 108 | with self._event_thread_cond: 109 | self._events[device.serialnumber] = ( 110 | self._sched.enter(0, 0, self._resubscribe, [device])) 111 | self._event_thread_cond.notify() 112 | 113 | def unregister(self, device): 114 | """Unregister a device from subscription updates.""" 115 | if not device: 116 | LOG.error("Called with an invalid device: %r", device) 117 | return 118 | 119 | LOG.info("Unsubscribing to events from %r", device) 120 | 121 | with self._event_thread_cond: 122 | # Remove any events, callbacks, and the device itself 123 | if self._callbacks[device.serialnumber] is not None: 124 | del self._callbacks[device.serialnumber] 125 | if self._events[device.serialnumber] is not None: 126 | del self._events[device.serialnumber] 127 | if self.devices[device.host] is not None: 128 | del self.devices[device.host] 129 | 130 | self._event_thread_cond.notify() 131 | 132 | def _resubscribe(self, device, sid=None, retry=0): 133 | LOG.info("Resubscribe for %s", device) 134 | headers = {'TIMEOUT': '300'} 135 | if sid is not None: 136 | headers['SID'] = sid 137 | else: 138 | host = get_ip_address(host=device.host) 139 | headers.update({ 140 | "CALLBACK": '' % (host, self._port), 141 | "NT": "upnp:event" 142 | }) 143 | try: 144 | # Basic events 145 | self._url_resubscribe(device, headers, sid, 146 | device.basicevent.eventSubURL) 147 | # Insight events 148 | # if hasattr(device, 'insight'): 149 | # self._url_resubscribe( 150 | # device, headers, sid, device.insight.eventSubURL) 151 | 152 | except requests.exceptions.RequestException as ex: 153 | LOG.warning( 154 | "Resubscribe error for %s (%s), will retry in %ss", 155 | device, ex, SUBSCRIPTION_RETRY) 156 | retry += 1 157 | if retry > 1: 158 | # If this wasn't a one-off, try rediscovery 159 | # in case the device has changed. 160 | if device.rediscovery_enabled: 161 | device.reconnect_with_device() 162 | with self._event_thread_cond: 163 | self._events[device.serialnumber] = ( 164 | self._sched.enter(SUBSCRIPTION_RETRY, 165 | 0, self._resubscribe, 166 | [device, sid, retry])) 167 | 168 | def _url_resubscribe(self, device, headers, sid, url): 169 | request_headers = headers.copy() 170 | response = requests.request(method="SUBSCRIBE", url=url, 171 | headers=request_headers) 172 | if response.status_code == 412 and sid: 173 | # Invalid subscription ID. Send an UNSUBSCRIBE for safety and 174 | # start over. 175 | requests.request( 176 | method='UNSUBSCRIBE', url=url, headers={'SID': sid}) 177 | return self._resubscribe(device) 178 | timeout = int(response.headers.get('timeout', '1801').replace( 179 | 'Second-', '')) 180 | sid = response.headers.get('sid', sid) 181 | with self._event_thread_cond: 182 | self._events[device.serialnumber] = ( 183 | self._sched.enter(int(timeout * 0.75), 184 | 0, self._resubscribe, [device, sid])) 185 | 186 | def event(self, device, type_, value): 187 | """Execute the callback for a received event.""" 188 | LOG.info("Received event from %s(%s) - %s %s", 189 | device, device.host, type_, value) 190 | for type_filter, callback in self._callbacks.get( 191 | device.serialnumber, ()): 192 | if type_filter is None or type_ == type_filter: 193 | callback(device, type_, value) 194 | 195 | # pylint: disable=invalid-name 196 | def on(self, device, type_filter, callback): 197 | """Add an event callback for a device.""" 198 | self._callbacks[device.serialnumber].append((type_filter, callback)) 199 | 200 | def _find_port(self): 201 | """Find a valid open port to run the HTTP server on.""" 202 | for i in range(0, 128): 203 | port = 8989 + i 204 | try: 205 | self._httpd = BaseHTTPServer.HTTPServer( 206 | ('', port), RequestHandler) 207 | self._port = port 208 | break 209 | except (OSError, socket.error): 210 | continue 211 | 212 | def start(self): 213 | """Start the subscription registry.""" 214 | self._port = None 215 | self._find_port() 216 | if self._port is None: 217 | raise SubscriptionRegistryFailed( 218 | 'Unable to bind a port for listening') 219 | self._http_thread = threading.Thread(target=self._run_http_server, 220 | name='Wemo HTTP Thread') 221 | self._http_thread.deamon = True 222 | self._http_thread.start() 223 | 224 | self._event_thread = threading.Thread(target=self._run_event_loop, 225 | name='Wemo Events Thread') 226 | self._event_thread.deamon = True 227 | self._event_thread.start() 228 | 229 | def stop(self): 230 | """Shutdown the HTTP server.""" 231 | self._httpd.shutdown() 232 | 233 | with self._event_thread_cond: 234 | self._exiting = True 235 | 236 | # Remove any pending events 237 | for event in self._events.values(): 238 | try: 239 | self._sched.cancel(event) 240 | except ValueError: 241 | # event might execute and be removed from queue 242 | # concurrently. Safe to ignore 243 | pass 244 | 245 | # Wake up event thread if its sleeping 246 | self._event_thread_cond.notify() 247 | self.join() 248 | LOG.info( 249 | "Terminated threads") 250 | 251 | def join(self): 252 | """Block until the HTTP server and event threads have terminated.""" 253 | self._http_thread.join() 254 | self._event_thread.join() 255 | 256 | def _run_http_server(self): 257 | """Start the HTTP server.""" 258 | self._httpd.allow_reuse_address = True 259 | self._httpd.outer = self 260 | LOG.info("Listening on port %d", self._port) 261 | self._httpd.serve_forever() 262 | 263 | def _run_event_loop(self): 264 | """Run the event thread loop.""" 265 | while not self._exiting: 266 | with self._event_thread_cond: 267 | while not self._exiting and self._sched.empty(): 268 | self._event_thread_cond.wait(10) 269 | self._sched.run() 270 | -------------------------------------------------------------------------------- /pywemo/ssdp.py: -------------------------------------------------------------------------------- 1 | """Module that implements SSDP protocol.""" 2 | import logging 3 | import re 4 | import select 5 | import socket 6 | import threading 7 | import time 8 | 9 | from datetime import datetime, timedelta 10 | import xml.etree.ElementTree as XMLElementTree 11 | import requests 12 | 13 | from .util import etree_to_dict, interface_addresses 14 | 15 | DISCOVER_TIMEOUT = 5 16 | 17 | RESPONSE_REGEX = re.compile(r'\n(.*)\: (.*)\r') 18 | 19 | MIN_TIME_BETWEEN_SCANS = timedelta(seconds=59) 20 | 21 | # Wemo specific urn: 22 | ST = "urn:Belkin:service:basicevent:1" 23 | 24 | 25 | class SSDP: 26 | """Controls the scanning of uPnP devices and services and caches output.""" 27 | 28 | def __init__(self): 29 | """Create SSDP object.""" 30 | self.entries = [] 31 | self.last_scan = None 32 | self._lock = threading.RLock() 33 | 34 | def scan(self): 35 | """Scan the network.""" 36 | with self._lock: 37 | self.update() 38 | 39 | def all(self): 40 | """ 41 | Return all found entries. 42 | 43 | Will scan for entries if not scanned recently. 44 | """ 45 | with self._lock: 46 | self.update() 47 | 48 | return list(self.entries) 49 | 50 | # pylint: disable=invalid-name 51 | def find_by_st(self, st): 52 | """Return a list of entries that match the ST.""" 53 | with self._lock: 54 | self.update() 55 | 56 | return [entry for entry in self.entries 57 | if entry.st == st] 58 | 59 | def find_by_device_description(self, values): 60 | """ 61 | Return a list of entries that match the description. 62 | 63 | Pass in a dict with values to match against the device tag in the 64 | description. 65 | """ 66 | with self._lock: 67 | self.update() 68 | 69 | return [entry for entry in self.entries 70 | if entry.match_device_description(values)] 71 | 72 | def update(self, force_update=False): 73 | """Scan for new uPnP devices and services.""" 74 | with self._lock: 75 | if self.last_scan is None or force_update or \ 76 | datetime.now()-self.last_scan > MIN_TIME_BETWEEN_SCANS: 77 | 78 | self.remove_expired() 79 | 80 | self.entries.extend( 81 | entry for entry in scan() + scan(ST) 82 | if entry not in self.entries) 83 | 84 | self.last_scan = datetime.now() 85 | 86 | def remove_expired(self): 87 | """Filter out expired entries.""" 88 | with self._lock: 89 | self.entries = [entry for entry in self.entries 90 | if not entry.is_expired] 91 | 92 | 93 | class UPNPEntry: 94 | """Found uPnP entry.""" 95 | 96 | DESCRIPTION_CACHE = {'_NO_LOCATION': {}} 97 | 98 | def __init__(self, values): 99 | """Create a UPNPEntry object.""" 100 | self.values = values 101 | self.created = datetime.now() 102 | 103 | if 'cache-control' in self.values: 104 | cache_seconds = int(self.values['cache-control'].split('=')[1]) 105 | 106 | self.expires = self.created + timedelta(seconds=cache_seconds) 107 | else: 108 | self.expires = None 109 | 110 | @property 111 | def is_expired(self): 112 | """Return whether the entry is expired or not.""" 113 | return self.expires is not None and datetime.now() > self.expires 114 | 115 | # pylint: disable=invalid-name 116 | @property 117 | def st(self): 118 | """Return ST value.""" 119 | return self.values.get('st') 120 | 121 | @property 122 | def location(self): 123 | """Return location value.""" 124 | return self.values.get('location') 125 | 126 | @property 127 | def description(self): 128 | """Return the description from the uPnP entry.""" 129 | url = self.values.get('location', '_NO_LOCATION') 130 | 131 | if url not in UPNPEntry.DESCRIPTION_CACHE: 132 | try: 133 | xml = requests.get(url, timeout=10).text 134 | 135 | tree = None 136 | if xml is not None: 137 | tree = XMLElementTree.fromstring(xml) 138 | 139 | if tree is not None: 140 | UPNPEntry.DESCRIPTION_CACHE[url] = \ 141 | etree_to_dict(tree).get('root', {}) 142 | else: 143 | UPNPEntry.DESCRIPTION_CACHE[url] = {} 144 | 145 | except requests.RequestException: 146 | logging.getLogger(__name__).warning( 147 | "Error fetching description at %s", url) 148 | 149 | UPNPEntry.DESCRIPTION_CACHE[url] = {} 150 | 151 | except XMLElementTree.ParseError: 152 | # There used to be a log message here to record an error about 153 | # malformed XML, but this only happens on non-WeMo devices 154 | # and can be safely ignored. 155 | UPNPEntry.DESCRIPTION_CACHE[url] = {} 156 | 157 | return UPNPEntry.DESCRIPTION_CACHE[url] 158 | 159 | def match_device_description(self, values): 160 | """ 161 | Fetch description and match against it. 162 | 163 | Values should only contain lowercase keys. 164 | """ 165 | if self.description is None: 166 | return False 167 | 168 | device = self.description.get('device') 169 | 170 | if device is None: 171 | return False 172 | 173 | return all(val == device.get(key) 174 | for key, val in values.items()) 175 | 176 | @classmethod 177 | def from_response(cls, response): 178 | """Create a uPnP entry from a response.""" 179 | return UPNPEntry({key.lower(): item for key, item 180 | in RESPONSE_REGEX.findall(response)}) 181 | 182 | def __eq__(self, other): 183 | """Equality operator.""" 184 | return (self.__class__ == other.__class__ and 185 | self.values == other.values) 186 | 187 | def __repr__(self): 188 | """Return the string representation of the object.""" 189 | return "".format( 190 | self.values.get('st', ''), self.values.get('location', '')) 191 | 192 | 193 | def build_ssdp_request(ssdp_st, ssdp_mx): 194 | """Build the standard request to send during SSDP discovery.""" 195 | ssdp_st = ssdp_st or ST 196 | return "\r\n".join([ 197 | 'M-SEARCH * HTTP/1.1', 198 | 'ST: {}'.format(ssdp_st), 199 | 'MX: {:d}'.format(ssdp_mx), 200 | 'MAN: "ssdp:discover"', 201 | 'HOST: 239.255.255.250:1900', 202 | '', '']).encode('ascii') 203 | 204 | 205 | def entry_in_entries(entry, entries, mac, serial): 206 | """Check if a device entry is in a list of device entries.""" 207 | # If we don't have a mac or serial, let's just compare objects instead: 208 | if mac is None and serial is None: 209 | return entry in entries 210 | 211 | for item in entries: 212 | if item.description is not None: 213 | e_device = item.description.get('device', {}) 214 | e_mac = e_device.get('macAddress') 215 | e_serial = e_device.get('serialNumber') 216 | else: 217 | e_mac = None 218 | e_serial = None 219 | 220 | if e_mac == mac and e_serial == serial and item.st == entry.st: 221 | return True 222 | 223 | return False 224 | 225 | 226 | # pylint: disable=invalid-name,too-many-nested-blocks 227 | def scan(st=None, timeout=DISCOVER_TIMEOUT, 228 | max_entries=None, match_mac=None, match_serial=None): 229 | """ 230 | Send a message over the network to discover upnp devices. 231 | 232 | Inspired by Crimsdings 233 | https://github.com/crimsdings/ChromeCast/blob/master/cc_discovery.py 234 | """ 235 | ssdp_target = ("239.255.255.250", 1900) 236 | 237 | entries = [] 238 | 239 | calc_now = datetime.now 240 | start = calc_now() 241 | 242 | sockets = [] 243 | try: 244 | for addr in interface_addresses(): 245 | try: 246 | s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 247 | sockets.append(s) 248 | s.bind((addr, 0)) 249 | 250 | # Send 2 separate ssdp requests to mimic wemo app behavior: 251 | ssdp_request = build_ssdp_request(st, ssdp_mx=1) 252 | s.sendto(ssdp_request, ssdp_target) 253 | 254 | time.sleep(0.5) 255 | 256 | ssdp_request = build_ssdp_request(st, ssdp_mx=2) 257 | s.sendto(ssdp_request, ssdp_target) 258 | 259 | s.setblocking(0) 260 | except socket.error: 261 | pass 262 | 263 | while sockets: 264 | time_diff = calc_now() - start 265 | 266 | # pylint: disable=maybe-no-member 267 | seconds_left = timeout - time_diff.seconds 268 | 269 | if seconds_left <= 0: 270 | return entries 271 | 272 | ready = select.select(sockets, [], [], seconds_left)[0] 273 | 274 | for sock in ready: 275 | response = sock.recv(1024).decode("UTF-8", "replace") 276 | 277 | entry = UPNPEntry.from_response(response) 278 | if entry.description is not None: 279 | device = entry.description.get('device', {}) 280 | mac = device.get('macAddress') 281 | serial = device.get('serialNumber') 282 | else: 283 | mac = None 284 | serial = None 285 | 286 | # Search for devices 287 | if (st is not None or 288 | match_mac is not None or 289 | match_serial is not None): 290 | if not entry_in_entries(entry, entries, mac, serial): 291 | if match_mac is not None: 292 | if match_mac == mac: 293 | entries.append(entry) 294 | elif match_serial is not None: 295 | if match_serial == serial: 296 | entries.append(entry) 297 | elif st is not None: 298 | if st == entry.st: 299 | entries.append(entry) 300 | elif not entry_in_entries(entry, entries, mac, serial): 301 | entries.append(entry) 302 | 303 | # Return if we've found the max number of devices 304 | if max_entries: 305 | if len(entries) == max_entries: 306 | return entries 307 | except socket.error: 308 | logging.getLogger(__name__).exception( 309 | "Socket error while discovering SSDP devices") 310 | finally: 311 | for s in sockets: 312 | s.close() 313 | 314 | return entries 315 | 316 | 317 | if __name__ == "__main__": 318 | from pprint import pprint 319 | 320 | pprint("Scanning UPNP..") 321 | pprint(scan()) 322 | -------------------------------------------------------------------------------- /pywemo/ouimeaux_device/bridge.py: -------------------------------------------------------------------------------- 1 | """Representation of a WeMo Bridge (Link) device.""" 2 | import time 3 | from xml.etree import cElementTree as et 4 | import six 5 | 6 | six.add_move(six.MovedAttribute('html_escape', 'cgi', 'html', 'escape')) 7 | 8 | # pylint: disable=wrong-import-position 9 | from six.moves import html_escape # noqa E402 10 | from . import Device # noqa E402 11 | from ..color import get_profiles, limit_to_gamut # noqa E402 12 | 13 | 14 | CAPABILITY_ID2NAME = dict(( 15 | ('10006', "onoff"), 16 | ('10008', "levelcontrol"), 17 | ('30008', "sleepfader"), 18 | ('30009', "levelcontrol_move"), 19 | ('3000A', "levelcontrol_stop"), 20 | ('10300', "colorcontrol"), 21 | ('30301', "colortemperature"), 22 | )) 23 | CAPABILITY_NAME2ID = dict( 24 | (val, cap) for cap, val in CAPABILITY_ID2NAME.items()) 25 | 26 | # acceptable values for 'onoff' 27 | OFF = 0 28 | ON = 1 29 | TOGGLE = 2 30 | 31 | 32 | def limit(value, min_val, max_val): 33 | """Return a value clipped to the range [min_val, max_val].""" 34 | return max(min_val, min(value, max_val)) 35 | 36 | 37 | class Bridge(Device): 38 | """Representation of a WeMo Bridge (Link) device.""" 39 | 40 | Lights = {} 41 | Groups = {} 42 | 43 | def __init__(self, *args, **kwargs): 44 | """Create a WeMo Bridge (Link) device.""" 45 | super(Bridge, self).__init__(*args, **kwargs) 46 | self.bridge_update() 47 | 48 | def __repr__(self): 49 | """Return a string representation of the device.""" 50 | return ('').format( 52 | name=self.name, lights=len(self.Lights), 53 | groups=len(self.Groups)) 54 | 55 | def bridge_update(self, force_update=True): 56 | """Get updated status information for the bridge and its lights.""" 57 | # pylint: disable=maybe-no-member 58 | if force_update or self.Lights is None or self.Groups is None: 59 | plugin_udn = self.basicevent.GetMacAddr().get('PluginUDN') 60 | 61 | if hasattr(self.bridge, 'GetEndDevicesWithStatus'): 62 | end_devices = self.bridge.GetEndDevicesWithStatus( 63 | DevUDN=plugin_udn, ReqListType='PAIRED_LIST') 64 | else: 65 | end_devices = self.bridge.GetEndDevices( 66 | DevUDN=plugin_udn, ReqListType='PAIRED_LIST') 67 | 68 | end_device_list = et.fromstring(end_devices.get('DeviceLists')) 69 | 70 | for light in end_device_list.iter('DeviceInfo'): 71 | # pylint: disable=invalid-name 72 | uniqueID = light.find('DeviceID').text 73 | if uniqueID in self.Lights: 74 | self.Lights[uniqueID].update_state(light) 75 | else: 76 | self.Lights[uniqueID] = Light(self, light) 77 | 78 | for group in end_device_list.iter('GroupInfo'): 79 | # pylint: disable=invalid-name 80 | uniqueID = group.find('GroupID').text 81 | if uniqueID in self.Groups: 82 | self.Groups[uniqueID].update_state(group) 83 | else: 84 | self.Groups[uniqueID] = Group(self, group) 85 | 86 | return self.Lights, self.Groups 87 | 88 | def bridge_getdevicestatus(self, deviceid): 89 | """Return the list of device statuses for the bridge's lights.""" 90 | # pylint: disable=maybe-no-member 91 | status_list = self.bridge.GetDeviceStatus(DeviceIDs=deviceid) 92 | device_status_list = et.fromstring(status_list.get('DeviceStatusList')) 93 | 94 | return device_status_list.find('DeviceStatus') 95 | 96 | def bridge_setdevicestatus(self, isgroup, deviceid, capids, values): 97 | """Set the status of the bridge's lights.""" 98 | req = et.Element('DeviceStatus') 99 | et.SubElement(req, 'IsGroupAction').text = isgroup 100 | et.SubElement(req, 'DeviceID', available="YES").text = deviceid 101 | et.SubElement(req, 'CapabilityID').text = ','.join(capids) 102 | et.SubElement(req, 'CapabilityValue').text = ','.join(values) 103 | 104 | buf = six.BytesIO() 105 | et.ElementTree(req).write(buf, encoding='utf-8', 106 | xml_declaration=True) 107 | send_state = html_escape(buf.getvalue().decode(), quote=True) 108 | 109 | # pylint: disable=maybe-no-member 110 | return self.bridge.SetDeviceStatus(DeviceStatusList=send_state) 111 | 112 | @property 113 | def device_type(self): 114 | """Return what kind of WeMo this device is.""" 115 | return "Bridge" 116 | 117 | 118 | class LinkedDevice: 119 | """Representation of a device connected to the bridge.""" 120 | 121 | def __init__(self, bridge, info): 122 | """Create a Linked Device.""" 123 | self.bridge = bridge 124 | self.host = self.bridge.host 125 | self.port = self.bridge.port 126 | self.state = {} 127 | self.capabilities = [] 128 | self._values = [] 129 | self.update_state(info) 130 | self._last_err = None 131 | self.mac = self.bridge.mac 132 | self.serialnumber = self.bridge.serialnumber 133 | 134 | def get_state(self, force_update=False): 135 | """Return the status of the device.""" 136 | if force_update: 137 | self.bridge.bridge_update() 138 | return self.state 139 | 140 | def update_state(self, status): 141 | """ 142 | Set the device state based on capabilities and values. 143 | 144 | Subclasses should parse status into self.capabilities and 145 | self._values and then call this to populate self.state. 146 | """ 147 | status = {} 148 | for capability, value in zip(self.capabilities, self._values): 149 | if not value: 150 | value = None 151 | elif ':' in value: 152 | value = tuple(int(round(float(v))) for v in value.split(':')) 153 | else: 154 | value = int(round(float(value))) 155 | status[capability] = value 156 | 157 | # unreachable devices have empty strings for all capability values 158 | if status.get('onoff') is None: 159 | self.state['available'] = False 160 | self.state['onoff'] = 0 161 | return 162 | 163 | self.state['available'] = True 164 | self.state['onoff'] = status['onoff'] 165 | 166 | if status.get('levelcontrol') is not None: 167 | self.state['level'] = status['levelcontrol'][0] 168 | 169 | if status.get('colortemperature') is not None: 170 | temperature = status['colortemperature'][0] 171 | self.state['temperature_mireds'] = temperature 172 | self.state['temperature_kelvin'] = int(1000000 / temperature) 173 | 174 | if status.get('colorcontrol') is not None: 175 | colorx, colory = status['colorcontrol'][:2] 176 | colorx, colory = colorx / 65535., colory / 65535. 177 | self.state['color_xy'] = colorx, colory 178 | 179 | def _setdevicestatus(self, **kwargs): 180 | """Ask the bridge to set the device status.""" 181 | isgroup = 'YES' if isinstance(self, Group) else 'NO' 182 | 183 | capids = [] 184 | values = [] 185 | for cap, val in kwargs.items(): 186 | capids.append(CAPABILITY_NAME2ID[cap]) 187 | 188 | if not isinstance(val, (list, tuple)): 189 | val = (val,) 190 | values.append(':'.join(str(v) for v in val)) 191 | 192 | # pylint: disable=maybe-no-member 193 | self._last_err = self.bridge.bridge_setdevicestatus( 194 | isgroup, self.uniqueID, capids, values) 195 | return self 196 | 197 | def turn_on(self, level=None, transition=0, force_update=False): 198 | """Turn on the device.""" 199 | return self._setdevicestatus(onoff=ON) 200 | 201 | def turn_off(self, transition=0): 202 | """Turn off the device.""" 203 | return self._setdevicestatus(onoff=OFF) 204 | 205 | def toggle(self): 206 | """Toggle the device from on to off or off to on.""" 207 | return self._setdevicestatus(onoff=TOGGLE) 208 | 209 | @property 210 | def device_type(self): 211 | """Return what kind of WeMo this device is.""" 212 | return "LinkedDevice" 213 | 214 | 215 | class Light(LinkedDevice): 216 | """Representation of a Light connected to the Bridge.""" 217 | 218 | def __init__(self, bridge, info): 219 | """Create a Light device.""" 220 | super(Light, self).__init__(bridge, info) 221 | 222 | self.device_index = info.findtext('DeviceIndex') 223 | # pylint: disable=invalid-name 224 | self.uniqueID = info.findtext('DeviceID') 225 | self.iconvalue = info.findtext('IconVersion') 226 | self.firmware = info.findtext('FirmwareVersion') 227 | self.manufacturer = info.findtext('Manufacturer') 228 | self.model = info.findtext('ModelCode') 229 | self.certified = info.findtext('WeMoCertified') 230 | 231 | self.temperature_range, self.gamut = get_profiles(self.model) 232 | self._pending = {} 233 | 234 | def _queuedevicestatus(self, queue=False, **kwargs): 235 | """Queue an update to the device.""" 236 | if kwargs: 237 | self._pending.update(kwargs) 238 | if not queue: 239 | self._setdevicestatus(**self._pending) 240 | self._pending = {} 241 | 242 | return self 243 | 244 | def update_state(self, status): 245 | """Update the device state.""" 246 | if status.tag == 'DeviceInfo': 247 | self.name = status.findtext('FriendlyName') 248 | 249 | capabilities = (status.findtext('CapabilityIDs') or 250 | status.findtext('CapabilityID')) 251 | currentstate = (status.findtext('CurrentState') or 252 | status.findtext('CapabilityValue')) 253 | 254 | if capabilities is not None: 255 | self.capabilities = [ 256 | CAPABILITY_ID2NAME.get(c, c) 257 | for c in capabilities.split(',') 258 | ] 259 | if currentstate is not None: 260 | self._values = currentstate.split(',') 261 | 262 | super(Light, self).update_state(status) 263 | 264 | def __repr__(self): 265 | """Return a string representation of the device.""" 266 | return ''.format(name=self.name) 267 | 268 | def turn_on(self, level=None, transition=0, force_update=False): 269 | """Turn on the light.""" 270 | transition_time = limit(int(transition * 10), 0, 65535) 271 | 272 | if level == 0: 273 | return self.turn_off(transition) 274 | elif 'levelcontrol' in self.capabilities: 275 | # Work around observed fw bugs. 276 | # - When we set a new brightness level but the bulb is off, it 277 | # first turns on at the old brightness and then fades to the new 278 | # setting. So we have to force the saved brightness to 0 first. 279 | # - When we turn a bulb on with levelcontrol the onoff state 280 | # doesn't update. 281 | # - After turning off a bulb with sleepfader, it fails to turn back 282 | # on unless the brightness is re-set with levelcontrol. 283 | self.get_state(force_update=force_update) 284 | # A freshly power cycled bridge has no record of the bulb 285 | # brightness, so default to full on if the client didn't request 286 | # a level and we have no record 287 | if level is None: 288 | level = self.state.get("level", 255) 289 | 290 | if self.state['onoff'] == 0: 291 | self._setdevicestatus(levelcontrol=(0, 0), onoff=ON) 292 | 293 | level = limit(int(level), 0, 255) 294 | return self._queuedevicestatus(levelcontrol=(level, 295 | transition_time)) 296 | 297 | return self._queuedevicestatus(onoff=ON) 298 | 299 | def turn_off(self, transition=0): 300 | """Turn off the light.""" 301 | if transition and 'sleepfader' in self.capabilities: 302 | # Sleepfader control did not turn off bulb when fadetime was 0 303 | transition_time = limit(int(transition * 10), 1, 65535) 304 | reference = int(time.time()) 305 | return self._queuedevicestatus(sleepfader=(transition_time, 306 | reference)) 307 | 308 | return self._queuedevicestatus(onoff=OFF) 309 | 310 | def set_temperature(self, kelvin=2700, mireds=None, 311 | transition=0, delay=True): 312 | """Set the color temperature of the light.""" 313 | transition_time = limit(int(transition * 10), 0, 65535) 314 | if mireds is None: 315 | mireds = 1000000 / kelvin 316 | mireds = limit(int(mireds), *self.temperature_range) 317 | return self._queuedevicestatus( 318 | colortemperature=(mireds, transition_time), queue=delay) 319 | 320 | def set_color(self, colorxy, transition=0, delay=True): 321 | """Set the color of the light.""" 322 | transition_time = limit(int(transition * 10), 0, 65535) 323 | colorxy = limit_to_gamut(colorxy, self.gamut) 324 | colorx = limit(int(colorxy[0] * 65535), 0, 65535) 325 | colory = limit(int(colorxy[1] * 65535), 0, 65535) 326 | return self._queuedevicestatus( 327 | colorcontrol=(colorx, colory, transition_time), queue=delay) 328 | 329 | def start_ramp(self, ramp_up, rate): 330 | """Start ramping the brightness up or down.""" 331 | up_down = '1' if ramp_up else '0' 332 | rate = limit(int(rate), 0, 255) 333 | return self._queuedevicestatus(levelcontrol_move=(up_down, rate)) 334 | 335 | def stop_ramp(self): 336 | """Start ramping the brightness up or down.""" 337 | return self._setdevicestatus(levelcontrol_stop='') 338 | 339 | @property 340 | def device_type(self): 341 | """Return what kind of WeMo this device is.""" 342 | return "Light" 343 | 344 | 345 | class Group(LinkedDevice): 346 | """Representation of a Group of lights connected to the Bridge.""" 347 | 348 | def __init__(self, bridge, info): 349 | """Create a Group device.""" 350 | super(Group, self).__init__(bridge, info) 351 | # pylint: disable=invalid-name 352 | self.uniqueID = info.findtext('GroupID') 353 | 354 | def update_state(self, status): 355 | """Update the device state.""" 356 | if status.tag == 'GroupInfo': 357 | self.name = status.findtext('GroupName') 358 | 359 | capabilities = (status.findtext('GroupCapabilityIDs') or 360 | status.findtext('CapabilityID')) 361 | currentstate = (status.findtext('GroupCapabilityValues') or 362 | status.findtext('CapabilityValue')) 363 | 364 | if capabilities is not None: 365 | self.capabilities = [ 366 | CAPABILITY_ID2NAME.get(c, c) 367 | for c in capabilities.split(',') 368 | ] 369 | if currentstate is not None: 370 | self._values = currentstate.split(',') 371 | super(Group, self).update_state(status) 372 | 373 | def __repr__(self): 374 | """Return a string representation of the device.""" 375 | return ''.format(name=self.name) 376 | 377 | @property 378 | def device_type(self): 379 | """Return what kind of WeMo this device is.""" 380 | return "Group" 381 | -------------------------------------------------------------------------------- /pywemo/ouimeaux_device/api/xsd/service.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | # 5 | # Generated Thu Jan 31 15:52:45 2013 by generateDS.py version 2.8b. 6 | # 7 | 8 | import sys 9 | import getopt 10 | import re as re_ 11 | import base64 12 | from datetime import datetime, tzinfo, timedelta 13 | 14 | etree_ = None 15 | Verbose_import_ = False 16 | ( XMLParser_import_none, XMLParser_import_lxml, 17 | XMLParser_import_elementtree 18 | ) = list(range(3)) 19 | XMLParser_import_library = None 20 | try: 21 | # lxml 22 | from lxml import etree as etree_ 23 | XMLParser_import_library = XMLParser_import_lxml 24 | if Verbose_import_: 25 | print("running with lxml.etree") 26 | except ImportError: 27 | try: 28 | # cElementTree from Python 2.5+ 29 | import xml.etree.cElementTree as etree_ 30 | XMLParser_import_library = XMLParser_import_elementtree 31 | if Verbose_import_: 32 | print("running with cElementTree on Python 2.5+") 33 | except ImportError: 34 | try: 35 | # ElementTree from Python 2.5+ 36 | import xml.etree.ElementTree as etree_ 37 | XMLParser_import_library = XMLParser_import_elementtree 38 | if Verbose_import_: 39 | print("running with ElementTree on Python 2.5+") 40 | except ImportError: 41 | try: 42 | # normal cElementTree install 43 | import cElementTree as etree_ 44 | XMLParser_import_library = XMLParser_import_elementtree 45 | if Verbose_import_: 46 | print("running with cElementTree") 47 | except ImportError: 48 | try: 49 | # normal ElementTree install 50 | import elementtree.ElementTree as etree_ 51 | XMLParser_import_library = XMLParser_import_elementtree 52 | if Verbose_import_: 53 | print("running with ElementTree") 54 | except ImportError: 55 | raise ImportError( 56 | "Failed to import ElementTree from any known place") 57 | 58 | def parsexml_(*args, **kwargs): 59 | if (XMLParser_import_library == XMLParser_import_lxml and 60 | 'parser' not in kwargs): 61 | # Use the lxml ElementTree compatible parser so that, e.g., 62 | # we ignore comments. 63 | kwargs['parser'] = etree_.ETCompatXMLParser() 64 | doc = etree_.parse(*args, **kwargs) 65 | return doc 66 | 67 | # 68 | # User methods 69 | # 70 | # Calls to the methods in these classes are generated by generateDS.py. 71 | # You can replace these methods by re-implementing the following class 72 | # in a module named generatedssuper.py. 73 | 74 | try: 75 | from generatedssuper import GeneratedsSuper 76 | except ImportError as exp: 77 | 78 | class GeneratedsSuper(object): 79 | tzoff_pattern = re_.compile(r'(\+|-)((0\d|1[0-3]):[0-5]\d|14:00)$') 80 | class _FixedOffsetTZ(tzinfo): 81 | def __init__(self, offset, name): 82 | self.__offset = timedelta(minutes = offset) 83 | self.__name = name 84 | def utcoffset(self, dt): 85 | return self.__offset 86 | def tzname(self, dt): 87 | return self.__name 88 | def dst(self, dt): 89 | return None 90 | def gds_format_string(self, input_data, input_name=''): 91 | return input_data 92 | def gds_validate_string(self, input_data, node, input_name=''): 93 | return input_data 94 | def gds_format_base64(self, input_data, input_name=''): 95 | return base64.b64encode(input_data) 96 | def gds_validate_base64(self, input_data, node, input_name=''): 97 | return input_data 98 | def gds_format_integer(self, input_data, input_name=''): 99 | return '%d' % input_data 100 | def gds_validate_integer(self, input_data, node, input_name=''): 101 | return input_data 102 | def gds_format_integer_list(self, input_data, input_name=''): 103 | return '%s' % input_data 104 | def gds_validate_integer_list(self, input_data, node, input_name=''): 105 | values = input_data.split() 106 | for value in values: 107 | try: 108 | fvalue = float(value) 109 | except (TypeError, ValueError) as exp: 110 | raise_parse_error(node, 'Requires sequence of integers') 111 | return input_data 112 | def gds_format_float(self, input_data, input_name=''): 113 | return '%f' % input_data 114 | def gds_validate_float(self, input_data, node, input_name=''): 115 | return input_data 116 | def gds_format_float_list(self, input_data, input_name=''): 117 | return '%s' % input_data 118 | def gds_validate_float_list(self, input_data, node, input_name=''): 119 | values = input_data.split() 120 | for value in values: 121 | try: 122 | fvalue = float(value) 123 | except (TypeError, ValueError) as exp: 124 | raise_parse_error(node, 'Requires sequence of floats') 125 | return input_data 126 | def gds_format_double(self, input_data, input_name=''): 127 | return '%e' % input_data 128 | def gds_validate_double(self, input_data, node, input_name=''): 129 | return input_data 130 | def gds_format_double_list(self, input_data, input_name=''): 131 | return '%s' % input_data 132 | def gds_validate_double_list(self, input_data, node, input_name=''): 133 | values = input_data.split() 134 | for value in values: 135 | try: 136 | fvalue = float(value) 137 | except (TypeError, ValueError) as exp: 138 | raise_parse_error(node, 'Requires sequence of doubles') 139 | return input_data 140 | def gds_format_boolean(self, input_data, input_name=''): 141 | return '%s' % input_data 142 | def gds_validate_boolean(self, input_data, node, input_name=''): 143 | return input_data 144 | def gds_format_boolean_list(self, input_data, input_name=''): 145 | return '%s' % input_data 146 | def gds_validate_boolean_list(self, input_data, node, input_name=''): 147 | values = input_data.split() 148 | for value in values: 149 | if value not in ('true', '1', 'false', '0', ): 150 | raise_parse_error(node, 151 | 'Requires sequence of booleans ' 152 | '("true", "1", "false", "0")') 153 | return input_data 154 | def gds_validate_datetime(self, input_data, node, input_name=''): 155 | return input_data 156 | def gds_format_datetime(self, input_data, input_name=''): 157 | if input_data.microsecond == 0: 158 | _svalue = input_data.strftime('%Y-%m-%dT%H:%M:%S') 159 | else: 160 | _svalue = input_data.strftime('%Y-%m-%dT%H:%M:%S.%f') 161 | if input_data.tzinfo is not None: 162 | tzoff = input_data.tzinfo.utcoffset(input_data) 163 | if tzoff is not None: 164 | total_seconds = tzoff.seconds + (86400 * tzoff.days) 165 | if total_seconds == 0: 166 | _svalue += 'Z' 167 | else: 168 | if total_seconds < 0: 169 | _svalue += '-' 170 | total_seconds *= -1 171 | else: 172 | _svalue += '+' 173 | hours = total_seconds // 3600 174 | minutes = (total_seconds - (hours * 3600)) // 60 175 | _svalue += '{0:02d}:{1:02d}'.format(hours, minutes) 176 | return _svalue 177 | def gds_parse_datetime(self, input_data, node, input_name=''): 178 | tz = None 179 | if input_data[-1] == 'Z': 180 | tz = GeneratedsSuper._FixedOffsetTZ(0, 'GMT') 181 | input_data = input_data[:-1] 182 | else: 183 | results = GeneratedsSuper.tzoff_pattern.search(input_data) 184 | if results is not None: 185 | tzoff_parts = results.group(2).split(':') 186 | tzoff = int(tzoff_parts[0]) * 60 + int(tzoff_parts[1]) 187 | if results.group(1) == '-': 188 | tzoff *= -1 189 | tz = GeneratedsSuper._FixedOffsetTZ( 190 | tzoff, results.group(0)) 191 | input_data = input_data[:-6] 192 | if len(input_data.split('.')) > 1: 193 | dt = datetime.strptime( 194 | input_data, '%Y-%m-%dT%H:%M:%S.%f') 195 | else: 196 | dt = datetime.strptime( 197 | input_data, '%Y-%m-%dT%H:%M:%S') 198 | return dt.replace(tzinfo = tz) 199 | 200 | def gds_validate_date(self, input_data, node, input_name=''): 201 | return input_data 202 | def gds_format_date(self, input_data, input_name=''): 203 | _svalue = input_data.strftime('%Y-%m-%d') 204 | if input_data.tzinfo is not None: 205 | tzoff = input_data.tzinfo.utcoffset(input_data) 206 | if tzoff is not None: 207 | total_seconds = tzoff.seconds + (86400 * tzoff.days) 208 | if total_seconds == 0: 209 | _svalue += 'Z' 210 | else: 211 | if total_seconds < 0: 212 | _svalue += '-' 213 | total_seconds *= -1 214 | else: 215 | _svalue += '+' 216 | hours = total_seconds // 3600 217 | minutes = (total_seconds - (hours * 3600)) // 60 218 | _svalue += '{0:02d}:{1:02d}'.format(hours, minutes) 219 | return _svalue 220 | def gds_parse_date(self, input_data, node, input_name=''): 221 | tz = None 222 | if input_data[-1] == 'Z': 223 | tz = GeneratedsSuper._FixedOffsetTZ(0, 'GMT') 224 | input_data = input_data[:-1] 225 | else: 226 | results = GeneratedsSuper.tzoff_pattern.search(input_data) 227 | if results is not None: 228 | tzoff_parts = results.group(2).split(':') 229 | tzoff = int(tzoff_parts[0]) * 60 + int(tzoff_parts[1]) 230 | if results.group(1) == '-': 231 | tzoff *= -1 232 | tz = GeneratedsSuper._FixedOffsetTZ( 233 | tzoff, results.group(0)) 234 | input_data = input_data[:-6] 235 | return datetime.strptime(input_data, 236 | '%Y-%m-%d').replace(tzinfo = tz) 237 | def gds_str_lower(self, instring): 238 | return instring.lower() 239 | def get_path_(self, node): 240 | path_list = [] 241 | self.get_path_list_(node, path_list) 242 | path_list.reverse() 243 | path = '/'.join(path_list) 244 | return path 245 | Tag_strip_pattern_ = re_.compile(r'\{.*\}') 246 | def get_path_list_(self, node, path_list): 247 | if node is None: 248 | return 249 | tag = GeneratedsSuper.Tag_strip_pattern_.sub('', node.tag) 250 | if tag: 251 | path_list.append(tag) 252 | self.get_path_list_(node.getparent(), path_list) 253 | def get_class_obj_(self, node, default_class=None): 254 | class_obj1 = default_class 255 | if 'xsi' in node.nsmap: 256 | classname = node.get('{%s}type' % node.nsmap['xsi']) 257 | if classname is not None: 258 | names = classname.split(':') 259 | if len(names) == 2: 260 | classname = names[1] 261 | class_obj2 = globals().get(classname) 262 | if class_obj2 is not None: 263 | class_obj1 = class_obj2 264 | return class_obj1 265 | def gds_build_any(self, node, type_name=None): 266 | return None 267 | 268 | 269 | # 270 | # If you have installed IPython you can uncomment and use the following. 271 | # IPython is available from http://ipython.scipy.org/. 272 | # 273 | 274 | ## from IPython.Shell import IPShellEmbed 275 | ## args = '' 276 | ## ipshell = IPShellEmbed(args, 277 | ## banner = 'Dropping into IPython', 278 | ## exit_msg = 'Leaving Interpreter, back to program.') 279 | 280 | # Then use the following line where and when you want to drop into the 281 | # IPython shell: 282 | # ipshell(' -- Entering ipshell.\nHit Ctrl-D to exit') 283 | 284 | # 285 | # Globals 286 | # 287 | 288 | ExternalEncoding = 'ascii' 289 | Tag_pattern_ = re_.compile(r'({.*})?(.*)') 290 | String_cleanup_pat_ = re_.compile(r"[\n\r\s]+") 291 | Namespace_extract_pat_ = re_.compile(r'{(.*)}(.*)') 292 | 293 | # 294 | # Support/utility functions. 295 | # 296 | 297 | def showIndent(outfile, level, pretty_print=True): 298 | if pretty_print: 299 | for idx in range(level): 300 | outfile.write(' ') 301 | 302 | def quote_xml(inStr): 303 | if not inStr: 304 | return '' 305 | s1 = (isinstance(inStr, str) and inStr or 306 | '%s' % inStr) 307 | s1 = s1.replace('&', '&') 308 | s1 = s1.replace('<', '<') 309 | s1 = s1.replace('>', '>') 310 | return s1 311 | 312 | def quote_attrib(inStr): 313 | s1 = (isinstance(inStr, str) and inStr or 314 | '%s' % inStr) 315 | s1 = s1.replace('&', '&') 316 | s1 = s1.replace('<', '<') 317 | s1 = s1.replace('>', '>') 318 | if '"' in s1: 319 | if "'" in s1: 320 | s1 = '"%s"' % s1.replace('"', """) 321 | else: 322 | s1 = "'%s'" % s1 323 | else: 324 | s1 = '"%s"' % s1 325 | return s1 326 | 327 | def quote_python(inStr): 328 | s1 = inStr 329 | if s1.find("'") == -1: 330 | if s1.find('\n') == -1: 331 | return "'%s'" % s1 332 | else: 333 | return "'''%s'''" % s1 334 | else: 335 | if s1.find('"') != -1: 336 | s1 = s1.replace('"', '\\"') 337 | if s1.find('\n') == -1: 338 | return '"%s"' % s1 339 | else: 340 | return '"""%s"""' % s1 341 | 342 | def get_all_text_(node): 343 | if node.text is not None: 344 | text = node.text 345 | else: 346 | text = '' 347 | for child in node: 348 | if child.tail is not None: 349 | text += child.tail 350 | return text 351 | 352 | def find_attr_value_(attr_name, node): 353 | attrs = node.attrib 354 | attr_parts = attr_name.split(':') 355 | value = None 356 | if len(attr_parts) == 1: 357 | value = attrs.get(attr_name) 358 | elif len(attr_parts) == 2: 359 | prefix, name = attr_parts 360 | namespace = node.nsmap.get(prefix) 361 | if namespace is not None: 362 | value = attrs.get('{%s}%s' % (namespace, name, )) 363 | return value 364 | 365 | 366 | class GDSParseError(Exception): 367 | pass 368 | 369 | def raise_parse_error(node, msg): 370 | if XMLParser_import_library == XMLParser_import_lxml: 371 | msg = '%s (element %s/line %d)' % ( 372 | msg, node.tag, node.sourceline, ) 373 | else: 374 | msg = '%s (element %s)' % (msg, node.tag, ) 375 | raise GDSParseError(msg) 376 | 377 | 378 | class MixedContainer: 379 | # Constants for category: 380 | CategoryNone = 0 381 | CategoryText = 1 382 | CategorySimple = 2 383 | CategoryComplex = 3 384 | # Constants for content_type: 385 | TypeNone = 0 386 | TypeText = 1 387 | TypeString = 2 388 | TypeInteger = 3 389 | TypeFloat = 4 390 | TypeDecimal = 5 391 | TypeDouble = 6 392 | TypeBoolean = 7 393 | TypeBase64 = 8 394 | def __init__(self, category, content_type, name, value): 395 | self.category = category 396 | self.content_type = content_type 397 | self.name = name 398 | self.value = value 399 | def getCategory(self): 400 | return self.category 401 | def getContenttype(self, content_type): 402 | return self.content_type 403 | def getValue(self): 404 | return self.value 405 | def getName(self): 406 | return self.name 407 | def export(self, outfile, level, name, namespace, pretty_print=True): 408 | if self.category == MixedContainer.CategoryText: 409 | # Prevent exporting empty content as empty lines. 410 | if self.value.strip(): 411 | outfile.write(self.value) 412 | elif self.category == MixedContainer.CategorySimple: 413 | self.exportSimple(outfile, level, name) 414 | else: # category == MixedContainer.CategoryComplex 415 | self.value.export(outfile, level, namespace, name, pretty_print) 416 | def exportSimple(self, outfile, level, name): 417 | if self.content_type == MixedContainer.TypeString: 418 | outfile.write('<%s>%s' % 419 | (self.name, self.value, self.name)) 420 | elif self.content_type == MixedContainer.TypeInteger or \ 421 | self.content_type == MixedContainer.TypeBoolean: 422 | outfile.write('<%s>%d' % 423 | (self.name, self.value, self.name)) 424 | elif self.content_type == MixedContainer.TypeFloat or \ 425 | self.content_type == MixedContainer.TypeDecimal: 426 | outfile.write('<%s>%f' % 427 | (self.name, self.value, self.name)) 428 | elif self.content_type == MixedContainer.TypeDouble: 429 | outfile.write('<%s>%g' % 430 | (self.name, self.value, self.name)) 431 | elif self.content_type == MixedContainer.TypeBase64: 432 | outfile.write('<%s>%s' % 433 | (self.name, base64.b64encode(self.value), self.name)) 434 | def exportLiteral(self, outfile, level, name): 435 | if self.category == MixedContainer.CategoryText: 436 | showIndent(outfile, level) 437 | outfile.write('model_.MixedContainer(%d, %d, "%s", "%s"),\n' 438 | % (self.category, self.content_type, self.name, self.value)) 439 | elif self.category == MixedContainer.CategorySimple: 440 | showIndent(outfile, level) 441 | outfile.write('model_.MixedContainer(%d, %d, "%s", "%s"),\n' 442 | % (self.category, self.content_type, self.name, self.value)) 443 | else: # category == MixedContainer.CategoryComplex 444 | showIndent(outfile, level) 445 | outfile.write('model_.MixedContainer(%d, %d, "%s",\n' % \ 446 | (self.category, self.content_type, self.name,)) 447 | self.value.exportLiteral(outfile, level + 1) 448 | showIndent(outfile, level) 449 | outfile.write(')\n') 450 | 451 | 452 | class MemberSpec_(object): 453 | def __init__(self, name='', data_type='', container=0): 454 | self.name = name 455 | self.data_type = data_type 456 | self.container = container 457 | def set_name(self, name): self.name = name 458 | def get_name(self): return self.name 459 | def set_data_type(self, data_type): self.data_type = data_type 460 | def get_data_type_chain(self): return self.data_type 461 | def get_data_type(self): 462 | if isinstance(self.data_type, list): 463 | if len(self.data_type) > 0: 464 | return self.data_type[-1] 465 | else: 466 | return 'xs:string' 467 | else: 468 | return self.data_type 469 | def set_container(self, container): self.container = container 470 | def get_container(self): return self.container 471 | 472 | def _cast(typ, value): 473 | if typ is None or value is None: 474 | return value 475 | return typ(value) 476 | 477 | # 478 | # Data representation classes. 479 | # 480 | 481 | class scpd(GeneratedsSuper): 482 | subclass = None 483 | superclass = None 484 | def __init__(self, specVersion=None, actionList=None, serviceStateTable=None): 485 | self.specVersion = specVersion 486 | self.actionList = actionList 487 | self.serviceStateTable = serviceStateTable 488 | def factory(*args_, **kwargs_): 489 | if scpd.subclass: 490 | return scpd.subclass(*args_, **kwargs_) 491 | else: 492 | return scpd(*args_, **kwargs_) 493 | factory = staticmethod(factory) 494 | def get_specVersion(self): return self.specVersion 495 | def set_specVersion(self, specVersion): self.specVersion = specVersion 496 | def get_actionList(self): return self.actionList 497 | def set_actionList(self, actionList): self.actionList = actionList 498 | def get_serviceStateTable(self): return self.serviceStateTable 499 | def set_serviceStateTable(self, serviceStateTable): self.serviceStateTable = serviceStateTable 500 | def export(self, outfile, level, namespace_='', name_='scpd', namespacedef_='', pretty_print=True): 501 | if pretty_print: 502 | eol_ = '\n' 503 | else: 504 | eol_ = '' 505 | showIndent(outfile, level, pretty_print) 506 | outfile.write('<%s%s%s' % (namespace_, name_, namespacedef_ and ' ' + namespacedef_ or '', )) 507 | already_processed = [] 508 | self.exportAttributes(outfile, level, already_processed, namespace_, name_='scpd') 509 | if self.hasContent_(): 510 | outfile.write('>%s' % (eol_, )) 511 | self.exportChildren(outfile, level + 1, namespace_, name_, pretty_print=pretty_print) 512 | showIndent(outfile, level, pretty_print) 513 | outfile.write('%s' % (namespace_, name_, eol_)) 514 | else: 515 | outfile.write('/>%s' % (eol_, )) 516 | def exportAttributes(self, outfile, level, already_processed, namespace_='', name_='scpd'): 517 | pass 518 | def exportChildren(self, outfile, level, namespace_='', name_='scpd', fromsubclass_=False, pretty_print=True): 519 | if pretty_print: 520 | eol_ = '\n' 521 | else: 522 | eol_ = '' 523 | if self.specVersion is not None: 524 | self.specVersion.export(outfile, level, namespace_, name_='specVersion', pretty_print=pretty_print) 525 | if self.actionList is not None: 526 | self.actionList.export(outfile, level, namespace_, name_='actionList', pretty_print=pretty_print) 527 | if self.serviceStateTable is not None: 528 | self.serviceStateTable.export(outfile, level, namespace_, name_='serviceStateTable', pretty_print=pretty_print) 529 | def hasContent_(self): 530 | if ( 531 | self.specVersion is not None or 532 | self.actionList is not None or 533 | self.serviceStateTable is not None 534 | ): 535 | return True 536 | else: 537 | return False 538 | def exportLiteral(self, outfile, level, name_='scpd'): 539 | level += 1 540 | self.exportLiteralAttributes(outfile, level, [], name_) 541 | if self.hasContent_(): 542 | self.exportLiteralChildren(outfile, level, name_) 543 | def exportLiteralAttributes(self, outfile, level, already_processed, name_): 544 | pass 545 | def exportLiteralChildren(self, outfile, level, name_): 546 | if self.specVersion is not None: 547 | showIndent(outfile, level) 548 | outfile.write('specVersion=model_.SpecVersionType(\n') 549 | self.specVersion.exportLiteral(outfile, level, name_='specVersion') 550 | showIndent(outfile, level) 551 | outfile.write('),\n') 552 | if self.actionList is not None: 553 | showIndent(outfile, level) 554 | outfile.write('actionList=model_.ActionListType(\n') 555 | self.actionList.exportLiteral(outfile, level, name_='actionList') 556 | showIndent(outfile, level) 557 | outfile.write('),\n') 558 | if self.serviceStateTable is not None: 559 | showIndent(outfile, level) 560 | outfile.write('serviceStateTable=model_.ServiceStateTableType(\n') 561 | self.serviceStateTable.exportLiteral(outfile, level, name_='serviceStateTable') 562 | showIndent(outfile, level) 563 | outfile.write('),\n') 564 | def build(self, node): 565 | self.buildAttributes(node, node.attrib, []) 566 | for child in node: 567 | nodeName_ = Tag_pattern_.match(child.tag).groups()[-1] 568 | self.buildChildren(child, node, nodeName_) 569 | def buildAttributes(self, node, attrs, already_processed): 570 | pass 571 | def buildChildren(self, child_, node, nodeName_, fromsubclass_=False): 572 | if nodeName_ == 'specVersion': 573 | obj_ = SpecVersionType.factory() 574 | obj_.build(child_) 575 | self.set_specVersion(obj_) 576 | elif nodeName_ == 'actionList': 577 | obj_ = ActionListType.factory() 578 | obj_.build(child_) 579 | self.set_actionList(obj_) 580 | elif nodeName_ == 'serviceStateTable': 581 | obj_ = ServiceStateTableType.factory() 582 | obj_.build(child_) 583 | self.set_serviceStateTable(obj_) 584 | # end class scpd 585 | 586 | 587 | class SpecVersionType(GeneratedsSuper): 588 | subclass = None 589 | superclass = None 590 | def __init__(self, major=None, minor=None): 591 | self.major = major 592 | self.minor = minor 593 | def factory(*args_, **kwargs_): 594 | if SpecVersionType.subclass: 595 | return SpecVersionType.subclass(*args_, **kwargs_) 596 | else: 597 | return SpecVersionType(*args_, **kwargs_) 598 | factory = staticmethod(factory) 599 | def get_major(self): return self.major 600 | def set_major(self, major): self.major = major 601 | def get_minor(self): return self.minor 602 | def set_minor(self, minor): self.minor = minor 603 | def export(self, outfile, level, namespace_='', name_='SpecVersionType', namespacedef_='', pretty_print=True): 604 | if pretty_print: 605 | eol_ = '\n' 606 | else: 607 | eol_ = '' 608 | showIndent(outfile, level, pretty_print) 609 | outfile.write('<%s%s%s' % (namespace_, name_, namespacedef_ and ' ' + namespacedef_ or '', )) 610 | already_processed = [] 611 | self.exportAttributes(outfile, level, already_processed, namespace_, name_='SpecVersionType') 612 | if self.hasContent_(): 613 | outfile.write('>%s' % (eol_, )) 614 | self.exportChildren(outfile, level + 1, namespace_, name_, pretty_print=pretty_print) 615 | showIndent(outfile, level, pretty_print) 616 | outfile.write('%s' % (namespace_, name_, eol_)) 617 | else: 618 | outfile.write('/>%s' % (eol_, )) 619 | def exportAttributes(self, outfile, level, already_processed, namespace_='', name_='SpecVersionType'): 620 | pass 621 | def exportChildren(self, outfile, level, namespace_='', name_='SpecVersionType', fromsubclass_=False, pretty_print=True): 622 | if pretty_print: 623 | eol_ = '\n' 624 | else: 625 | eol_ = '' 626 | if self.major is not None: 627 | showIndent(outfile, level, pretty_print) 628 | outfile.write('<%smajor>%s%s' % (namespace_, self.gds_format_integer(self.major, input_name='major'), namespace_, eol_)) 629 | if self.minor is not None: 630 | showIndent(outfile, level, pretty_print) 631 | outfile.write('<%sminor>%s%s' % (namespace_, self.gds_format_integer(self.minor, input_name='minor'), namespace_, eol_)) 632 | def hasContent_(self): 633 | if ( 634 | self.major is not None or 635 | self.minor is not None 636 | ): 637 | return True 638 | else: 639 | return False 640 | def exportLiteral(self, outfile, level, name_='SpecVersionType'): 641 | level += 1 642 | self.exportLiteralAttributes(outfile, level, [], name_) 643 | if self.hasContent_(): 644 | self.exportLiteralChildren(outfile, level, name_) 645 | def exportLiteralAttributes(self, outfile, level, already_processed, name_): 646 | pass 647 | def exportLiteralChildren(self, outfile, level, name_): 648 | if self.major is not None: 649 | showIndent(outfile, level) 650 | outfile.write('major=%d,\n' % self.major) 651 | if self.minor is not None: 652 | showIndent(outfile, level) 653 | outfile.write('minor=%d,\n' % self.minor) 654 | def build(self, node): 655 | self.buildAttributes(node, node.attrib, []) 656 | for child in node: 657 | nodeName_ = Tag_pattern_.match(child.tag).groups()[-1] 658 | self.buildChildren(child, node, nodeName_) 659 | def buildAttributes(self, node, attrs, already_processed): 660 | pass 661 | def buildChildren(self, child_, node, nodeName_, fromsubclass_=False): 662 | if nodeName_ == 'major': 663 | sval_ = child_.text 664 | try: 665 | ival_ = int(sval_) 666 | except (TypeError, ValueError) as exp: 667 | raise_parse_error(child_, 'requires integer: %s' % exp) 668 | ival_ = self.gds_validate_integer(ival_, node, 'major') 669 | self.major = ival_ 670 | elif nodeName_ == 'minor': 671 | sval_ = child_.text 672 | try: 673 | ival_ = int(sval_) 674 | except (TypeError, ValueError) as exp: 675 | raise_parse_error(child_, 'requires integer: %s' % exp) 676 | ival_ = self.gds_validate_integer(ival_, node, 'minor') 677 | self.minor = ival_ 678 | # end class SpecVersionType 679 | 680 | 681 | class ActionListType(GeneratedsSuper): 682 | subclass = None 683 | superclass = None 684 | def __init__(self, action=None): 685 | if action is None: 686 | self.action = [] 687 | else: 688 | self.action = action 689 | def factory(*args_, **kwargs_): 690 | if ActionListType.subclass: 691 | return ActionListType.subclass(*args_, **kwargs_) 692 | else: 693 | return ActionListType(*args_, **kwargs_) 694 | factory = staticmethod(factory) 695 | def get_action(self): return self.action 696 | def set_action(self, action): self.action = action 697 | def add_action(self, value): self.action.append(value) 698 | def insert_action(self, index, value): self.action[index] = value 699 | def export(self, outfile, level, namespace_='', name_='ActionListType', namespacedef_='', pretty_print=True): 700 | if pretty_print: 701 | eol_ = '\n' 702 | else: 703 | eol_ = '' 704 | showIndent(outfile, level, pretty_print) 705 | outfile.write('<%s%s%s' % (namespace_, name_, namespacedef_ and ' ' + namespacedef_ or '', )) 706 | already_processed = [] 707 | self.exportAttributes(outfile, level, already_processed, namespace_, name_='ActionListType') 708 | if self.hasContent_(): 709 | outfile.write('>%s' % (eol_, )) 710 | self.exportChildren(outfile, level + 1, namespace_, name_, pretty_print=pretty_print) 711 | showIndent(outfile, level, pretty_print) 712 | outfile.write('%s' % (namespace_, name_, eol_)) 713 | else: 714 | outfile.write('/>%s' % (eol_, )) 715 | def exportAttributes(self, outfile, level, already_processed, namespace_='', name_='ActionListType'): 716 | pass 717 | def exportChildren(self, outfile, level, namespace_='', name_='ActionListType', fromsubclass_=False, pretty_print=True): 718 | if pretty_print: 719 | eol_ = '\n' 720 | else: 721 | eol_ = '' 722 | for action_ in self.action: 723 | action_.export(outfile, level, namespace_, name_='action', pretty_print=pretty_print) 724 | def hasContent_(self): 725 | if ( 726 | self.action 727 | ): 728 | return True 729 | else: 730 | return False 731 | def exportLiteral(self, outfile, level, name_='ActionListType'): 732 | level += 1 733 | self.exportLiteralAttributes(outfile, level, [], name_) 734 | if self.hasContent_(): 735 | self.exportLiteralChildren(outfile, level, name_) 736 | def exportLiteralAttributes(self, outfile, level, already_processed, name_): 737 | pass 738 | def exportLiteralChildren(self, outfile, level, name_): 739 | showIndent(outfile, level) 740 | outfile.write('action=[\n') 741 | level += 1 742 | for action_ in self.action: 743 | showIndent(outfile, level) 744 | outfile.write('model_.ActionType(\n') 745 | action_.exportLiteral(outfile, level, name_='ActionType') 746 | showIndent(outfile, level) 747 | outfile.write('),\n') 748 | level -= 1 749 | showIndent(outfile, level) 750 | outfile.write('],\n') 751 | def build(self, node): 752 | self.buildAttributes(node, node.attrib, []) 753 | for child in node: 754 | nodeName_ = Tag_pattern_.match(child.tag).groups()[-1] 755 | self.buildChildren(child, node, nodeName_) 756 | def buildAttributes(self, node, attrs, already_processed): 757 | pass 758 | def buildChildren(self, child_, node, nodeName_, fromsubclass_=False): 759 | if nodeName_ == 'action': 760 | obj_ = ActionType.factory() 761 | obj_.build(child_) 762 | self.action.append(obj_) 763 | # end class ActionListType 764 | 765 | 766 | class ActionType(GeneratedsSuper): 767 | subclass = None 768 | superclass = None 769 | def __init__(self, name=None, argumentList=None): 770 | self.name = name 771 | self.argumentList = argumentList 772 | def factory(*args_, **kwargs_): 773 | if ActionType.subclass: 774 | return ActionType.subclass(*args_, **kwargs_) 775 | else: 776 | return ActionType(*args_, **kwargs_) 777 | factory = staticmethod(factory) 778 | def get_name(self): return self.name 779 | def set_name(self, name): self.name = name 780 | def get_argumentList(self): return self.argumentList 781 | def set_argumentList(self, argumentList): self.argumentList = argumentList 782 | def export(self, outfile, level, namespace_='', name_='ActionType', namespacedef_='', pretty_print=True): 783 | if pretty_print: 784 | eol_ = '\n' 785 | else: 786 | eol_ = '' 787 | showIndent(outfile, level, pretty_print) 788 | outfile.write('<%s%s%s' % (namespace_, name_, namespacedef_ and ' ' + namespacedef_ or '', )) 789 | already_processed = [] 790 | self.exportAttributes(outfile, level, already_processed, namespace_, name_='ActionType') 791 | if self.hasContent_(): 792 | outfile.write('>%s' % (eol_, )) 793 | self.exportChildren(outfile, level + 1, namespace_, name_, pretty_print=pretty_print) 794 | showIndent(outfile, level, pretty_print) 795 | outfile.write('%s' % (namespace_, name_, eol_)) 796 | else: 797 | outfile.write('/>%s' % (eol_, )) 798 | def exportAttributes(self, outfile, level, already_processed, namespace_='', name_='ActionType'): 799 | pass 800 | def exportChildren(self, outfile, level, namespace_='', name_='ActionType', fromsubclass_=False, pretty_print=True): 801 | if pretty_print: 802 | eol_ = '\n' 803 | else: 804 | eol_ = '' 805 | if self.name is not None: 806 | showIndent(outfile, level, pretty_print) 807 | outfile.write('<%sname>%s%s' % (namespace_, self.gds_format_string(quote_xml(self.name).encode(ExternalEncoding), input_name='name'), namespace_, eol_)) 808 | if self.argumentList is not None: 809 | self.argumentList.export(outfile, level, namespace_, name_='argumentList', pretty_print=pretty_print) 810 | def hasContent_(self): 811 | if ( 812 | self.name is not None or 813 | self.argumentList is not None 814 | ): 815 | return True 816 | else: 817 | return False 818 | def exportLiteral(self, outfile, level, name_='ActionType'): 819 | level += 1 820 | self.exportLiteralAttributes(outfile, level, [], name_) 821 | if self.hasContent_(): 822 | self.exportLiteralChildren(outfile, level, name_) 823 | def exportLiteralAttributes(self, outfile, level, already_processed, name_): 824 | pass 825 | def exportLiteralChildren(self, outfile, level, name_): 826 | if self.name is not None: 827 | showIndent(outfile, level) 828 | outfile.write('name=%s,\n' % quote_python(self.name).encode(ExternalEncoding)) 829 | if self.argumentList is not None: 830 | showIndent(outfile, level) 831 | outfile.write('argumentList=model_.ArgumentListType(\n') 832 | self.argumentList.exportLiteral(outfile, level, name_='argumentList') 833 | showIndent(outfile, level) 834 | outfile.write('),\n') 835 | def build(self, node): 836 | self.buildAttributes(node, node.attrib, []) 837 | for child in node: 838 | nodeName_ = Tag_pattern_.match(child.tag).groups()[-1] 839 | self.buildChildren(child, node, nodeName_) 840 | def buildAttributes(self, node, attrs, already_processed): 841 | pass 842 | def buildChildren(self, child_, node, nodeName_, fromsubclass_=False): 843 | if nodeName_ == 'name': 844 | name_ = child_.text 845 | name_ = self.gds_validate_string(name_, node, 'name') 846 | self.name = name_ 847 | elif nodeName_ == 'argumentList': 848 | obj_ = ArgumentListType.factory() 849 | obj_.build(child_) 850 | self.set_argumentList(obj_) 851 | # end class ActionType 852 | 853 | 854 | class ArgumentListType(GeneratedsSuper): 855 | subclass = None 856 | superclass = None 857 | def __init__(self, argument=None): 858 | if argument is None: 859 | self.argument = [] 860 | else: 861 | self.argument = argument 862 | def factory(*args_, **kwargs_): 863 | if ArgumentListType.subclass: 864 | return ArgumentListType.subclass(*args_, **kwargs_) 865 | else: 866 | return ArgumentListType(*args_, **kwargs_) 867 | factory = staticmethod(factory) 868 | def get_argument(self): return self.argument 869 | def set_argument(self, argument): self.argument = argument 870 | def add_argument(self, value): self.argument.append(value) 871 | def insert_argument(self, index, value): self.argument[index] = value 872 | def export(self, outfile, level, namespace_='', name_='ArgumentListType', namespacedef_='', pretty_print=True): 873 | if pretty_print: 874 | eol_ = '\n' 875 | else: 876 | eol_ = '' 877 | showIndent(outfile, level, pretty_print) 878 | outfile.write('<%s%s%s' % (namespace_, name_, namespacedef_ and ' ' + namespacedef_ or '', )) 879 | already_processed = [] 880 | self.exportAttributes(outfile, level, already_processed, namespace_, name_='ArgumentListType') 881 | if self.hasContent_(): 882 | outfile.write('>%s' % (eol_, )) 883 | self.exportChildren(outfile, level + 1, namespace_, name_, pretty_print=pretty_print) 884 | showIndent(outfile, level, pretty_print) 885 | outfile.write('%s' % (namespace_, name_, eol_)) 886 | else: 887 | outfile.write('/>%s' % (eol_, )) 888 | def exportAttributes(self, outfile, level, already_processed, namespace_='', name_='ArgumentListType'): 889 | pass 890 | def exportChildren(self, outfile, level, namespace_='', name_='ArgumentListType', fromsubclass_=False, pretty_print=True): 891 | if pretty_print: 892 | eol_ = '\n' 893 | else: 894 | eol_ = '' 895 | for argument_ in self.argument: 896 | argument_.export(outfile, level, namespace_, name_='argument', pretty_print=pretty_print) 897 | def hasContent_(self): 898 | if ( 899 | self.argument 900 | ): 901 | return True 902 | else: 903 | return False 904 | def exportLiteral(self, outfile, level, name_='ArgumentListType'): 905 | level += 1 906 | self.exportLiteralAttributes(outfile, level, [], name_) 907 | if self.hasContent_(): 908 | self.exportLiteralChildren(outfile, level, name_) 909 | def exportLiteralAttributes(self, outfile, level, already_processed, name_): 910 | pass 911 | def exportLiteralChildren(self, outfile, level, name_): 912 | showIndent(outfile, level) 913 | outfile.write('argument=[\n') 914 | level += 1 915 | for argument_ in self.argument: 916 | showIndent(outfile, level) 917 | outfile.write('model_.ArgumentType(\n') 918 | argument_.exportLiteral(outfile, level, name_='ArgumentType') 919 | showIndent(outfile, level) 920 | outfile.write('),\n') 921 | level -= 1 922 | showIndent(outfile, level) 923 | outfile.write('],\n') 924 | def build(self, node): 925 | self.buildAttributes(node, node.attrib, []) 926 | for child in node: 927 | nodeName_ = Tag_pattern_.match(child.tag).groups()[-1] 928 | self.buildChildren(child, node, nodeName_) 929 | def buildAttributes(self, node, attrs, already_processed): 930 | pass 931 | def buildChildren(self, child_, node, nodeName_, fromsubclass_=False): 932 | if nodeName_ == 'argument': 933 | obj_ = ArgumentType.factory() 934 | obj_.build(child_) 935 | self.argument.append(obj_) 936 | # end class ArgumentListType 937 | 938 | 939 | class ArgumentType(GeneratedsSuper): 940 | subclass = None 941 | superclass = None 942 | def __init__(self, name=None, direction=None, relatedStateVariable=None, retval=None): 943 | self.name = name 944 | self.direction = direction 945 | self.relatedStateVariable = relatedStateVariable 946 | self.retval = retval 947 | def factory(*args_, **kwargs_): 948 | if ArgumentType.subclass: 949 | return ArgumentType.subclass(*args_, **kwargs_) 950 | else: 951 | return ArgumentType(*args_, **kwargs_) 952 | factory = staticmethod(factory) 953 | def get_name(self): return self.name 954 | def set_name(self, name): self.name = name 955 | def get_direction(self): return self.direction 956 | def set_direction(self, direction): self.direction = direction 957 | def get_relatedStateVariable(self): return self.relatedStateVariable 958 | def set_relatedStateVariable(self, relatedStateVariable): self.relatedStateVariable = relatedStateVariable 959 | def get_retval(self): return self.retval 960 | def set_retval(self, retval): self.retval = retval 961 | def export(self, outfile, level, namespace_='', name_='ArgumentType', namespacedef_='', pretty_print=True): 962 | if pretty_print: 963 | eol_ = '\n' 964 | else: 965 | eol_ = '' 966 | showIndent(outfile, level, pretty_print) 967 | outfile.write('<%s%s%s' % (namespace_, name_, namespacedef_ and ' ' + namespacedef_ or '', )) 968 | already_processed = [] 969 | self.exportAttributes(outfile, level, already_processed, namespace_, name_='ArgumentType') 970 | if self.hasContent_(): 971 | outfile.write('>%s' % (eol_, )) 972 | self.exportChildren(outfile, level + 1, namespace_, name_, pretty_print=pretty_print) 973 | showIndent(outfile, level, pretty_print) 974 | outfile.write('%s' % (namespace_, name_, eol_)) 975 | else: 976 | outfile.write('/>%s' % (eol_, )) 977 | def exportAttributes(self, outfile, level, already_processed, namespace_='', name_='ArgumentType'): 978 | pass 979 | def exportChildren(self, outfile, level, namespace_='', name_='ArgumentType', fromsubclass_=False, pretty_print=True): 980 | if pretty_print: 981 | eol_ = '\n' 982 | else: 983 | eol_ = '' 984 | if self.name is not None: 985 | showIndent(outfile, level, pretty_print) 986 | outfile.write('<%sname>%s%s' % (namespace_, self.gds_format_string(quote_xml(self.name).encode(ExternalEncoding), input_name='name'), namespace_, eol_)) 987 | if self.direction is not None: 988 | showIndent(outfile, level, pretty_print) 989 | outfile.write('<%sdirection>%s%s' % (namespace_, self.gds_format_string(quote_xml(self.direction).encode(ExternalEncoding), input_name='direction'), namespace_, eol_)) 990 | if self.relatedStateVariable is not None: 991 | showIndent(outfile, level, pretty_print) 992 | outfile.write('<%srelatedStateVariable>%s%s' % (namespace_, self.gds_format_string(quote_xml(self.relatedStateVariable).encode(ExternalEncoding), input_name='relatedStateVariable'), namespace_, eol_)) 993 | if self.retval is not None: 994 | self.retval.export(outfile, level, namespace_, name_='retval', pretty_print=pretty_print) 995 | def hasContent_(self): 996 | if ( 997 | self.name is not None or 998 | self.direction is not None or 999 | self.relatedStateVariable is not None or 1000 | self.retval is not None 1001 | ): 1002 | return True 1003 | else: 1004 | return False 1005 | def exportLiteral(self, outfile, level, name_='ArgumentType'): 1006 | level += 1 1007 | self.exportLiteralAttributes(outfile, level, [], name_) 1008 | if self.hasContent_(): 1009 | self.exportLiteralChildren(outfile, level, name_) 1010 | def exportLiteralAttributes(self, outfile, level, already_processed, name_): 1011 | pass 1012 | def exportLiteralChildren(self, outfile, level, name_): 1013 | if self.name is not None: 1014 | showIndent(outfile, level) 1015 | outfile.write('name=%s,\n' % quote_python(self.name).encode(ExternalEncoding)) 1016 | if self.direction is not None: 1017 | showIndent(outfile, level) 1018 | outfile.write('direction=%s,\n' % quote_python(self.direction).encode(ExternalEncoding)) 1019 | if self.relatedStateVariable is not None: 1020 | showIndent(outfile, level) 1021 | outfile.write('relatedStateVariable=%s,\n' % quote_python(self.relatedStateVariable).encode(ExternalEncoding)) 1022 | if self.retval is not None: 1023 | showIndent(outfile, level) 1024 | outfile.write('retval=model_.retvalType(\n') 1025 | self.retval.exportLiteral(outfile, level, name_='retval') 1026 | showIndent(outfile, level) 1027 | outfile.write('),\n') 1028 | def build(self, node): 1029 | self.buildAttributes(node, node.attrib, []) 1030 | for child in node: 1031 | nodeName_ = Tag_pattern_.match(child.tag).groups()[-1] 1032 | self.buildChildren(child, node, nodeName_) 1033 | def buildAttributes(self, node, attrs, already_processed): 1034 | pass 1035 | def buildChildren(self, child_, node, nodeName_, fromsubclass_=False): 1036 | if nodeName_ == 'name': 1037 | name_ = child_.text 1038 | name_ = self.gds_validate_string(name_, node, 'name') 1039 | self.name = name_ 1040 | elif nodeName_ == 'direction': 1041 | direction_ = child_.text 1042 | direction_ = self.gds_validate_string(direction_, node, 'direction') 1043 | self.direction = direction_ 1044 | elif nodeName_ == 'relatedStateVariable': 1045 | relatedStateVariable_ = child_.text 1046 | relatedStateVariable_ = self.gds_validate_string(relatedStateVariable_, node, 'relatedStateVariable') 1047 | self.relatedStateVariable = relatedStateVariable_ 1048 | elif nodeName_ == 'retval': 1049 | obj_ = retvalType.factory() 1050 | obj_.build(child_) 1051 | self.set_retval(obj_) 1052 | # end class ArgumentType 1053 | 1054 | 1055 | class ServiceStateTableType(GeneratedsSuper): 1056 | subclass = None 1057 | superclass = None 1058 | def __init__(self, stateVariable=None): 1059 | if stateVariable is None: 1060 | self.stateVariable = [] 1061 | else: 1062 | self.stateVariable = stateVariable 1063 | def factory(*args_, **kwargs_): 1064 | if ServiceStateTableType.subclass: 1065 | return ServiceStateTableType.subclass(*args_, **kwargs_) 1066 | else: 1067 | return ServiceStateTableType(*args_, **kwargs_) 1068 | factory = staticmethod(factory) 1069 | def get_stateVariable(self): return self.stateVariable 1070 | def set_stateVariable(self, stateVariable): self.stateVariable = stateVariable 1071 | def add_stateVariable(self, value): self.stateVariable.append(value) 1072 | def insert_stateVariable(self, index, value): self.stateVariable[index] = value 1073 | def export(self, outfile, level, namespace_='', name_='ServiceStateTableType', namespacedef_='', pretty_print=True): 1074 | if pretty_print: 1075 | eol_ = '\n' 1076 | else: 1077 | eol_ = '' 1078 | showIndent(outfile, level, pretty_print) 1079 | outfile.write('<%s%s%s' % (namespace_, name_, namespacedef_ and ' ' + namespacedef_ or '', )) 1080 | already_processed = [] 1081 | self.exportAttributes(outfile, level, already_processed, namespace_, name_='ServiceStateTableType') 1082 | if self.hasContent_(): 1083 | outfile.write('>%s' % (eol_, )) 1084 | self.exportChildren(outfile, level + 1, namespace_, name_, pretty_print=pretty_print) 1085 | showIndent(outfile, level, pretty_print) 1086 | outfile.write('%s' % (namespace_, name_, eol_)) 1087 | else: 1088 | outfile.write('/>%s' % (eol_, )) 1089 | def exportAttributes(self, outfile, level, already_processed, namespace_='', name_='ServiceStateTableType'): 1090 | pass 1091 | def exportChildren(self, outfile, level, namespace_='', name_='ServiceStateTableType', fromsubclass_=False, pretty_print=True): 1092 | if pretty_print: 1093 | eol_ = '\n' 1094 | else: 1095 | eol_ = '' 1096 | for stateVariable_ in self.stateVariable: 1097 | stateVariable_.export(outfile, level, namespace_, name_='stateVariable', pretty_print=pretty_print) 1098 | def hasContent_(self): 1099 | if ( 1100 | self.stateVariable 1101 | ): 1102 | return True 1103 | else: 1104 | return False 1105 | def exportLiteral(self, outfile, level, name_='ServiceStateTableType'): 1106 | level += 1 1107 | self.exportLiteralAttributes(outfile, level, [], name_) 1108 | if self.hasContent_(): 1109 | self.exportLiteralChildren(outfile, level, name_) 1110 | def exportLiteralAttributes(self, outfile, level, already_processed, name_): 1111 | pass 1112 | def exportLiteralChildren(self, outfile, level, name_): 1113 | showIndent(outfile, level) 1114 | outfile.write('stateVariable=[\n') 1115 | level += 1 1116 | for stateVariable_ in self.stateVariable: 1117 | showIndent(outfile, level) 1118 | outfile.write('model_.StateVariableType(\n') 1119 | stateVariable_.exportLiteral(outfile, level, name_='StateVariableType') 1120 | showIndent(outfile, level) 1121 | outfile.write('),\n') 1122 | level -= 1 1123 | showIndent(outfile, level) 1124 | outfile.write('],\n') 1125 | def build(self, node): 1126 | self.buildAttributes(node, node.attrib, []) 1127 | for child in node: 1128 | nodeName_ = Tag_pattern_.match(child.tag).groups()[-1] 1129 | self.buildChildren(child, node, nodeName_) 1130 | def buildAttributes(self, node, attrs, already_processed): 1131 | pass 1132 | def buildChildren(self, child_, node, nodeName_, fromsubclass_=False): 1133 | if nodeName_ == 'stateVariable': 1134 | obj_ = StateVariableType.factory() 1135 | obj_.build(child_) 1136 | self.stateVariable.append(obj_) 1137 | # end class ServiceStateTableType 1138 | 1139 | 1140 | class StateVariableType(GeneratedsSuper): 1141 | subclass = None 1142 | superclass = None 1143 | def __init__(self, sendEvents='yes', name=None, dataType=None, defaultValue=None, allowedValueList=None, allowedValueRange=None): 1144 | self.sendEvents = _cast(None, sendEvents) 1145 | self.name = name 1146 | self.dataType = dataType 1147 | self.defaultValue = defaultValue 1148 | self.allowedValueList = allowedValueList 1149 | self.allowedValueRange = allowedValueRange 1150 | def factory(*args_, **kwargs_): 1151 | if StateVariableType.subclass: 1152 | return StateVariableType.subclass(*args_, **kwargs_) 1153 | else: 1154 | return StateVariableType(*args_, **kwargs_) 1155 | factory = staticmethod(factory) 1156 | def get_name(self): return self.name 1157 | def set_name(self, name): self.name = name 1158 | def get_dataType(self): return self.dataType 1159 | def set_dataType(self, dataType): self.dataType = dataType 1160 | def get_defaultValue(self): return self.defaultValue 1161 | def set_defaultValue(self, defaultValue): self.defaultValue = defaultValue 1162 | def get_allowedValueList(self): return self.allowedValueList 1163 | def set_allowedValueList(self, allowedValueList): self.allowedValueList = allowedValueList 1164 | def get_allowedValueRange(self): return self.allowedValueRange 1165 | def set_allowedValueRange(self, allowedValueRange): self.allowedValueRange = allowedValueRange 1166 | def get_sendEvents(self): return self.sendEvents 1167 | def set_sendEvents(self, sendEvents): self.sendEvents = sendEvents 1168 | def export(self, outfile, level, namespace_='', name_='StateVariableType', namespacedef_='', pretty_print=True): 1169 | if pretty_print: 1170 | eol_ = '\n' 1171 | else: 1172 | eol_ = '' 1173 | showIndent(outfile, level, pretty_print) 1174 | outfile.write('<%s%s%s' % (namespace_, name_, namespacedef_ and ' ' + namespacedef_ or '', )) 1175 | already_processed = [] 1176 | self.exportAttributes(outfile, level, already_processed, namespace_, name_='StateVariableType') 1177 | if self.hasContent_(): 1178 | outfile.write('>%s' % (eol_, )) 1179 | self.exportChildren(outfile, level + 1, namespace_, name_, pretty_print=pretty_print) 1180 | showIndent(outfile, level, pretty_print) 1181 | outfile.write('%s' % (namespace_, name_, eol_)) 1182 | else: 1183 | outfile.write('/>%s' % (eol_, )) 1184 | def exportAttributes(self, outfile, level, already_processed, namespace_='', name_='StateVariableType'): 1185 | if self.sendEvents is not None and 'sendEvents' not in already_processed: 1186 | already_processed.append('sendEvents') 1187 | outfile.write(' sendEvents=%s' % (self.gds_format_string(quote_attrib(self.sendEvents).encode(ExternalEncoding), input_name='sendEvents'), )) 1188 | def exportChildren(self, outfile, level, namespace_='', name_='StateVariableType', fromsubclass_=False, pretty_print=True): 1189 | if pretty_print: 1190 | eol_ = '\n' 1191 | else: 1192 | eol_ = '' 1193 | if self.name is not None: 1194 | showIndent(outfile, level, pretty_print) 1195 | outfile.write('<%sname>%s%s' % (namespace_, self.gds_format_string(quote_xml(self.name).encode(ExternalEncoding), input_name='name'), namespace_, eol_)) 1196 | if self.dataType is not None: 1197 | showIndent(outfile, level, pretty_print) 1198 | outfile.write('<%sdataType>%s%s' % (namespace_, self.gds_format_string(quote_xml(self.dataType).encode(ExternalEncoding), input_name='dataType'), namespace_, eol_)) 1199 | if self.defaultValue is not None: 1200 | showIndent(outfile, level, pretty_print) 1201 | outfile.write('<%sdefaultValue>%s%s' % (namespace_, self.gds_format_string(quote_xml(self.defaultValue).encode(ExternalEncoding), input_name='defaultValue'), namespace_, eol_)) 1202 | if self.allowedValueList is not None: 1203 | self.allowedValueList.export(outfile, level, namespace_, name_='allowedValueList', pretty_print=pretty_print) 1204 | if self.allowedValueRange is not None: 1205 | self.allowedValueRange.export(outfile, level, namespace_, name_='allowedValueRange', pretty_print=pretty_print) 1206 | def hasContent_(self): 1207 | if ( 1208 | self.name is not None or 1209 | self.dataType is not None or 1210 | self.defaultValue is not None or 1211 | self.allowedValueList is not None or 1212 | self.allowedValueRange is not None 1213 | ): 1214 | return True 1215 | else: 1216 | return False 1217 | def exportLiteral(self, outfile, level, name_='StateVariableType'): 1218 | level += 1 1219 | self.exportLiteralAttributes(outfile, level, [], name_) 1220 | if self.hasContent_(): 1221 | self.exportLiteralChildren(outfile, level, name_) 1222 | def exportLiteralAttributes(self, outfile, level, already_processed, name_): 1223 | if self.sendEvents is not None and 'sendEvents' not in already_processed: 1224 | already_processed.append('sendEvents') 1225 | showIndent(outfile, level) 1226 | outfile.write('sendEvents = "%s",\n' % (self.sendEvents,)) 1227 | def exportLiteralChildren(self, outfile, level, name_): 1228 | if self.name is not None: 1229 | showIndent(outfile, level) 1230 | outfile.write('name=%s,\n' % quote_python(self.name).encode(ExternalEncoding)) 1231 | if self.dataType is not None: 1232 | showIndent(outfile, level) 1233 | outfile.write('dataType=%s,\n' % quote_python(self.dataType).encode(ExternalEncoding)) 1234 | if self.defaultValue is not None: 1235 | showIndent(outfile, level) 1236 | outfile.write('defaultValue=%s,\n' % quote_python(self.defaultValue).encode(ExternalEncoding)) 1237 | if self.allowedValueList is not None: 1238 | showIndent(outfile, level) 1239 | outfile.write('allowedValueList=model_.AllowedValueListType(\n') 1240 | self.allowedValueList.exportLiteral(outfile, level, name_='allowedValueList') 1241 | showIndent(outfile, level) 1242 | outfile.write('),\n') 1243 | if self.allowedValueRange is not None: 1244 | showIndent(outfile, level) 1245 | outfile.write('allowedValueRange=model_.AllowedValueRangeType(\n') 1246 | self.allowedValueRange.exportLiteral(outfile, level, name_='allowedValueRange') 1247 | showIndent(outfile, level) 1248 | outfile.write('),\n') 1249 | def build(self, node): 1250 | self.buildAttributes(node, node.attrib, []) 1251 | for child in node: 1252 | nodeName_ = Tag_pattern_.match(child.tag).groups()[-1] 1253 | self.buildChildren(child, node, nodeName_) 1254 | def buildAttributes(self, node, attrs, already_processed): 1255 | value = find_attr_value_('sendEvents', node) 1256 | if value is not None and 'sendEvents' not in already_processed: 1257 | already_processed.append('sendEvents') 1258 | self.sendEvents = value 1259 | def buildChildren(self, child_, node, nodeName_, fromsubclass_=False): 1260 | if nodeName_ == 'name': 1261 | name_ = child_.text 1262 | name_ = self.gds_validate_string(name_, node, 'name') 1263 | self.name = name_ 1264 | elif nodeName_ == 'dataType': 1265 | dataType_ = child_.text 1266 | dataType_ = self.gds_validate_string(dataType_, node, 'dataType') 1267 | self.dataType = dataType_ 1268 | elif nodeName_ == 'defaultValue': 1269 | defaultValue_ = child_.text 1270 | defaultValue_ = self.gds_validate_string(defaultValue_, node, 'defaultValue') 1271 | self.defaultValue = defaultValue_ 1272 | elif nodeName_ == 'allowedValueList': 1273 | obj_ = AllowedValueListType.factory() 1274 | obj_.build(child_) 1275 | self.set_allowedValueList(obj_) 1276 | elif nodeName_ == 'allowedValueRange': 1277 | obj_ = AllowedValueRangeType.factory() 1278 | obj_.build(child_) 1279 | self.set_allowedValueRange(obj_) 1280 | # end class StateVariableType 1281 | 1282 | 1283 | class AllowedValueListType(GeneratedsSuper): 1284 | subclass = None 1285 | superclass = None 1286 | def __init__(self, allowedValue=None): 1287 | if allowedValue is None: 1288 | self.allowedValue = [] 1289 | else: 1290 | self.allowedValue = allowedValue 1291 | def factory(*args_, **kwargs_): 1292 | if AllowedValueListType.subclass: 1293 | return AllowedValueListType.subclass(*args_, **kwargs_) 1294 | else: 1295 | return AllowedValueListType(*args_, **kwargs_) 1296 | factory = staticmethod(factory) 1297 | def get_allowedValue(self): return self.allowedValue 1298 | def set_allowedValue(self, allowedValue): self.allowedValue = allowedValue 1299 | def add_allowedValue(self, value): self.allowedValue.append(value) 1300 | def insert_allowedValue(self, index, value): self.allowedValue[index] = value 1301 | def export(self, outfile, level, namespace_='', name_='AllowedValueListType', namespacedef_='', pretty_print=True): 1302 | if pretty_print: 1303 | eol_ = '\n' 1304 | else: 1305 | eol_ = '' 1306 | showIndent(outfile, level, pretty_print) 1307 | outfile.write('<%s%s%s' % (namespace_, name_, namespacedef_ and ' ' + namespacedef_ or '', )) 1308 | already_processed = [] 1309 | self.exportAttributes(outfile, level, already_processed, namespace_, name_='AllowedValueListType') 1310 | if self.hasContent_(): 1311 | outfile.write('>%s' % (eol_, )) 1312 | self.exportChildren(outfile, level + 1, namespace_, name_, pretty_print=pretty_print) 1313 | showIndent(outfile, level, pretty_print) 1314 | outfile.write('%s' % (namespace_, name_, eol_)) 1315 | else: 1316 | outfile.write('/>%s' % (eol_, )) 1317 | def exportAttributes(self, outfile, level, already_processed, namespace_='', name_='AllowedValueListType'): 1318 | pass 1319 | def exportChildren(self, outfile, level, namespace_='', name_='AllowedValueListType', fromsubclass_=False, pretty_print=True): 1320 | if pretty_print: 1321 | eol_ = '\n' 1322 | else: 1323 | eol_ = '' 1324 | for allowedValue_ in self.allowedValue: 1325 | showIndent(outfile, level, pretty_print) 1326 | outfile.write('<%sallowedValue>%s%s' % (namespace_, self.gds_format_string(quote_xml(allowedValue_).encode(ExternalEncoding), input_name='allowedValue'), namespace_, eol_)) 1327 | def hasContent_(self): 1328 | if ( 1329 | self.allowedValue 1330 | ): 1331 | return True 1332 | else: 1333 | return False 1334 | def exportLiteral(self, outfile, level, name_='AllowedValueListType'): 1335 | level += 1 1336 | self.exportLiteralAttributes(outfile, level, [], name_) 1337 | if self.hasContent_(): 1338 | self.exportLiteralChildren(outfile, level, name_) 1339 | def exportLiteralAttributes(self, outfile, level, already_processed, name_): 1340 | pass 1341 | def exportLiteralChildren(self, outfile, level, name_): 1342 | showIndent(outfile, level) 1343 | outfile.write('allowedValue=[\n') 1344 | level += 1 1345 | for allowedValue_ in self.allowedValue: 1346 | showIndent(outfile, level) 1347 | outfile.write('%s,\n' % quote_python(allowedValue_).encode(ExternalEncoding)) 1348 | level -= 1 1349 | showIndent(outfile, level) 1350 | outfile.write('],\n') 1351 | def build(self, node): 1352 | self.buildAttributes(node, node.attrib, []) 1353 | for child in node: 1354 | nodeName_ = Tag_pattern_.match(child.tag).groups()[-1] 1355 | self.buildChildren(child, node, nodeName_) 1356 | def buildAttributes(self, node, attrs, already_processed): 1357 | pass 1358 | def buildChildren(self, child_, node, nodeName_, fromsubclass_=False): 1359 | if nodeName_ == 'allowedValue': 1360 | allowedValue_ = child_.text 1361 | allowedValue_ = self.gds_validate_string(allowedValue_, node, 'allowedValue') 1362 | self.allowedValue.append(allowedValue_) 1363 | # end class AllowedValueListType 1364 | 1365 | 1366 | class AllowedValueRangeType(GeneratedsSuper): 1367 | subclass = None 1368 | superclass = None 1369 | def __init__(self, minimum=None, maximum=None, step=None): 1370 | self.minimum = minimum 1371 | self.maximum = maximum 1372 | self.step = step 1373 | def factory(*args_, **kwargs_): 1374 | if AllowedValueRangeType.subclass: 1375 | return AllowedValueRangeType.subclass(*args_, **kwargs_) 1376 | else: 1377 | return AllowedValueRangeType(*args_, **kwargs_) 1378 | factory = staticmethod(factory) 1379 | def get_minimum(self): return self.minimum 1380 | def set_minimum(self, minimum): self.minimum = minimum 1381 | def get_maximum(self): return self.maximum 1382 | def set_maximum(self, maximum): self.maximum = maximum 1383 | def get_step(self): return self.step 1384 | def set_step(self, step): self.step = step 1385 | def export(self, outfile, level, namespace_='', name_='AllowedValueRangeType', namespacedef_='', pretty_print=True): 1386 | if pretty_print: 1387 | eol_ = '\n' 1388 | else: 1389 | eol_ = '' 1390 | showIndent(outfile, level, pretty_print) 1391 | outfile.write('<%s%s%s' % (namespace_, name_, namespacedef_ and ' ' + namespacedef_ or '', )) 1392 | already_processed = [] 1393 | self.exportAttributes(outfile, level, already_processed, namespace_, name_='AllowedValueRangeType') 1394 | if self.hasContent_(): 1395 | outfile.write('>%s' % (eol_, )) 1396 | self.exportChildren(outfile, level + 1, namespace_, name_, pretty_print=pretty_print) 1397 | showIndent(outfile, level, pretty_print) 1398 | outfile.write('%s' % (namespace_, name_, eol_)) 1399 | else: 1400 | outfile.write('/>%s' % (eol_, )) 1401 | def exportAttributes(self, outfile, level, already_processed, namespace_='', name_='AllowedValueRangeType'): 1402 | pass 1403 | def exportChildren(self, outfile, level, namespace_='', name_='AllowedValueRangeType', fromsubclass_=False, pretty_print=True): 1404 | if pretty_print: 1405 | eol_ = '\n' 1406 | else: 1407 | eol_ = '' 1408 | if self.minimum is not None: 1409 | showIndent(outfile, level, pretty_print) 1410 | outfile.write('<%sminimum>%s%s' % (namespace_, self.gds_format_float(self.minimum, input_name='minimum'), namespace_, eol_)) 1411 | if self.maximum is not None: 1412 | showIndent(outfile, level, pretty_print) 1413 | outfile.write('<%smaximum>%s%s' % (namespace_, self.gds_format_float(self.maximum, input_name='maximum'), namespace_, eol_)) 1414 | if self.step is not None: 1415 | showIndent(outfile, level, pretty_print) 1416 | outfile.write('<%sstep>%s%s' % (namespace_, self.gds_format_float(self.step, input_name='step'), namespace_, eol_)) 1417 | def hasContent_(self): 1418 | if ( 1419 | self.minimum is not None or 1420 | self.maximum is not None or 1421 | self.step is not None 1422 | ): 1423 | return True 1424 | else: 1425 | return False 1426 | def exportLiteral(self, outfile, level, name_='AllowedValueRangeType'): 1427 | level += 1 1428 | self.exportLiteralAttributes(outfile, level, [], name_) 1429 | if self.hasContent_(): 1430 | self.exportLiteralChildren(outfile, level, name_) 1431 | def exportLiteralAttributes(self, outfile, level, already_processed, name_): 1432 | pass 1433 | def exportLiteralChildren(self, outfile, level, name_): 1434 | if self.minimum is not None: 1435 | showIndent(outfile, level) 1436 | outfile.write('minimum=%f,\n' % self.minimum) 1437 | if self.maximum is not None: 1438 | showIndent(outfile, level) 1439 | outfile.write('maximum=%f,\n' % self.maximum) 1440 | if self.step is not None: 1441 | showIndent(outfile, level) 1442 | outfile.write('step=%f,\n' % self.step) 1443 | def build(self, node): 1444 | self.buildAttributes(node, node.attrib, []) 1445 | for child in node: 1446 | nodeName_ = Tag_pattern_.match(child.tag).groups()[-1] 1447 | self.buildChildren(child, node, nodeName_) 1448 | def buildAttributes(self, node, attrs, already_processed): 1449 | pass 1450 | def buildChildren(self, child_, node, nodeName_, fromsubclass_=False): 1451 | if nodeName_ == 'minimum': 1452 | sval_ = child_.text 1453 | try: 1454 | fval_ = float(sval_) 1455 | except (TypeError, ValueError) as exp: 1456 | raise_parse_error(child_, 'requires float or double: %s' % exp) 1457 | fval_ = self.gds_validate_float(fval_, node, 'minimum') 1458 | self.minimum = fval_ 1459 | elif nodeName_ == 'maximum': 1460 | sval_ = child_.text 1461 | try: 1462 | fval_ = float(sval_) 1463 | except (TypeError, ValueError) as exp: 1464 | raise_parse_error(child_, 'requires float or double: %s' % exp) 1465 | fval_ = self.gds_validate_float(fval_, node, 'maximum') 1466 | self.maximum = fval_ 1467 | elif nodeName_ == 'step': 1468 | sval_ = child_.text 1469 | try: 1470 | fval_ = float(sval_) 1471 | except (TypeError, ValueError) as exp: 1472 | raise_parse_error(child_, 'requires float or double: %s' % exp) 1473 | fval_ = self.gds_validate_float(fval_, node, 'step') 1474 | self.step = fval_ 1475 | # end class AllowedValueRangeType 1476 | 1477 | 1478 | class retvalType(GeneratedsSuper): 1479 | subclass = None 1480 | superclass = None 1481 | def __init__(self): 1482 | pass 1483 | def factory(*args_, **kwargs_): 1484 | if retvalType.subclass: 1485 | return retvalType.subclass(*args_, **kwargs_) 1486 | else: 1487 | return retvalType(*args_, **kwargs_) 1488 | factory = staticmethod(factory) 1489 | def export(self, outfile, level, namespace_='', name_='retvalType', namespacedef_='', pretty_print=True): 1490 | if pretty_print: 1491 | eol_ = '\n' 1492 | else: 1493 | eol_ = '' 1494 | showIndent(outfile, level, pretty_print) 1495 | outfile.write('<%s%s%s' % (namespace_, name_, namespacedef_ and ' ' + namespacedef_ or '', )) 1496 | already_processed = [] 1497 | self.exportAttributes(outfile, level, already_processed, namespace_, name_='retvalType') 1498 | if self.hasContent_(): 1499 | outfile.write('>%s' % (eol_, )) 1500 | self.exportChildren(outfile, level + 1, namespace_, name_, pretty_print=pretty_print) 1501 | outfile.write('%s' % (namespace_, name_, eol_)) 1502 | else: 1503 | outfile.write('/>%s' % (eol_, )) 1504 | def exportAttributes(self, outfile, level, already_processed, namespace_='', name_='retvalType'): 1505 | pass 1506 | def exportChildren(self, outfile, level, namespace_='', name_='retvalType', fromsubclass_=False, pretty_print=True): 1507 | pass 1508 | def hasContent_(self): 1509 | if ( 1510 | 1511 | ): 1512 | return True 1513 | else: 1514 | return False 1515 | def exportLiteral(self, outfile, level, name_='retvalType'): 1516 | level += 1 1517 | self.exportLiteralAttributes(outfile, level, [], name_) 1518 | if self.hasContent_(): 1519 | self.exportLiteralChildren(outfile, level, name_) 1520 | def exportLiteralAttributes(self, outfile, level, already_processed, name_): 1521 | pass 1522 | def exportLiteralChildren(self, outfile, level, name_): 1523 | pass 1524 | def build(self, node): 1525 | self.buildAttributes(node, node.attrib, []) 1526 | for child in node: 1527 | nodeName_ = Tag_pattern_.match(child.tag).groups()[-1] 1528 | self.buildChildren(child, node, nodeName_) 1529 | def buildAttributes(self, node, attrs, already_processed): 1530 | pass 1531 | def buildChildren(self, child_, node, nodeName_, fromsubclass_=False): 1532 | pass 1533 | # end class retvalType 1534 | 1535 | 1536 | GDSClassesMapping = { 1537 | 'argumentList': ArgumentListType, 1538 | 'actionList': ActionListType, 1539 | 'retval': retvalType, 1540 | 'stateVariable': StateVariableType, 1541 | 'argument': ArgumentType, 1542 | 'action': ActionType, 1543 | 'serviceStateTable': ServiceStateTableType, 1544 | 'allowedValueRange': AllowedValueRangeType, 1545 | 'specVersion': SpecVersionType, 1546 | 'allowedValueList': AllowedValueListType, 1547 | } 1548 | 1549 | 1550 | USAGE_TEXT = """ 1551 | Usage: python .py [ -s ] 1552 | """ 1553 | 1554 | def usage(): 1555 | print(USAGE_TEXT) 1556 | sys.exit(1) 1557 | 1558 | 1559 | def get_root_tag(node): 1560 | tag = Tag_pattern_.match(node.tag).groups()[-1] 1561 | rootClass = GDSClassesMapping.get(tag) 1562 | if rootClass is None: 1563 | rootClass = globals().get(tag) 1564 | return tag, rootClass 1565 | 1566 | 1567 | def parse(inFileName): 1568 | doc = parsexml_(inFileName) 1569 | rootNode = doc.getroot() 1570 | rootTag, rootClass = get_root_tag(rootNode) 1571 | if rootClass is None: 1572 | rootTag = 'scpd' 1573 | rootClass = scpd 1574 | rootObj = rootClass.factory() 1575 | rootObj.build(rootNode) 1576 | # Enable Python to collect the space used by the DOM. 1577 | doc = None 1578 | return rootObj 1579 | 1580 | 1581 | def parseString(inString): 1582 | from io import BytesIO 1583 | doc = parsexml_(BytesIO(inString)) 1584 | rootNode = doc.getroot() 1585 | rootTag, rootClass = get_root_tag(rootNode) 1586 | if rootClass is None: 1587 | rootTag = 'scpd' 1588 | rootClass = scpd 1589 | rootObj = rootClass.factory() 1590 | rootObj.build(rootNode) 1591 | # Enable Python to collect the space used by the DOM. 1592 | doc = None 1593 | return rootObj 1594 | 1595 | 1596 | def parseLiteral(inFileName): 1597 | doc = parsexml_(inFileName) 1598 | rootNode = doc.getroot() 1599 | rootTag, rootClass = get_root_tag(rootNode) 1600 | if rootClass is None: 1601 | rootTag = 'scpd' 1602 | rootClass = scpd 1603 | rootObj = rootClass.factory() 1604 | rootObj.build(rootNode) 1605 | # Enable Python to collect the space used by the DOM. 1606 | doc = None 1607 | sys.stdout.write('#from service import *\n\n') 1608 | sys.stdout.write('from datetime import datetime as datetime_\n\n') 1609 | sys.stdout.write('import service as model_\n\n') 1610 | sys.stdout.write('rootObj = model_.rootTag(\n') 1611 | rootObj.exportLiteral(sys.stdout, 0, name_=rootTag) 1612 | sys.stdout.write(')\n') 1613 | return rootObj 1614 | 1615 | 1616 | def main(): 1617 | args = sys.argv[1:] 1618 | if len(args) == 1: 1619 | parse(args[0]) 1620 | else: 1621 | usage() 1622 | 1623 | 1624 | if __name__ == '__main__': 1625 | #import pdb; pdb.set_trace() 1626 | main() 1627 | 1628 | 1629 | __all__ = [ 1630 | "ActionListType", 1631 | "ActionType", 1632 | "AllowedValueListType", 1633 | "AllowedValueRangeType", 1634 | "ArgumentListType", 1635 | "ArgumentType", 1636 | "ServiceStateTableType", 1637 | "SpecVersionType", 1638 | "StateVariableType", 1639 | "retvalType", 1640 | "scpd" 1641 | ] 1642 | --------------------------------------------------------------------------------