Skip to content

Commit d287f03

Browse files
authored
Add sync method to get device properties (#2)
1 parent 3ca3326 commit d287f03

File tree

5 files changed

+123
-17
lines changed

5 files changed

+123
-17
lines changed

.vscode/launch.json

+4-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@
66
"type": "python",
77
"request": "launch",
88
"program": "${workspaceFolder}/debug.py",
9-
"args": ["-d", "get-urls"],
9+
"args": [
10+
"-d",
11+
"urls"
12+
],
1013
"console": "integratedTerminal",
1114
"justMyCode": true
1215
}

karcher/cli.py

+51-8
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,11 @@ def default(self, o):
2626

2727

2828
class GlobalContextObject:
29-
def __init__(self,
30-
debug: int = 0,
31-
output: str = 'json',
32-
region: Region = Region.EU
33-
):
29+
def __init__(
30+
self,
31+
debug: int = 0,
32+
output: str = 'json',
33+
region: Region = Region.EU):
3434
self.debug = debug
3535
self.output = output
3636
self.region = region
@@ -46,7 +46,7 @@ def print(self, result):
4646

4747

4848
@click.group()
49-
@click.option('-d', '--debug', is_flag=True)
49+
@click.option('-d', '--debug', is_flag=True, help='Enable debug mode.')
5050
@click.option(
5151
'-o',
5252
'--output',
@@ -84,7 +84,7 @@ def safe_cli():
8484

8585
@cli.command()
8686
@click.pass_context
87-
def get_urls(ctx: click.Context):
87+
def urls(ctx: click.Context):
8888
"""Get region information."""
8989

9090
kh = KarcherHome(region=ctx.obj.region)
@@ -107,7 +107,7 @@ def login(ctx: click.Context, username: str, password: str):
107107
@cli.command()
108108
@click.option('--username', '-u', default=None, help='Username to login with.')
109109
@click.option('--password', '-p', default=None, help='Password to login with.')
110-
@click.option('--token', '-t', default=None, help='Authorization token.')
110+
@click.option('--auth-token', '-t', default=None, help='Authorization token.')
111111
@click.pass_context
112112
def devices(ctx: click.Context, username: str, password: str, token: str):
113113
"""List all devices."""
@@ -128,3 +128,46 @@ def devices(ctx: click.Context, username: str, password: str, token: str):
128128
kh.logout()
129129

130130
ctx.obj.print(devices)
131+
132+
133+
@cli.command()
134+
@click.option('--username', '-u', default=None, help='Username to login with.')
135+
@click.option('--password', '-p', default=None, help='Password to login with.')
136+
@click.option('--auth-token', '-t', default=None, help='Authorization token.')
137+
@click.option('--mqtt-token', '-m', default=None, help='MQTT authorization token.')
138+
@click.option('--device-id', '-d', required=True, help='Device ID.')
139+
@click.pass_context
140+
def device_properties(
141+
ctx: click.Context,
142+
username: str,
143+
password: str,
144+
auth_token: str,
145+
mqtt_token: str,
146+
device_id: str):
147+
"""Get device properties."""
148+
149+
kh = KarcherHome(region=ctx.obj.region)
150+
if auth_token is not None:
151+
kh.login_token(auth_token, mqtt_token)
152+
elif username is not None and password is not None:
153+
kh.login(username, password)
154+
else:
155+
raise click.BadParameter(
156+
'Must provide either token or username and password.')
157+
158+
dev = None
159+
for device in kh.get_devices():
160+
if device.device_id == device_id:
161+
dev = device
162+
break
163+
164+
if dev is None:
165+
raise click.BadParameter('Device ID not found.')
166+
167+
props = kh.get_device_properties(dev)
168+
169+
# Logout if we used a username and password
170+
if auth_token is None:
171+
kh.logout()
172+
173+
ctx.obj.print(props)

karcher/karcher.py

+59-6
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import collections
77
import json
8+
import threading
89
import requests
910
import urllib.parse
1011

@@ -15,7 +16,7 @@
1516
from .device import Device, DeviceProperties
1617
from .exception import KarcherHomeAccessDenied, KarcherHomeException, handle_error_code
1718
from .map import Map
18-
from .mqtt import MqttClient, get_device_topics
19+
from .mqtt import MqttClient, get_device_topic_property_get_reply, get_device_topics
1920
from .utils import decrypt, decrypt_map, encrypt, get_nonce, get_random_string, \
2021
get_timestamp, get_timestamp_ms, is_email, md5
2122

@@ -33,6 +34,7 @@ def __init__(self, region: Region = Region.EU):
3334
self._session = None
3435
self._mqtt = None
3536
self._device_props = {}
37+
self._wait_events = {}
3638

3739
d = self.get_urls()
3840
# Update base URLs
@@ -126,7 +128,7 @@ def _process_response(self, resp, prop=None):
126128
return json.loads(decrypt(result[prop]))
127129
return result
128130

129-
def _mqtt_connect(self):
131+
def _mqtt_connect(self, wait_for_connect=False):
130132
if self._session is None \
131133
or self._session.mqtt_token == '' or self._session.user_id == '':
132134
raise KarcherHomeAccessDenied('Not authorized')
@@ -141,9 +143,20 @@ def _mqtt_connect(self):
141143
port=u.port,
142144
username=self._session.user_id,
143145
password=self._session.mqtt_token)
146+
147+
# Special logic for waiting for connection
148+
event = None
149+
if wait_for_connect:
150+
event = threading.Event()
151+
self._mqtt.on_connect = lambda: event.set()
152+
144153
self._mqtt.connect()
145154
self._mqtt.on_message = self._process_mqtt_message
146155

