Skip to content
This repository was archived by the owner on Jan 5, 2024. It is now read-only.

Commit e48ab3c

Browse files
authored
Merge pull request #2 from rccoleman/async
Convert to async
2 parents 9bc934e + 5d8c98f commit e48ab3c

File tree

6 files changed

+100
-100
lines changed

6 files changed

+100
-100
lines changed

README.md

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,6 @@ Now, run `python test.py` and you should get a prompt that looks like this:
3131

3232
`1 = ON, 2 = OFF, 3 = Status, Other = quit:`
3333

34-
You'll also see an `INFO` message when the machine at first when the current machine state is detected and whenever it changes after that. It looks like this:
35-
36-
`INFO:lmdirect:Device is OFF`.
37-
3834
You can hit `1` to turn the machine on, `2` to turn it off, `3` to dump a dict of all the config and status items that it's received from your machine, and any other key to quit. The app requests all status & config information every 5 seconds, so you should see the values change when the state of the machine changes.
3935

4036
Note that the machine only accepts a single connection at a time, so you cannot run this app and the mobile app at the same time. The second one will block until you close the first instance. This means that you can't experiment by running this app and manipulating settings using the mobile app simultaneously, but you can change settings on the machine itself and see the values update here.

lmdirect/__init__.py

Lines changed: 43 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,10 @@
22
from . import cmds as CMD
33
from .const import *
44

5-
import socket, select
6-
from threading import Thread
7-
from time import sleep
8-
5+
import asyncio
6+
from functools import partial
97
import logging
108

11-
129
_LOGGER = logging.getLogger(__name__)
1310
_LOGGER.setLevel(logging.INFO)
1411

@@ -18,53 +15,49 @@ def __init__(self, key):
1815
"""Init LMDirect"""
1916
self.run = True
2017
self.cipher = AESCipher(key)
21-
self.response_thread = None
22-
self.status_thread = None
18+
self.response_task = None
19+
self.status_task = None
2320
self.current_status = {}
24-
self.is_on = None
2521

26-
def connect(self, addr):
22+
async def connect(self, addr):
2723
"""Conmnect to espresso machine"""
2824
TCP_PORT = 1774
2925

30-
self.s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
31-
self.s.connect((addr, TCP_PORT))
26+
"""Connect to the machine"""
27+
self.reader, self.writer = await asyncio.open_connection(addr, TCP_PORT)
3228

33-
"""Start listening for responses"""
34-
self.response_thread = Thread(target=self.response, name="Response")
35-
self.response_thread.start()
29+
loop = asyncio.get_event_loop()
30+
31+
"""Start listening for responses & sending status requests"""
32+
self.response_task = loop.create_task(self.response())
3633

3734
"""Start sending status requests"""
38-
self.status_thread = Thread(target=self.status, name="Status")
39-
self.status_thread.start()
35+
self.status_task = loop.create_task(self.status())
4036

41-
def close(self):
37+
async def close(self):
4238
"""Stop listening for responses and close the socket"""
4339
self.run = False
44-
threads = [self.response_thread, self.status_thread]
4540

46-
[t.join() for t in threads if t is not None]
41+
await asyncio.gather(*[self.response_task, self.status_task])
4742

48-
self.s.shutdown(socket.SHUT_WR)
49-
self.s.close()
43+
"""Close the connection"""
44+
self.writer.close()
5045

51-
def response(self):
46+
async def response(self):
5247
"""Start thread to receive responses"""
5348
BUFFER_SIZE = 1000
5449

5550
while self.run:
56-
reading = [self.s]
57-
writing = []
58-
exceptions = []
59-
select.select(reading, writing, exceptions, 1)
60-
if self.s in reading:
61-
encoded_data = self.s.recv(BUFFER_SIZE)
62-
_LOGGER.debug(encoded_data)
63-
if encoded_data is not None:
64-
plaintext = self.cipher.decrypt(encoded_data[1:-1])
65-
self.process_data(plaintext)
66-
67-
def process_data(self, plaintext):
51+
encoded_data = await self.reader.read(BUFFER_SIZE)
52+
53+
_LOGGER.debug(encoded_data)
54+
if encoded_data is not None:
55+
loop = asyncio.get_running_loop()
56+
fn = partial(self.cipher.decrypt, encoded_data[1:-1])
57+
plaintext = await loop.run_in_executor(None, fn)
58+
await self.process_data(plaintext)
59+
60+
async def process_data(self, plaintext):
6861
"""Process incoming packet"""
6962

