Skip to content

Commit 3d5307c

Browse files
author
Nicolas Berthel
authored
Merge pull request #10 from RainerStaude/master
Update pybecker to add support to receive packets/commands from Becker Remotes
2 parents 1aa1cd5 + d1488e7 commit 3d5307c

File tree

5 files changed

+338
-58
lines changed

5 files changed

+338
-58
lines changed

README.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@ pip install pybecker
2929

3030
Usage
3131
-----
32-
The goal of this library is to be used in your home automation component.
32+
The goal of this library is to be used in your home automation component.
3333
however after installation you can test it by runing
3434
```console
35-
pybecker -a <UP|DOWN|PAIR|HALT> -c <CHANNEL>
35+
python -m pybecker -a <UP|DOWN|PAIR|HALT> -c <CHANNEL>
3636
```

pybecker/__main__.py

+45-4
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,46 @@
11
import argparse
22
import asyncio
3+
import time
34

45
from pybecker.becker import Becker
56

67

78
async def main():
89
"""Main function"""
910
parser = argparse.ArgumentParser()
10-
parser.add_argument('-c', '--channel', required=True, help='channel')
11-
parser.add_argument('-a', '--action', required=True, help='Command to execute (UP, DOWN, HALT, PAIR)')
12-
parser.add_argument('-d', '--device', required=False, help='Device to use for connectivity')
11+
parser.add_argument('-c', '--channel')
12+
parser.add_argument(
13+
'-a',
14+
'--action',
15+
choices=['UP', 'UP2', 'DOWN', 'DOWN2', 'HALT', 'PAIR'],
16+
help='Command to execute (UP, DOWN, HALT, PAIR)',
17+
)
18+
parser.add_argument('-d', '--device', help='Device to use for connectivity')
19+
parser.add_argument('-f', '--file', help='Database file')
20+
parser.add_argument(
21+
'-l',
22+
'--log',
23+
type = int,
24+
help='Logs received commands (only UP, DOWN, HALT) for a certain time (in seconds)'
25+
)
1326
args = parser.parse_args()
1427

15-
client = Becker()
28+
if (args.channel is None) != (args.action is None):
29+
parser.error('both --channel and --action are required')
30+
31+
if args.log is None:
32+
callback = None
33+
else:
34+
commands = {'1':'HALT', '2':'UP', '4':'DOWN',}
35+
callback = lambda packet: print(
36+
"Received packet: "
37+
+ "unit_id: {}, ".format(packet.group('unit_id').decode())
38+
+ "channel: {}, ".format(packet.group('channel').decode())
39+
+ "command: {}, ".format(commands[packet.group('command').decode()])
40+
+ "argument: {}".format(packet.group('argument').decode())
41+
)
42+
43+
client = Becker(device_name=args.device, db_filename=args.file, callback=callback)
1644

1745
if args.action == "UP":
1846
await client.move_up(args.channel)
@@ -27,6 +55,19 @@ async def main():
2755
elif args.action == "PAIR":
2856
await client.pair(args.channel)
2957

58+
# wait for log
59+
timeout = time.time() + (args.log or 0)
60+
while timeout > time.time():
61+
time.sleep(0.01)
62+
63+
# graceful shutdown
64+
client.close()
3065

3166
if __name__ == '__main__':
67+
import sys
68+
69+
# to avoid crashing on exit when running on windows
70+
if sys.version_info[0] == 3 and sys.version_info[1] >= 8 and sys.platform.startswith('win'):
71+
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
72+
3273
asyncio.run(main())

pybecker/becker.py

+53-49
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
11
import logging
2-
import os
32
import re
4-
import serial
5-
import socket
63
import time
4+
from random import randrange
75

86
from .becker_helper import finalize_code
97
from .becker_helper import generate_code
10-
from .becker_helper import BeckerConnectionError
8+
from .becker_helper import BeckerCommunicator
119
from .database import Database
1210

1311
COMMAND_UP = 0x20
@@ -31,7 +29,7 @@
3129
COMMAND_CLEARPOS3 = 0x92
3230
COMMAND_CLEARPOS4 = 0x93
3331

34-
DEFAULT_DEVICE_NAME = '/dev/serial/by-id/usb-BECKER-ANTRIEBE_GmbH_CDC_RS232_v125_Centronic-if00'
32+
# DEFAULT_DEVICE_NAME moved to becker_helper
3533