156+
if wait_for_connect:
157+
event.wait()
158+
self._mqtt.on_connect = None
159+
147160
def get_urls(self):
148161
"""Get URLs for API and MQTT."""
149162

@@ -293,11 +306,15 @@ def _process_mqtt_message(self, topic, msg):
293306

294307
if sn is None:
295308
# Ignore messages for devices we have not subscribed to
309+
if topic in self._wait_events:
310+
self._wait_events[topic].set()
296311
return
297312

298313
if 'thing/event/property/post' in topic \
299314
or 'thing/event/cur_path/post' in topic \
300315
or 'upgrade/post' in topic:
316+
if topic in self._wait_events:
317+
self._wait_events[topic].set()
301318
return
302319

303320
if 'thing/service/property/get_reply' in topic:
@@ -306,19 +323,38 @@ def _process_mqtt_message(self, topic, msg):
306323
# TODO: handle error
307324
return
308325
self._update_device_properties(sn, data['data'])
326+
if topic in self._wait_events:
327+
self._wait_events[topic].set()
328+
return
329+
330+
if topic in self._wait_events:
331+
self._wait_events[topic].set()
332+
333+
def _wait_for_topic(self, topic: str, timeout: float = 5):
334+
if self._mqtt is None:
335+
return
336+
337+
if topic in self._wait_events:
309338
return
310339

340+
event = threading.Event()
341+
self._wait_events[topic] = event
342+
343+
event.wait(timeout)
344+
del self._wait_events[topic]
345+
311346
def _update_device_properties(self, sn: str, data: dict):
312347
if sn not in self._device_props:
313348
return
314349

315350
self._device_props[sn].update(data)
351+
self._device_props[sn].last_update_time = get_timestamp()
316352

317353
def request_device_update(self, dev: Device):
318354
"""Request device update."""
319355

320356
if self._session is None \
321-
or self._session.auth_token == '' or self._session.user_id == '':
357+
or self._session.mqtt_token == '' or self._session.user_id == '':
322358
raise KarcherHomeAccessDenied('Not authorized')
323359

324360
self._mqtt_connect()
@@ -337,7 +373,24 @@ def request_device_update(self, dev: Device):
337373
def get_device_properties(self, dev: Device):
338374
"""Get device properties if it has subscription."""
339375

340-
if dev.sn not in self._device_props:
341-
return None
376+
if dev.sn in self._device_props:
377+
return self._device_props[dev.sn]
378+
379+
if self._session is None \
380+
or self._session.mqtt_token == '' or self._session.user_id == '':
381+
raise KarcherHomeAccessDenied('Not authorized')
382+
383+
self._mqtt_connect(wait_for_connect=True)
384+
subscr = dev.sn not in self._device_props
385+
if subscr:
386+
self.subscribe_device(dev)
387+
self.request_device_update(dev)
388+
self._wait_for_topic(
389+
get_device_topic_property_get_reply(dev.product_id, dev.sn))
390+
391+
props = self._device_props[dev.sn]
392+
393+
if subscr:
394+
self.unsubscribe_device(dev)
342395

343-
return self._device_props[dev.sn]
396+
return props

karcher/mqtt.py

+8-1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ def __init__(self, host, port, username, password):
1616
clean_session=True,
1717
protocol=MQTTv311)
1818
self.on_message = None
19+
self.on_connect = None
1920

2021
def connect(self):
2122
# TODO validate certificate
@@ -64,6 +65,8 @@ def publish(self, topic, payload):
6465

6566
def _on_connect(self, client, userdata, flags, rc):
6667
self._subscribe(self._topics)
68+
if self.on_connect is not None:
69+
self.on_connect()
6770

6871
def _on_message(self, client, userdata, msg):
6972
if self.on_message is not None:
@@ -77,7 +80,7 @@ def get_device_topics(product_id: str, sn: str):
7780
return [
7881
'/mqtt/' + product_id + '/' + sn + '/thing/event/property/post',
7982
'/mqtt/' + product_id + '/' + sn + '/thing/service/property/set_reply',
80-
'/mqtt/' + product_id + '/' + sn + '/thing/service/property/get_reply',
83+
get_device_topic_property_get_reply(product_id, sn),
8184
'/mqtt/' + product_id + '/' + sn + '/thing/service_invoke',
8285
'/mqtt/' + product_id + '/' + sn + '/thing/service_invoke_reply/#',
8386
'/mqtt/' + product_id + '/' + sn + '/thing/event/cur_path/post',
@@ -86,3 +89,7 @@ def get_device_topics(product_id: str, sn: str):
8689
'/mqtt/' + product_id + '/' + sn + '/ota/service/upgrade/get_reply',
8790
'/mqtt/' + product_id + '/' + sn + '/ota/service/version/post',
8891
]
92+
93+
94+
def get_device_topic_property_get_reply(product_id: str, sn: str):
95+
return '/mqtt/' + product_id + '/' + sn + '/thing/service/property/get_reply'

setup.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
name='karcher-home',
88
packages=['karcher'],
99
include_package_data=True,
10-
version='0.1.1',
10+
version='0.2',
1111
license='MIT',
1212
description='Kärcher Home Robots client',
1313
long_description=open('README.md').read(),

0 commit comments

Comments
 (0)