7063
"""Separate the preamble from the data"""
@@ -82,39 +75,18 @@ def process_data(self, plaintext):
8275
)
8376
return
8477

85-
if preamble == CMD.D8_PREAMBLE:
86-
_LOGGER.debug("D8 status")
87-
self.populate_items(data, CMD.D8_MAP)
88-
89-
new_state = self.current_status["POWER"] == 1
90-
if new_state is not self.is_on:
91-
self.is_on = new_state
92-
93-
_LOGGER.info("Device is {}".format("ON" if self.is_on else "OFF"))
94-
elif preamble == CMD.E9_PREAMBLE:
95-
_LOGGER.debug("E9 status")
96-
self.populate_items(data, CMD.E9_MAP)
97-
elif preamble == CMD.SHORT_PREAMBLE:
98-
_LOGGER.debug("Short status")
99-
self.populate_items(data, CMD.SHORT_MAP)
100-
elif preamble == CMD.EB_PREAMBLE:
101-
_LOGGER.debug("EB auto schedule")
102-
self.populate_items(data, CMD.EB_MAP)
103-
104-
_LOGGER.debug(self.current_status)
78+
if any(preamble in x for x in CMD.PREAMBLES):
79+
await self.populate_items(data, CMD.RESP_MAP[preamble])
80+
_LOGGER.debug(self.current_status)
10581

106-
def populate_items(self, data, map):
82+
async def populate_items(self, data, map):
10783
for elem in map:
10884
index = elem.index * 2
10985
size = elem.size * 2
11086

111-
value = int(data[index : index + size], 16)
87+
value = int(data[index: index + size], 16)
11288
if any(x in map[elem] for x in ["TEMP", "PREBREWING_K"]):
11389
value = value / 10
114-
elif "UNITS" in map[elem]:
115-
print(data)
116-
print("{}, {}".format(value, chr(value)))
117-
value = u"\xb0F" if chr(value) == "f" else u"\xb0C"
11890
elif "AUTO_BITFIELD" in map[elem]:
11991
for i in range(0, 7):
12092
setting = ENABLED if value & 0x01 else DISABLED
@@ -123,19 +95,21 @@ def populate_items(self, data, map):
12395
continue
12496
self.current_status[map[elem]] = value
12597

126-
def status(self):
98+
async def status(self):
12799
"""Send periodic status requests"""
128100
while self.run:
129-
self.send_cmd(CMD.STATUS)
130-
self.send_cmd(CMD.CONFIG)
131-
self.send_cmd(CMD.AUTO_SCHED)
132-
sleep(5)
101+
await self.send_cmd(CMD.STATUS)
102+
await self.send_cmd(CMD.CONFIG)
103+
await self.send_cmd(CMD.AUTO_SCHED)
104+
await asyncio.sleep(5)
133105

134-
def send_cmd(self, cmd):
106+
async def send_cmd(self, cmd):
135107
"""Send command to espresso machine"""
136-
ciphertext = "@" + self.cipher.encrypt(cmd).decode("utf-8") + "%"
108+
loop = asyncio.get_running_loop()
109+
fn = partial(self.cipher.encrypt, cmd)
110+
ciphertext = "@" + (await loop.run_in_executor(None, fn)).decode("utf-8") + "%"
137111
_LOGGER.debug(ciphertext)
138112

139113
_LOGGER.debug("Before sending: {}".format(ciphertext))
140-
self.s.send(bytes(ciphertext, "utf-8"))
141-
_LOGGER.debug("After sending")
114+
self.writer.write(bytes(ciphertext, "utf-8"))
115+
await self.writer.drain()

