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.
Files changed (40) hide show
  1. pywemo/README.md +69 -0
  2. pywemo/__init__.py +33 -0
  3. pywemo/color.py +79 -0
  4. pywemo/discovery.py +194 -0
  5. pywemo/exceptions.py +94 -0
  6. pywemo/ouimeaux_device/LICENSE +12 -0
  7. pywemo/ouimeaux_device/__init__.py +679 -0
  8. pywemo/ouimeaux_device/api/__init__.py +1 -0
  9. pywemo/ouimeaux_device/api/attributes.py +131 -0
  10. pywemo/ouimeaux_device/api/db_orm.py +197 -0
  11. pywemo/ouimeaux_device/api/long_press.py +168 -0
  12. pywemo/ouimeaux_device/api/rules_db.py +467 -0
  13. pywemo/ouimeaux_device/api/service.py +363 -0
  14. pywemo/ouimeaux_device/api/wemo_services.py +25 -0
  15. pywemo/ouimeaux_device/api/wemo_services.pyi +241 -0
  16. pywemo/ouimeaux_device/api/xsd/__init__.py +1 -0
  17. pywemo/ouimeaux_device/api/xsd/device.py +3888 -0
  18. pywemo/ouimeaux_device/api/xsd/device.xsd +95 -0
  19. pywemo/ouimeaux_device/api/xsd/service.py +3872 -0
  20. pywemo/ouimeaux_device/api/xsd/service.xsd +93 -0
  21. pywemo/ouimeaux_device/api/xsd_types.py +222 -0
  22. pywemo/ouimeaux_device/bridge.py +506 -0
  23. pywemo/ouimeaux_device/coffeemaker.py +92 -0
  24. pywemo/ouimeaux_device/crockpot.py +157 -0
  25. pywemo/ouimeaux_device/dimmer.py +70 -0
  26. pywemo/ouimeaux_device/humidifier.py +223 -0
  27. pywemo/ouimeaux_device/insight.py +191 -0
  28. pywemo/ouimeaux_device/lightswitch.py +11 -0
  29. pywemo/ouimeaux_device/maker.py +54 -0
  30. pywemo/ouimeaux_device/motion.py +6 -0
  31. pywemo/ouimeaux_device/outdoor_plug.py +6 -0
  32. pywemo/ouimeaux_device/switch.py +32 -0
  33. pywemo/py.typed +0 -0
  34. pywemo/ssdp.py +372 -0
  35. pywemo/subscribe.py +782 -0
  36. pywemo/util.py +139 -0
  37. pywemo-1.4.0.dist-info/LICENSE +54 -0
  38. pywemo-1.4.0.dist-info/METADATA +192 -0
  39. pywemo-1.4.0.dist-info/RECORD +40 -0
  40. 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.