pywemo 1.4.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pywemo/README.md +69 -0
- pywemo/__init__.py +33 -0
- pywemo/color.py +79 -0
- pywemo/discovery.py +194 -0
- pywemo/exceptions.py +94 -0
- pywemo/ouimeaux_device/LICENSE +12 -0
- pywemo/ouimeaux_device/__init__.py +679 -0
- pywemo/ouimeaux_device/api/__init__.py +1 -0
- pywemo/ouimeaux_device/api/attributes.py +131 -0
- pywemo/ouimeaux_device/api/db_orm.py +197 -0
- pywemo/ouimeaux_device/api/long_press.py +168 -0
- pywemo/ouimeaux_device/api/rules_db.py +467 -0
- pywemo/ouimeaux_device/api/service.py +363 -0
- pywemo/ouimeaux_device/api/wemo_services.py +25 -0
- pywemo/ouimeaux_device/api/wemo_services.pyi +241 -0
- pywemo/ouimeaux_device/api/xsd/__init__.py +1 -0
- pywemo/ouimeaux_device/api/xsd/device.py +3888 -0
- pywemo/ouimeaux_device/api/xsd/device.xsd +95 -0
- pywemo/ouimeaux_device/api/xsd/service.py +3872 -0
- pywemo/ouimeaux_device/api/xsd/service.xsd +93 -0
- pywemo/ouimeaux_device/api/xsd_types.py +222 -0
- pywemo/ouimeaux_device/bridge.py +506 -0
- pywemo/ouimeaux_device/coffeemaker.py +92 -0
- pywemo/ouimeaux_device/crockpot.py +157 -0
- pywemo/ouimeaux_device/dimmer.py +70 -0
- pywemo/ouimeaux_device/humidifier.py +223 -0
- pywemo/ouimeaux_device/insight.py +191 -0
- pywemo/ouimeaux_device/lightswitch.py +11 -0
- pywemo/ouimeaux_device/maker.py +54 -0
- pywemo/ouimeaux_device/motion.py +6 -0
- pywemo/ouimeaux_device/outdoor_plug.py +6 -0
- pywemo/ouimeaux_device/switch.py +32 -0
- pywemo/py.typed +0 -0
- pywemo/ssdp.py +372 -0
- pywemo/subscribe.py +782 -0
- pywemo/util.py +139 -0
- pywemo-1.4.0.dist-info/LICENSE +54 -0
- pywemo-1.4.0.dist-info/METADATA +192 -0
- pywemo-1.4.0.dist-info/RECORD +40 -0
- pywemo-1.4.0.dist-info/WHEEL +4 -0
pywemo/README.md
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# API Documentation
|
|
2
|
+
|
|
3
|
+
For example usage and installation instructions see
|
|
4
|
+
[README.rst](https://github.com/pywemo/pywemo/blob/main/README.rst)
|
|
5
|
+
|
|
6
|
+
## General structure of the pyWeMo API
|
|
7
|
+
|
|
8
|
+
### Discovery
|
|
9
|
+
|
|
10
|
+
The `pywemo.discovery` module contains methods to locate WeMo devices on a
|
|
11
|
+
network. For example, use the following to discover all devices on the
|
|
12
|
+
network:
|
|
13
|
+
|
|
14
|
+
```python
|
|
15
|
+
>>> import pywemo
|
|
16
|
+
>>> devices = pywemo.discover_devices()
|
|
17
|
+
>>> print(devices)
|
|
18
|
+
[<WeMo Insight "AC Insight">]
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Or, if you know the IP address of the device, use this example.
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
>>> import pywemo
|
|
25
|
+
>>> url = pywemo.setup_url_for_address("192.168.1.192")
|
|
26
|
+
>>> print(url)
|
|
27
|
+
http://192.168.1.192:49153/setup.xml
|
|
28
|
+
>>> device = pywemo.device_from_description(url)
|
|
29
|
+
>>> print(device)
|
|
30
|
+
[<WeMo Insight "AC Insight">]
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### Devices
|
|
34
|
+
|
|
35
|
+
The device(s) returned by the discovery methods above will be instances of one
|
|
36
|
+
of the classes below. These classes, used for communicating with the various
|
|
37
|
+
WeMo devices, are in submodules under the `pywemo.ouimeaux_device` module. They
|
|
38
|
+
can also be accessed as top-level members of the pywemo module.
|
|
39
|
+
|
|
40
|
+
WeMo Model|Alias / Class
|
|
41
|
+
----------|-------------
|
|
42
|
+
F7C031 |`pywemo.Bridge` / `pywemo.ouimeaux_device.bridge.Bridge`
|
|
43
|
+
F7C050 |`pywemo.CoffeeMaker` / `pywemo.ouimeaux_device.coffeemaker.CoffeeMaker`
|
|
44
|
+
F7C045 |`pywemo.CrockPot` / `pywemo.ouimeaux_device.crockpot.CrockPot`
|
|
45
|
+
F7C059 |`pywemo.DimmerLongPress` / `pywemo.ouimeaux_device.dimmer.DimmerLongPress`
|
|
46
|
+
WDS060 |`pywemo.DimmerV2` / `pywemo.ouimeaux_device.dimmer.DimmerV2`
|
|
47
|
+
F7C046 |`pywemo.Humidifier` / `pywemo.ouimeaux_device.humidifier.Humidifier`
|
|
48
|
+
F7C029 |`pywemo.Insight` / `pywemo.ouimeaux_device.insight.Insight`
|
|
49
|
+
F7C030 |`pywemo.LightSwitchLongPress` / `pywemo.ouimeaux_device.lightswitch.LightSwitchLongPress`
|
|
50
|
+
WLS040 |`pywemo.LightSwitchLongPress` / `pywemo.ouimeaux_device.lightswitch.LightSwitchLongPress`
|
|
51
|
+
WLS0403 |`pywemo.LightSwitchLongPress` / `pywemo.ouimeaux_device.lightswitch.LightSwitchLongPress`
|
|
52
|
+
F7C043 |`pywemo.Maker` / `pywemo.ouimeaux_device.maker.Maker`
|
|
53
|
+
F7C028 |`pywemo.Motion` / `pywemo.ouimeaux_device.motion.Motion`
|
|
54
|
+
WSP090 |`pywemo.OutdoorPlug` / `pywemo.ouimeaux_device.outdoor_plug.OutdoorPlug`
|
|
55
|
+
F7C027 |`pywemo.Switch` / `pywemo.ouimeaux_device.switch.Switch`
|
|
56
|
+
F7C063 |`pywemo.Switch` / `pywemo.ouimeaux_device.switch.Switch`
|
|
57
|
+
WSP080 |`pywemo.Switch` / `pywemo.ouimeaux_device.switch.Switch`
|
|
58
|
+
|
|
59
|
+
The following are base classes of all of the above device classes.
|
|
60
|
+
|
|
61
|
+
* pywemo.ouimeaux_device.Device: Provides common methods for getting/setting
|
|
62
|
+
device state.
|
|
63
|
+
* pywemo.ouimeaux_device.api.xsd_types.DeviceDescription: Provides information
|
|
64
|
+
about the device name, mac address, firmware version, serial number, etc.
|
|
65
|
+
|
|
66
|
+
### Subscriptions
|
|
67
|
+
|
|
68
|
+
Most WeMo devices support a push/callback model for reporting state changes.
|
|
69
|
+
The `pywemo.subscribe` module provides a way to subscribe to push events.
|
pywemo/__init__.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
r"""Lightweight Python module to discover and control WeMo devices.
|
|
2
|
+
.. include:: README.md
|
|
3
|
+
"""
|
|
4
|
+
# flake8: noqa F401
|
|
5
|
+
|
|
6
|
+
from .discovery import (
|
|
7
|
+
device_from_description,
|
|
8
|
+
discover_devices,
|
|
9
|
+
setup_url_for_address,
|
|
10
|
+
)
|
|
11
|
+
from .exceptions import PyWeMoException
|
|
12
|
+
from .ouimeaux_device import Device as WeMoDevice
|
|
13
|
+
from .ouimeaux_device.api.long_press import LongPressMixin
|
|
14
|
+
from .ouimeaux_device.api.service import Action, Service
|
|
15
|
+
from .ouimeaux_device.bridge import Bridge
|
|
16
|
+
from .ouimeaux_device.bridge import Group as BridgeGroup
|
|
17
|
+
from .ouimeaux_device.bridge import Light as BridgeLight
|
|
18
|
+
from .ouimeaux_device.coffeemaker import CoffeeMaker, CoffeeMakerMode
|
|
19
|
+
from .ouimeaux_device.crockpot import CrockPot, CrockPotMode
|
|
20
|
+
from .ouimeaux_device.dimmer import Dimmer, DimmerLongPress, DimmerV2
|
|
21
|
+
from .ouimeaux_device.humidifier import (
|
|
22
|
+
DesiredHumidity,
|
|
23
|
+
FanMode,
|
|
24
|
+
Humidifier,
|
|
25
|
+
WaterLevel,
|
|
26
|
+
)
|
|
27
|
+
from .ouimeaux_device.insight import Insight, StandbyState
|
|
28
|
+
from .ouimeaux_device.lightswitch import LightSwitch, LightSwitchLongPress
|
|
29
|
+
from .ouimeaux_device.maker import Maker
|
|
30
|
+
from .ouimeaux_device.motion import Motion
|
|
31
|
+
from .ouimeaux_device.outdoor_plug import OutdoorPlug
|
|
32
|
+
from .ouimeaux_device.switch import Switch
|
|
33
|
+
from .subscribe import SubscriptionRegistry
|
pywemo/color.py
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Various utilities for handling colors."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from typing import Tuple
|
|
5
|
+
|
|
6
|
+
# Define usable ranges as bulbs either ignore or behave unexpectedly
|
|
7
|
+
# when it is sent a value is outside of the range.
|
|
8
|
+
TemperatureRange = Tuple[int, int]
|
|
9
|
+
TEMPERATURE_PROFILES: dict[str, TemperatureRange] = dict(
|
|
10
|
+
(model, temp)
|
|
11
|
+
for models, temp in (
|
|
12
|
+
# Lightify RGBW, 1900-6500K
|
|
13
|
+
(["LIGHTIFY A19 RGBW"], (151, 555)),
|
|
14
|
+
)
|
|
15
|
+
for model in models
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
ColorXY = Tuple[float, float]
|
|
19
|
+
ColorGamut = Tuple[ColorXY, ColorXY, ColorXY]
|
|
20
|
+
COLOR_PROFILES: dict[str, ColorGamut] = dict(
|
|
21
|
+
(model, gamut)
|
|
22
|
+
for models, gamut in (
|
|
23
|
+
# Lightify RGBW, 1900-6500K
|
|
24
|
+
# https://flow-morewithless.blogspot.com/2015/01/osram-lightify-color-gamut-and-spectrum.html
|
|
25
|
+
(
|
|
26
|
+
["LIGHTIFY A19 RGBW"],
|
|
27
|
+
((0.683924, 0.315904), (0.391678, 0.501414), (0.136990, 0.051035)),
|
|
28
|
+
),
|
|
29
|
+
)
|
|
30
|
+
for model in models
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def get_profiles(model: str) -> tuple[TemperatureRange, ColorGamut]:
|
|
35
|
+
"""Return the temperature and color profiles for a given model."""
|
|
36
|
+
return (
|
|
37
|
+
TEMPERATURE_PROFILES.get(model, (150, 600)),
|
|
38
|
+
COLOR_PROFILES.get(model, ((1.0, 0.0), (0.0, 1.0), (0.0, 0.0))),
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def is_same_side(p1: ColorXY, p2: ColorXY, a: ColorXY, b: ColorXY) -> bool:
|
|
43
|
+
"""Test if points p1 and p2 lie on the same side of line a-b."""
|
|
44
|
+
# pylint: disable=invalid-name
|
|
45
|
+
vector_ab = [y - x for x, y in zip(a, b)]
|
|
46
|
+
vector_ap1 = [y - x for x, y in zip(a, p1)]
|
|
47
|
+
vector_ap2 = [y - x for x, y in zip(a, p2)]
|
|
48
|
+
cross_vab_ap1 = vector_ab[0] * vector_ap1[1] - vector_ab[1] * vector_ap1[0]
|
|
49
|
+
cross_vab_ap2 = vector_ab[0] * vector_ap2[1] - vector_ab[1] * vector_ap2[0]
|
|
50
|
+
return (cross_vab_ap1 * cross_vab_ap2) >= 0
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def closest_point(p: ColorXY, a: ColorXY, b: ColorXY) -> ColorXY:
|
|
54
|
+
"""Test if points p1 and p2 lie on the same side of line a-b."""
|
|
55
|
+
# pylint: disable=invalid-name
|
|
56
|
+
vector_ab = [y - x for x, y in zip(a, b)]
|
|
57
|
+
vector_ap = [y - x for x, y in zip(a, p)]
|
|
58
|
+
dot_ap_ab = sum(x * y for x, y in zip(vector_ap, vector_ab))
|
|
59
|
+
dot_ab_ab = sum(x * y for x, y in zip(vector_ab, vector_ab))
|
|
60
|
+
t = max(0.0, min(dot_ap_ab / dot_ab_ab, 1.0))
|
|
61
|
+
return a[0] + vector_ab[0] * t, a[1] + vector_ab[1] * t
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def limit_to_gamut(xy: ColorXY, gamut: ColorGamut) -> ColorXY:
|
|
65
|
+
"""Return the closest point within the gamut triangle for colorxy."""
|
|
66
|
+
# pylint: disable=invalid-name
|
|
67
|
+
r, g, b = gamut
|
|
68
|
+
|
|
69
|
+
# http://www.blackpawn.com/texts/pointinpoly/
|
|
70
|
+
if not is_same_side(xy, r, g, b):
|
|
71
|
+
xy = closest_point(xy, g, b)
|
|
72
|
+
|
|
73
|
+
if not is_same_side(xy, g, b, r):
|
|
74
|
+
xy = closest_point(xy, b, r)
|
|
75
|
+
|
|
76
|
+
if not is_same_side(xy, b, r, g):
|
|
77
|
+
xy = closest_point(xy, r, g)
|
|
78
|
+
|
|
79
|
+
return xy
|
pywemo/discovery.py
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"""Module to discover WeMo devices."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import logging
|
|
5
|
+
from ipaddress import ip_address
|
|
6
|
+
from socket import gaierror, gethostbyname
|
|
7
|
+
from typing import Any, Callable
|
|
8
|
+
|
|
9
|
+
import requests
|
|
10
|
+
|
|
11
|
+
from . import ssdp
|
|
12
|
+
from .exceptions import (
|
|
13
|
+
InvalidSchemaError,
|
|
14
|
+
MissingServiceError,
|
|
15
|
+
PyWeMoException,
|
|
16
|
+
)
|
|
17
|
+
from .ouimeaux_device import Device, UnsupportedDevice, probe_wemo
|
|
18
|
+
from .ouimeaux_device.api.service import REQUESTS_TIMEOUT
|
|
19
|
+
from .ouimeaux_device.api.xsd_types import DeviceDescription
|
|
20
|
+
from .ouimeaux_device.bridge import Bridge
|
|
21
|
+
from .ouimeaux_device.coffeemaker import CoffeeMaker
|
|
22
|
+
from .ouimeaux_device.crockpot import CrockPot
|
|
23
|
+
from .ouimeaux_device.dimmer import Dimmer, DimmerLongPress, DimmerV2
|
|
24
|
+
from .ouimeaux_device.humidifier import Humidifier
|
|
25
|
+
from .ouimeaux_device.insight import Insight
|
|
26
|
+
from .ouimeaux_device.lightswitch import LightSwitch, LightSwitchLongPress
|
|
27
|
+
from .ouimeaux_device.maker import Maker
|
|
28
|
+
from .ouimeaux_device.motion import Motion
|
|
29
|
+
from .ouimeaux_device.outdoor_plug import OutdoorPlug
|
|
30
|
+
from .ouimeaux_device.switch import Switch
|
|
31
|
+
|
|
32
|
+
LOG = logging.getLogger(__name__)
|
|
33
|
+
_uuid_seen = set() # See _call_once_per_uuid.
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _call_once_per_uuid(
|
|
37
|
+
uuid: str, method: Callable[..., Any], *args: Any, **kwargs: Any
|
|
38
|
+
) -> None:
|
|
39
|
+
key = (uuid, method)
|
|
40
|
+
if key in _uuid_seen:
|
|
41
|
+
return
|
|
42
|
+
_uuid_seen.add(key)
|
|
43
|
+
method(*args, *kwargs)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def discover_devices(*, debug: bool = False, **kwargs: Any) -> list[Device]:
|
|
47
|
+
"""Find WeMo devices on the local network."""
|
|
48
|
+
devices = (
|
|
49
|
+
device_from_uuid_and_location(entry.udn, entry.location, debug=debug)
|
|
50
|
+
for entry in ssdp.scan(**kwargs)
|
|
51
|
+
)
|
|
52
|
+
return [d for d in devices if d is not None]
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def device_from_description(
|
|
56
|
+
description_url: str, *, debug: bool = False
|
|
57
|
+
) -> Device | None:
|
|
58
|
+
"""Return object representing WeMo device running at host, else None."""
|
|
59
|
+
try:
|
|
60
|
+
xml = requests.get(description_url, timeout=REQUESTS_TIMEOUT)
|
|
61
|
+
except requests.RequestException:
|
|
62
|
+
LOG.exception("Failed to fetch description %s", description_url)
|
|
63
|
+
return None
|
|
64
|
+
|
|
65
|
+
try:
|
|
66
|
+
device = DeviceDescription.from_xml(xml.content)
|
|
67
|
+
except PyWeMoException:
|
|
68
|
+
LOG.exception("Failed to parse description %s", description_url)
|
|
69
|
+
return None
|
|
70
|
+
|
|
71
|
+
return device_from_uuid_and_location(
|
|
72
|
+
device.udn, description_url, debug=debug
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def device_from_uuid_and_location( # noqa: C901
|
|
77
|
+
# pylint: disable=too-many-branches,too-many-return-statements
|
|
78
|
+
uuid: str | None,
|
|
79
|
+
location: str | None,
|
|
80
|
+
*,
|
|
81
|
+
debug: bool = False,
|
|
82
|
+
) -> Device | None:
|
|
83
|
+
"""Determine device class based on the device uuid."""
|
|
84
|
+
if not (uuid and location):
|
|
85
|
+
return None
|
|
86
|
+
try:
|
|
87
|
+
if uuid.startswith("uuid:Socket"):
|
|
88
|
+
return Switch(location)
|
|
89
|
+
if uuid.startswith("uuid:Lightswitch-1_0"):
|
|
90
|
+
if not location.endswith("/setup.xml"):
|
|
91
|
+
return LightSwitch(location)
|
|
92
|
+
return LightSwitchLongPress(location)
|
|
93
|
+
if uuid.startswith("uuid:Lightswitch-2_0"):
|
|
94
|
+
return LightSwitchLongPress(location)
|
|
95
|
+
if uuid.startswith("uuid:Lightswitch-3_0"):
|
|
96
|
+
return LightSwitchLongPress(location)
|
|
97
|
+
if uuid.startswith("uuid:Lightswitch"):
|
|
98
|
+
return LightSwitch(location)
|
|
99
|
+
if uuid.startswith("uuid:Dimmer-1_0"):
|
|
100
|
+
return DimmerLongPress(location)
|
|
101
|
+
if uuid.startswith("uuid:Dimmer-2_0"):
|
|
102
|
+
return DimmerV2(location)
|
|
103
|
+
if uuid.startswith("uuid:Dimmer"):
|
|
104
|
+
return Dimmer(location)
|
|
105
|
+
if uuid.startswith("uuid:Insight"):
|
|
106
|
+
return Insight(location)
|
|
107
|
+
if uuid.startswith("uuid:Sensor"):
|
|
108
|
+
return Motion(location)
|
|
109
|
+
if uuid.startswith("uuid:Maker"):
|
|
110
|
+
return Maker(location)
|
|
111
|
+
if uuid.startswith("uuid:Bridge"):
|
|
112
|
+
return Bridge(location)
|
|
113
|
+
if uuid.startswith("uuid:CoffeeMaker"):
|
|
114
|
+
return CoffeeMaker(location)
|
|
115
|
+
if uuid.startswith("uuid:Crockpot"):
|
|
116
|
+
return CrockPot(location)
|
|
117
|
+
if uuid.startswith("uuid:Humidifier"):
|
|
118
|
+
return Humidifier(location)
|
|
119
|
+
if uuid.startswith("uuid:OutdoorPlug"):
|
|
120
|
+
return OutdoorPlug(location)
|
|
121
|
+
except (InvalidSchemaError, MissingServiceError) as err:
|
|
122
|
+
_call_once_per_uuid(
|
|
123
|
+
uuid,
|
|
124
|
+
LOG.info,
|
|
125
|
+
"pyWeMo encountered a non-WeMo device %s %s: %r",
|
|
126
|
+
uuid,
|
|
127
|
+
location,
|
|
128
|
+
err,
|
|
129
|
+
)
|
|
130
|
+
# Fall-through: Try UnsupportedDevice if debug is enabled.
|
|
131
|
+
except PyWeMoException:
|
|
132
|
+
_call_once_per_uuid(
|
|
133
|
+
uuid, LOG.exception, "Device setup failed %s %s", uuid, location
|
|
134
|
+
)
|
|
135
|
+
# Fall-through: Try UnsupportedDevice if debug is enabled.
|
|
136
|
+
|
|
137
|
+
if uuid.startswith("uuid:") and debug:
|
|
138
|
+
# unsupported device, but if this function was called from
|
|
139
|
+
# discover_devices then this should be a Belkin product and is probably
|
|
140
|
+
# a WeMo product without a custom class yet. So attempt to return a
|
|
141
|
+
# basic object to allow manual interaction.
|
|
142
|
+
try:
|
|
143
|
+
device = UnsupportedDevice(location)
|
|
144
|
+
except PyWeMoException:
|
|
145
|
+
LOG.exception("Device setup failed %s %s", uuid, location)
|
|
146
|
+
else:
|
|
147
|
+
LOG.info(
|
|
148
|
+
"Device with %s is not supported by pywemo, returning "
|
|
149
|
+
"UnsupportedDevice object to allow manual interaction",
|
|
150
|
+
uuid,
|
|
151
|
+
)
|
|
152
|
+
return device
|
|
153
|
+
|
|
154
|
+
return None
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def hostname_lookup(hostname: str) -> str:
|
|
158
|
+
"""Resolve a hostname into an IP address."""
|
|
159
|
+
try:
|
|
160
|
+
# The {host} must be resolved to an IP address; if this fails, this
|
|
161
|
+
# will throw a socket.gaierror.
|
|
162
|
+
host_address = gethostbyname(hostname)
|
|
163
|
+
|
|
164
|
+
# Reset {host} to the resolved address.
|
|
165
|
+
LOG.debug(
|
|
166
|
+
"Resolved hostname %s to IP address %s.", hostname, host_address
|
|
167
|
+
)
|
|
168
|
+
return host_address
|
|
169
|
+
|
|
170
|
+
except gaierror:
|
|
171
|
+
# The {host}-as-hostname did not resolve to an IP address.
|
|
172
|
+
LOG.debug("Could not resolve hostname %s to an IP address.", hostname)
|
|
173
|
+
return hostname
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def setup_url_for_address(host: str, port: int | None = None) -> str | None:
|
|
177
|
+
"""Determine setup.xml url for a given host and port pair."""
|
|
178
|
+
# Force hostnames into IP addresses
|
|
179
|
+
try:
|
|
180
|
+
# Attempt to register {host} as an IP address; if this fails ({host} is
|
|
181
|
+
# not an IP address), this will throw a ValueError.
|
|
182
|
+
ip_address(host)
|
|
183
|
+
except ValueError:
|
|
184
|
+
# The provided {host} should be treated as a hostname.
|
|
185
|
+
host = hostname_lookup(host)
|
|
186
|
+
|
|
187
|
+
# Automatically determine the port if not provided.
|
|
188
|
+
if not port:
|
|
189
|
+
port = probe_wemo(host)
|
|
190
|
+
|
|
191
|
+
if not port:
|
|
192
|
+
return None
|
|
193
|
+
|
|
194
|
+
return f"http://{host}:{port}/setup.xml"
|
pywemo/exceptions.py
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""Exceptions raised by pywemo."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from lxml import etree as et
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class PyWeMoException(Exception):
|
|
8
|
+
"""Base exception class for pyWeMo exceptions."""
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ActionException(PyWeMoException):
|
|
12
|
+
"""Generic exceptions when dealing with SOAP request Actions."""
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class SOAPFault(ActionException):
|
|
16
|
+
"""Raised when the SOAP response contains a Fault message."""
|
|
17
|
+
|
|
18
|
+
fault_code: str = ""
|
|
19
|
+
fault_string: str = ""
|
|
20
|
+
error_code: str = ""
|
|
21
|
+
error_description: str = ""
|
|
22
|
+
|
|
23
|
+
def __init__(
|
|
24
|
+
self, message: str = "", fault_element: et._Element | None = None
|
|
25
|
+
) -> None:
|
|
26
|
+
"""Initialize from a SOAP Fault lxml.etree Element."""
|
|
27
|
+
details = ""
|
|
28
|
+
if fault_element is not None:
|
|
29
|
+
upnp_error_prefix = (
|
|
30
|
+
"detail"
|
|
31
|
+
"/{urn:schemas-upnp-org:control-1-0}UPnPError/"
|
|
32
|
+
"{urn:schemas-upnp-org:control-1-0}"
|
|
33
|
+
)
|
|
34
|
+
self.fault_code = fault_element.findtext("faultcode", "")
|
|
35
|
+
self.fault_string = fault_element.findtext("faultstring", "")
|
|
36
|
+
self.error_code = fault_element.findtext(
|
|
37
|
+
f"{upnp_error_prefix}errorCode", ""
|
|
38
|
+
)
|
|
39
|
+
self.error_description = fault_element.findtext(
|
|
40
|
+
f"{upnp_error_prefix}errorDescription", ""
|
|
41
|
+
)
|
|
42
|
+
details = (
|
|
43
|
+
f" SOAP Fault {self.fault_code}:{self.fault_string}, "
|
|
44
|
+
f"{self.error_code}:{self.error_description}"
|
|
45
|
+
)
|
|
46
|
+
super().__init__(f"{message}{details}")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class SubscriptionRegistryFailed(PyWeMoException):
|
|
50
|
+
"""General exceptions related to the subscription registry."""
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class UnknownService(PyWeMoException):
|
|
54
|
+
"""Exception raised when a non-existent service is called."""
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class ResetException(PyWeMoException):
|
|
58
|
+
"""Exception raised when reset fails."""
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class SetupException(PyWeMoException):
|
|
62
|
+
"""Exception raised when setup fails."""
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class APNotFound(SetupException):
|
|
66
|
+
"""Exception raised when the AP requested is not found."""
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class ShortPassword(SetupException):
|
|
70
|
+
"""Exception raised when a password is too short (<8 characters)."""
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class HTTPException(PyWeMoException):
|
|
74
|
+
"""HTTP request to the device failed."""
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class HTTPNotOkException(HTTPException):
|
|
78
|
+
"""Raised when a non-200 status is returned."""
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class RulesDbError(PyWeMoException):
|
|
82
|
+
"""Base class for errors related to the Rules database."""
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class RulesDbQueryError(RulesDbError):
|
|
86
|
+
"""Exception when querying the rules database."""
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class InvalidSchemaError(PyWeMoException):
|
|
90
|
+
"""Raised when an unexpected XML response is received."""
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class MissingServiceError(PyWeMoException):
|
|
94
|
+
"""All required services were not found in the device schema."""
|
|
@@ -0,0 +1,12 @@
|
|
|
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.
|