lmdirect/aescipher.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import logging
66

77
_LOGGER = logging.getLogger(__name__)
8-
_LOGGER.setLevel(logging.DEBUG)
8+
_LOGGER.setLevel(logging.INFO)
99

1010

1111
class AESCipher:

lmdirect/cmds.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,13 @@
2020
E9_PREAMBLE = "0000001F"
2121
EB_PREAMBLE = "0310001D"
2222

23+
PREAMBLES = [
24+
SHORT_PREAMBLE,
25+
D8_PREAMBLE,
26+
E9_PREAMBLE,
27+
EB_PREAMBLE,
28+
]
29+
2330
RESPONSE_GOOD = "OK"
2431

2532
# Response Maps
@@ -153,4 +160,11 @@ def size(self):
153160
5: "THU_AUTO",
154161
6: "FRI_AUTO",
155162
7: "SAT_AUTO",
163+
}
164+
165+
RESP_MAP = {
166+
D8_PREAMBLE: D8_MAP,
167+
E9_PREAMBLE: E9_MAP,
168+
SHORT_PREAMBLE: SHORT_MAP,
169+
EB_PREAMBLE: EB_MAP,
156170
}

lmdirect/const.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
"""Constants for lmdirect"""
22
ENABLED = "ENABLED"
3-
DISABLED = "DISABLED"
3+
DISABLED = "DISABLED"

test.py

Lines changed: 41 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,54 @@
1+
import asyncio
12
from lmdirect import LMDirect
2-
from lmdirect.cmds import ON, OFF
33
import json
4+
import sys
45
import logging
6+
from lmdirect.cmds import ON, OFF
57

68
logging.basicConfig(level=logging.DEBUG)
79
_LOGGER = logging.getLogger(__name__)
810
_LOGGER.setLevel(logging.DEBUG)
911

10-
try:
11-
with open("config.json") as config_file:
12-
data = json.load(config_file)
1312

14-
key = data["key"]
15-
ip_addr = data["ip_addr"]
16-
except Exception as err:
17-
print(err)
18-
exit(1)
13+
def read_config():
14+
"""Read key and machine IP from config file"""
15+
try:
16+
with open("config.json") as config_file:
17+
data = json.load(config_file)
1918

20-
lmdirect = LMDirect(key)
21-
lmdirect.connect(ip_addr)
19+
key = data["key"]
20+
ip_addr = data["ip_addr"]
21+
except Exception as err:
22+
print(err)
23+
exit(1)
2224

23-
while True:
24-
try:
25-
response = input("1 = ON, 2 = OFF, 3 = Status, Other = quit: ")
26-
if response == "1":
27-
lmdirect.send_cmd(ON)
28-
elif response == "2":
29-
lmdirect.send_cmd(OFF)
30-
elif response == "3":
31-
print(lmdirect.current_status)
32-
else:
25+
return key, ip_addr
26+
27+
28+
async def main():
29+
"""Main execution loop"""
30+
loop = asyncio.get_event_loop()
31+
key, ip_addr = await loop.run_in_executor(None, read_config)
32+
33+
lmdirect = LMDirect(key)
34+
await lmdirect.connect(ip_addr)
35+
36+
while True:
37+
try:
38+
print("\n1 = ON, 2 = OFF, 3 = Status, Other = quit: ")
39+
response = (await loop.run_in_executor(None, sys.stdin.readline)).rstrip()
40+
41+
if response == "1":
42+
await lmdirect.send_cmd(ON)
43+
elif response == "2":
44+
await lmdirect.send_cmd(OFF)
45+
elif response == "3":
46+
print(lmdirect.current_status)
47+
else:
48+
break
49+
except KeyboardInterrupt:
3350
break
34-
except KeyboardInterrupt:
35-
break
3651

37-
lmdirect.close()
38-
exit(0)
52+
await lmdirect.close()
53+
54+
asyncio.run(main())

0 commit comments

Comments
 (0)