Skip to content

Commit e6a4788

Browse files
committed
Merge pending changes in order to fix release process
2 parents d3bc927 + 8e431c3 commit e6a4788

4 files changed

Lines changed: 120 additions & 63 deletions

File tree

hnap/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,10 @@
1717
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
1818
# USA.
1919

20-
2120
from .devices import Camera, DeviceFactory, Motion, Router, Siren, Sound, Water
2221
from .soapclient import AuthenticationError, MethodCallError
2322

23+
2424
__all__ = [
2525
"AuthenticationError",
2626
"MethodCallError",

hnap/devices.py

Lines changed: 94 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,71 @@
2020

2121
import datetime
2222
import enum
23+
import logging
2324

2425
from .soapclient import MethodCallError, SoapClient
2526

27+
_LOGGER = logging.getLogger(__name__)
2628

27-
def DeviceFactory(hostname, password, username="Admin", port=80, **kwargs):
28-
device = SoapClient(hostname, password, username, port)
29-
device.authenticate()
30-
info = device.device_info()
29+
30+
class Sound(enum.Enum):
31+
EMERGENCY = 1
32+
FIRE = 2
33+
AMBULANCE = 3
34+
POLICE = 4
35+
DOOR_CHIME = 5
36+
BEEP = 6
37+
38+
@classmethod
39+
def fromstring(cls, s):
40+
s = s.upper()
41+
for c in ["-", " ", "."]:
42+
s = s.replace(c, "_")
43+
44+
return getattr(cls, s)
45+
46+
47+
class Device:
48+
def __init__(
49+
self,
50+
*,
51+
client=None,
52+
hostname=None,
53+
password=None,
54+
username="Admin",
55+
port=80,
56+
):
57+
self.client = client or SoapClient(
58+
hostname=hostname, password=password, username=username, port=port
59+
)
60+
self._info = None
61+
62+
def authenticate(self):
63+
self.client.authenticate()
64+
info = dict(self.client.call("GetDeviceSettings"))
65+
for k in ["@xmlns", "SOAPActions", "GetDeviceSettingsResult"]:
66+
info.pop(k, None)
67+
68+
try:
69+
info["ModuleTypes"] = info["ModuleTypes"]["string"]
70+
except KeyError:
71+
pass
72+
73+
self._info = info
74+
75+
@property
76+
def info(self):
77+
return self._info
78+
79+
80+
def DeviceFactory(
81+
*, client=None, hostname=None, password=None, username="Admin", port=80
82+
):
83+
client = client or SoapClient(
84+
hostname=hostname, password=password, username=username, port=port
85+
)
86+
client.authenticate()
87+
info = client.device_info()
3188

3289
module_types = info["ModuleTypes"]
3390
if not isinstance(module_types, list):
@@ -43,47 +100,40 @@ def DeviceFactory(hostname, password, username="Admin", port=80, **kwargs):
43100
else:
44101
raise TypeError(module_types)
45102

46-
return cls(hostname, password, username=username, port=port, **kwargs)
47-
48-
49-
class _Device(SoapClient):
50-
def __init__(self, *args, **kwargs):
51-
super().__init__(*args, **kwargs)
52-
self._info = None
53-
54-
def authenticate(self):
55-
super().authenticate()
56-
self._info = self.device_info()
57-
58-
@property
59-
def info(self):
60-
return self._info
103+
return cls(client=client)
61104

62105

63-
class Camera(_Device):
106+
class Camera(Device):
64107
pass
65108

66109

67-
class Motion(_Device):
68-
def __init__(self, *args, delta=30, **kwargs):
110+
class Motion(Device):
111+
def __init__(self, *args, **kwargs):
69112
super().__init__(*args, **kwargs)
70-
self.delta = delta
113+
self._delta = None
71114

72-
def login(self):
73-
super().login()
115+
@property
116+
def delta(self):
117+
return self._delta
74118

75-
# Auto-adjust delta
76-
res = self.call(
77-
"SetMotionDetectorSettings", ModuleID=1, Backoff=self.delta
119+
@delta.setter
120+
def delta(self, seconds):
121+
self.client.call(
122+
"SetMotionDetectorSettings", ModuleID=1, Backoff=self._delta
78123
)
79-
res = self.call("GetMotionDetectorSettings", ModuleID=1)
124+
_LOGGER.warning("set delta property has no effect")
125+
126+
def authenticate(self):
127+
super().authenticate()
128+
129+
res = self.client.call("GetMotionDetectorSettings", ModuleID=1)
80130
try:
81-
self.delta = int(res["Backoff"])
82-
except (ValueError, TypeError):
83-
self.delta = 30
131+
self._delta = int(res["Backoff"])
132+
except (ValueError, TypeError, KeyError):
133+
_LOGGER.warning("Unable to get delta from device")
84134

85135
def get_latest_detection(self):
86-
res = self.call("GetLatestDetection", ModuleID=1)
136+
res = self.client.call("GetLatestDetection", ModuleID=1)
87137
return datetime.datetime.fromtimestamp(float(res["LatestDetectTime"]))
88138

89139
def is_active(self):
@@ -93,7 +143,7 @@ def is_active(self):
93143
return delta <= self.delta
94144

95145

96-
class Router(SoapClient):
146+
class Router(Device):
97147
# NOT tested
98148
# See https://github.com/waffelheld/dlink-device-tracker/blob/master/custom_components/dlink_device_tracker/dlink_hnap.py#L95
99149

@@ -116,30 +166,15 @@ def get_clients(self):
116166
return ret
117167

118168

119-
class Sound(enum.Enum):
120-
EMERGENCY = 1
121-
FIRE = 2
122-
AMBULANCE = 3
123-
POLICE = 4
124-
DOOR_CHIME = 5
125-
BEEP = 6
126-
127-
@classmethod
128-
def fromstring(cls, s):
129-
s = s.upper()
130-
for c in ["-", " ", "."]:
131-
s = s.replace(c, "_")
132-
133-
return getattr(cls, s)
134-
135-
136-
class Siren(_Device):
169+
class Siren(Device):
137170
def is_playing(self):
138-
res = self.call("GetSirenAlarmSettings", ModuleID=1, Controller=1)
171+
res = self.client.call(
172+
"GetSirenAlarmSettings", ModuleID=1, Controller=1
173+
)
139174
return res["IsSounding"] == "true"
140175

141176
def play(self, sound=Sound.EMERGENCY, volume=100, duration=60):
142-
ret = self.call(
177+
ret = self.client.call(
143178
"SetSoundPlay",
144179
ModuleID=1,
145180
Controller=1,
@@ -151,16 +186,18 @@ def play(self, sound=Sound.EMERGENCY, volume=100, duration=60):
151186
raise MethodCallError(f"Unable to play. Response: {ret}")
152187

153188
def beep(self, volume=100, duration=1):
154-
return self.play(sound=Sound.BEEP, duration=duration, volume=volume)
189+
return self.client.play(
190+
sound=Sound.BEEP, duration=duration, volume=volume
191+
)
155192

156193
def stop(self):
157-
ret = self.call("SetAlarmDismissed", ModuleID=1, Controller=1)
194+
ret = self.client.call("SetAlarmDismissed", ModuleID=1, Controller=1)
158195

159196
if ret["SetAlarmDismissedResult"] != "OK":
160197
raise MethodCallError(f"Unable to stop. Response: {ret}")
161198

162199

163-
class Water(_Device):
200+
class Water(Device):
164201
def is_active(self):
165202
ret = self.call("GetWaterDetectorState", ModuleID=1)
166203
return ret.get("IsWater") == "true"

hnap/soapclient.py

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
# USA.
1919

2020

21+
import functools
2122
import hashlib
2223
import hmac
2324
import logging
@@ -48,19 +49,24 @@ class SoapClient:
4849
"cookie": "",
4950
"private_key": "",
5051
"public_key": "",
51-
"pin": None,
52+
"password": None,
5253
"result": "",
5354
"url": "http://{hostname}:{port}/HNAP1",
5455
"username": None,
5556
}
5657

57-
def __init__(self, hostname, pin, username="admin", port=80):
58+
def __init__(self, hostname, password, username="admin", port=80):
5859
self.HNAP_AUTH = self.HNAP_AUTH.copy()
5960
self.HNAP_AUTH["url"] = self.HNAP_AUTH["url"].format(
6061
hostname=hostname, port=port
6162
)
6263
self.HNAP_AUTH["username"] = username
63-
self.HNAP_AUTH["pin"] = pin
64+
self.HNAP_AUTH["password"] = password
65+
self._authenticated = False
66+
67+
@property
68+
def authenticated(self):
69+
return self._authenticated
6470

6571
def _build_method_envelope(self, method, **parameters):
6672
parameters_xml = "\n".join(
@@ -95,7 +101,7 @@ def _save_login_result(self, body):
95101
self.HNAP_AUTH[key] = elements[0].firstChild.nodeValue
96102

97103
self.HNAP_AUTH["private_key"] = hex_hmac_md5(
98-
self.HNAP_AUTH["public_key"] + self.HNAP_AUTH["pin"],
104+
self.HNAP_AUTH["public_key"] + self.HNAP_AUTH["password"],
99105
self.HNAP_AUTH["challenge"],
100106
).upper()
101107

@@ -162,9 +168,11 @@ def authenticate(self):
162168
+ self.HNAP_LOGIN_METHOD
163169
+ '"',
164170
}
171+
165172
resp = requests.request(
166173
method=method, url=url, data=data, headers=headers
167174
)
175+
168176
if resp.status_code != 200:
169177
raise AuthenticationError(
170178
f"Invalid response while login-in: {resp.status_code} "
@@ -189,6 +197,8 @@ def authenticate(self):
189197
if res["LoginResult"] != "success":
190198
raise AuthenticationError(res["LoginResult"])
191199

200+
self._authenticated = True
201+
192202
def device_info(self):
193203
info = dict(self.call("GetDeviceSettings"))
194204
for k in ["@xmlns", "SOAPActions", "GetDeviceSettingsResult"]:
@@ -228,3 +238,13 @@ class AuthenticationError(ClientError):
228238

229239
class MethodCallError(ClientError):
230240
pass
241+
242+
243+
def auth_required(fn):
244+
@functools.wraps(fn)
245+
def _wrap(self, *args, **kwargs):
246+
if not self._authenticated:
247+
self.authenticated()
248+
return fn(self, *args, **kwargs)
249+
250+
return _wrap

setup.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
[metadata]
44
name = hnap
5-
version = 0.0.5
5+
version = 0.0.7
66
author = Luis López
77
author_email = luis@cuarentaydos.com
88
description = Python clients for HNAP devices

0 commit comments

Comments
 (0)