3634
logging.basicConfig()
3735
_LOGGER = logging.getLogger(__name__)
@@ -45,7 +43,7 @@ class Becker:
4543
Use this class to perform operations on your Becker Shutter using a centronic USB Stick
4644
This class will as well maintain a call increment in an internal database
4745
"""
48-
def __init__(self, device_name=DEFAULT_DEVICE_NAME, init_dummy=False, db_filename=None):
46+
def __init__(self, device_name=None, init_dummy=False, db_filename=None, callback=None):
4947
"""
5048
Create a new instance of the Becker controller
5149
@@ -54,53 +52,30 @@ def __init__(self, device_name=DEFAULT_DEVICE_NAME, init_dummy=False, db_filenam
5452
:type device_name: str
5553
:type init_dummy: bool
5654
"""
57-
self.is_serial = "/" in device_name
58-
if self.is_serial and not os.path.exists(device_name):
59-
raise BeckerConnectionError(device_name + " is not existing")
60-
self.device = device_name
55+
self.communicator = BeckerCommunicator(device_name, callback)
6156
self.db = Database(db_filename)
6257

6358
# If no unit is defined create a dummy one
6459
units = self.db.get_all_units()
6560
if not units and init_dummy:
6661
self.db.init_dummy()
6762

68-
try:
69-
self._connect()
70-
except serial.SerialException:
71-
raise BeckerConnectionError("Error when trying to establish connection using " + device_name)
63+
# Start communicator thread
64+
self.communicator.start()
7265

73-
def _connect(self):
74-
if self.is_serial:
75-
self.s = serial.Serial(self.device, 115200, timeout=1)
76-
self.write_function = self.s.write
77-
else:
78-
if ':' in self.device:
79-
host, port = self.device.split(':', 1)
80-
else:
81-
host = self.device
82-
port = '5000'
83-
self.s = socket.create_connection((host, port))
84-
self.write_function = self._reconnecting_sendall
85-
86-
def _reconnecting_sendall(self, *args, **kwargs):
87-
"""Wrapper for socker.sendall that reconnects (once) on failure"""
88-
89-
try:
90-
return self.s.sendall(*args, **kwargs)
91-
except OSError:
92-
# Assume the connection failed, and connect again
93-
self._connect()
94-
return self.s.sendall(*args, **kwargs)
66+
def close(self):
67+
"""Stop communicator thread, close device and database"""
68+
self.communicator.close()
69+
self.db.conn.close()
9570

9671
async def write(self, codes):
9772
for code in codes:
98-
self.write_function(finalize_code(code))
99-
time.sleep(0.1)
73+
self.communicator.send(finalize_code(code))
74+
# Sleep implemented in BeckerCommunicator
10075

10176
async def run_codes(self, channel, unit, cmd, test):
10277
if unit[2] == 0 and cmd != "TRAIN":
103-
_LOGGER.error("The unit %s is not configured" % (unit[0]))
78+
_LOGGER.error("The unit %s is not configured", (unit[0]))
10479
return
10580

10681
# move up/down dependent on given time
@@ -171,21 +146,14 @@ async def run_codes(self, channel, unit, cmd, test):
171146
self.db.set_unit(unit, test)
172147

173148
async def send(self, channel, cmd, test=False):
174-
b = channel.split(':')
175-
if len(b) > 1:
176-
ch = int(b[1])
177-
un = int(b[0])
178-
else:
179-
ch = int(channel)
180-
un = 1
149+
150+
un, ch = self._split_channel(channel)
181151

182152
if not 1 <= ch <= 7 and ch != 15:
183153
_LOGGER.error("Channel must be in range of 1-7 or 15")
184154
return
185155

186-
if not self.device:
187-
_LOGGER.error("No device defined")
188-
return
156+
# device check implemented in BeckerCommunicator
189157

190158
if un > 0:
191159
unit = self.db.get_unit(un)
@@ -255,3 +223,39 @@ async def list_units(self):
255223
"""
256224

257225
return self.db.get_all_units()
226+
227+
@staticmethod
228+
def _split_channel(channel):
229+
b = channel.split(':')
230+
if len(b) > 1:
231+
ch = int(b[1])
232+
un = int(b[0])
233+
else:
234+
ch = int(channel)
235+
un = 1
236+
return un, ch
237+
238+
async def init_unconfigured_unit(self, channel, name=None):
239+
"""Init unconfigured units in database and send init call"""
240+
# check if unit is configured
241+
un, ch = self._split_channel(channel) # pylint: disable=unused-variable
242+
unit = self.db.get_unit(un)
243+
if unit[2] == 0:
244+
_LOGGER.warning(
245+
"Unit %s%s with channel %s not registered in database file %s!",
246+
un,
247+
" of " + name if name is not None else "",
248+
channel,
249+
self.db.filename,
250+
)
251+
# set the unit as configured
252+
unit[1] = randrange(10, 40, 1)
253+
unit[2] = 1
254+
self.db.set_unit(unit)
255+
# send init call to sync with database (5 required for my Roto cover)
256+
for init_call_count in range(1,6):
257+
_LOGGER.debug(
258+
"Init call to %s:%s #%d", un, 1, init_call_count)
259+
await self.stop(':'.join((str(un), '1')))
260+
# 0.5 to 0.9 seconds (works with my Roto cover)
261+
time.sleep(randrange(5, 10, 1) / 10)

0 commit comments

Comments
 (0)