From 5cb99670668eeee4f0f1b3f7e5be62818a29087e Mon Sep 17 00:00:00 2001
From: johannes walcher <johannes.walcher@stusta.de>
Date: Thu, 3 May 2018 15:54:00 +0200
Subject: [PATCH] added rupprechtinterface

---
 rupprecht/__init__.py            |   0
 rupprecht/__main__.py            |  20 +++
 rupprecht/rcswitch.py            | 128 +++++++++++++++++++
 rupprecht/rupprecht.py           | 206 +++++++++++++++++++++++++++++++
 rupprecht/test.py                |  39 ++++++
 util/rupprechtemulator.py        |   9 ++
 util/start_rupprecht_emulator.sh |   1 +
 7 files changed, 403 insertions(+)
 create mode 100644 rupprecht/__init__.py
 create mode 100644 rupprecht/__main__.py
 create mode 100644 rupprecht/rcswitch.py
 create mode 100644 rupprecht/rupprecht.py
 create mode 100644 rupprecht/test.py
 create mode 100644 util/rupprechtemulator.py
 create mode 100755 util/start_rupprecht_emulator.sh

diff --git a/rupprecht/__init__.py b/rupprecht/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/rupprecht/__main__.py b/rupprecht/__main__.py
new file mode 100644
index 0000000..2fb14ed
--- /dev/null
+++ b/rupprecht/__main__.py
@@ -0,0 +1,20 @@
+import asyncio
+
+from rupprecht.rupprecht import Rupprecht
+
+def main():
+    """
+    Actually start the shit
+    """
+    loop = asyncio.get_event_loop()
+    rup = Rupprecht()
+    loop.set_debug(True)
+    try:
+        loop.run_forever()
+    except KeyboardInterrupt:
+        pass
+
+    loop.run_until_complete(rup.teardown())
+
+if __name__ == "__main__":
+    main()
diff --git a/rupprecht/rcswitch.py b/rupprecht/rcswitch.py
new file mode 100644
index 0000000..9851d52
--- /dev/null
+++ b/rupprecht/rcswitch.py
@@ -0,0 +1,128 @@
+#!/usr/bin/python3
+"""
+generate packets for quigg rc switches
+"""
+import argparse
+import serial
+import asyncio
+
+class Quigg1000:
+    def __init__(self, loop=None, serial=None, rupprecht=None, code=1337, subaddr=0):
+        """
+        Create a new switch representation
+        The switch can either be connected to a raw-serial port (old arduino)
+        or it can be connected via a rupprecht-interface, that will handle all interfacing
+        """
+        if loop == None:
+            loop=asyncio.get_event_loop()
+        self.loop = loop
+        self.serial = serial
+        self.rupprecht = rupprecht
+        self.code = code
+        self.subaddr = str(subaddr)
+
+        if serial != None and rupprecht != None:
+            raise ArgumentException("Only one of serial and rupprecht may be set")
+
+
+    async def state(self, state, debug=False):
+        """
+        Set the switch to the value indicated by state (true or false)
+        """
+        msg = self.__build_msg(state, debug=debug)
+        if debug:
+            print(msg)
+        await self.__send_msg(msg)
+
+    async def on(self, debug=False):
+        msg = self.__build_msg(True, debug=debug)
+        if debug:
+            print(msg)
+        await self.__send_msg(msg)
+
+    async def off(self, debug=False):
+        msg = self.__build_msg(False, debug=debug)
+        if debug:
+            print(msg)
+        await self.__send_msg(msg)
+
+    def __build_msg(self, state, dimm=0, debug=False):
+        """
+        Build a quigg message.
+        Read the inline docs for details
+
+        state: 1 bit value: True: On, False: Off
+        dimm: ??
+        """
+        # key on remote -> real addr mapping
+        key_lookup = {'1': 0, '2': 1, '4': 2, '3': 3}
+        # 12 bits set code addr
+        set_code = '{0:0>12b}'.format(self.code)
+        # 2 bit addr of rc switch
+        addr = '{0:0>2b}'.format(key_lookup[self.subaddr])[::-1]
+        # address all rf devices (not implemented)
+        allflag = '0'
+        # 1 bit state
+        state = '1' if int(state) else '0'
+        # 2 bit dimm information
+        dimm = '{0:0>2}'.format(dimm)
+        # repeat bit 13 / addr bit 0
+        internal = addr[0]
+        # build buffer with leading 1
+        buffer_string = '1' + set_code + addr + allflag + state + dimm + internal
+        # parity over last 7 bits
+        parity_bit = '1' if buffer_string[13:20].count('1') % 2 else '0'
+        # append parity bit
+        buffer_string += parity_bit
+
+        if debug:
+            print('set code:\t{}'.format(set_code))
+            print('addr:\t\t{}'.format(addr))
+            print('state:\t\t{}'.format(state))
+            print('dimm:\t\t{}'.format(dimm))
+            print('internal:\t{}'.format(internal))
+            print('parity range:\t{}'.format(buffer_string[13:20]))
+            print('parity bit:\t{}'.format(parity_bit))
+            print('\nbuffer string:\t{}'.format(buffer_string))
+
+        if len(buffer_string) != 21:
+            print('Bad packet length.')
+            return None
+        return buffer_string
+
+    async def __send_msg(self, msg):
+        if self.rupprecht:
+            await self.rupprecht.light(msg)
+        if self.serial:
+            try:
+                self.ser.write(str.encode(msg))
+            except serial.SerialException:
+                print('Serial communication failed.')
+
+def main():
+    parser = argparse.ArgumentParser(description='Remote COntrol rc switches')
+    parser.add_argument('addr', help='number of the switch')
+    parser.add_argument('state', help='1 (on) or 0 (off)')
+    parser.add_argument('--set-code', '-c', help='set-code-number',
+                        default=2816, type=int)
+    parser.add_argument('--dimm', help='dimm value', default=0)
+    parser.add_argument('--device',
+                        help='serial device path/name (e.g. COM11 or /dev/ttyUSB3)',
+                        default='/dev/ttyUSB3')
+    parser.add_argument('--debug', '-d', help='enable debug output',
+                        default=False, action='store_true')
+
+    args = parser.parse_args()
+
+    try:
+        s = serial.Serial(args.device, 9600)
+    except serial.SerialException:
+        print('Serial communication failed.')
+        exit()
+
+    r = Quigg1000(serial=s, loop=loop, code=args.set_code, subaddr=args.addr)
+    loop=asyncio.get_event_loop()
+    loop.run_until_complete(r.state(args.state, args.debug))
+
+if __name__ == '__main__':
+    main()
diff --git a/rupprecht/rupprecht.py b/rupprecht/rupprecht.py
new file mode 100644
index 0000000..5d21209
--- /dev/null
+++ b/rupprecht/rupprecht.py
@@ -0,0 +1,206 @@
+#!/usr/bin/env python3
+
+import asyncio
+import json
+import serial_asyncio
+import serial
+
+from hauptbahnhof import Hauptbahnhof
+
+from . import rcswitch
+
+class RupprechtError:
+    pass
+
+class RupprechtCommandError(RupprechtError):
+    def __init__(self, command):
+        self.command = command
+
+    def __repr__(self):
+        return "RupprechtCommandError: {}".format(self.command)
+
+class Rupprecht:
+    """
+    Implement serial connection to rupprecht
+    """
+
+    def __init__(self, loop=None):
+        if not loop:
+            loop = asyncio.get_event_loop()
+
+        self.loop = loop
+        self.hbf = Hauptbahnhof(loop)
+        self.hbf.subscribe('/haspa/led', self.command_led)
+        self.space_is_open = False
+
+        try:
+            r = RupprechtInterface("/dev/ttyACM0")
+        except serial.SerialException:
+            r = RupprechtInterface("/tmp/rupprechtemulator")
+
+        r.subscribe_button(self.button_message)
+
+        self.imposed_ids = {
+            'rupprecht-table': rcswitch.Quigg1000(code=1337, subaddr=1, rupprecht=r),
+            'rupprecht-alarm': rcswitch.Quigg1000(code=1337, subaddr=2, rupprecht=r),
+            'rupprecht-fan': rcswitch.Quigg1000(code=1337, subaddr=3, rupprecht=r)
+        }
+
+    async def teardown(self):
+        await self.hbf.teardown()
+
+    async def command_led(self, source, payload, msg):
+        for devid, value in msg.items():
+            try:
+                if value == 0:
+                    self.imposed_ids[devid].off()
+                elif value == 1023:
+                    self.imposed_ids[devid].on()
+            except KeyError:
+                pass
+
+    async def button_message(self, msg):
+        if msg['open'] and not self.space_is_open:
+            self.space_is_open = True
+            await self.hbf.publish('/haspa/status', 'open')
+        elif not msg['open'] and self.space_is_open:
+            self.space_is_open = False
+            await self.hbf.publish('/haspa/status', 'closed')
+
+class RupprechtInterface:
+    class SerialProtocol(asyncio.Protocol):
+        def __init__(self, master):
+            self.master = master
+            self.buffer = []
+
+        def connection_made(self, transport):
+            #transport.serial.rts = False
+            self.master.transport = transport
+
+        def data_received(self, data):
+            for d in data:
+                if chr(d) == '\n':
+                    self.master.received(self.buffer)
+                    self.buffer = []
+                else:
+                    self.buffer.append(chr(d))
+
+        def connection_lost(self, exc):
+            print('port closed, exiting')
+            self.master.loop.stop()
+
+
+    def __init__(self, serial_port, baudrate=115200, loop=None):
+        if loop == None:
+            loop = asyncio.get_event_loop()
+        self.loop = loop
+        self.serial_port = serial_port
+        self.baudrate = baudrate
+        self.transport = None
+        self.response_pending = False
+        self._queue = []
+        self.response_event = asyncio.Event(loop=loop)
+        self.ready = False
+        self.ready_event = asyncio.Event(loop=loop)
+        self.button_callbacks = [];
+        self.last_result = ''
+        coro = serial_asyncio.create_serial_connection(loop,
+                (lambda: RupprechtInterface.SerialProtocol(self)),
+                serial_port, baudrate=baudrate)
+        loop.run_until_complete(coro)
+        self.echo_allowed = True
+
+    def subscribe_button(self, callback):
+        self.button_callbacks.append(callback)
+
+    async def send_raw(self, msg, expect_response=True, force=False):
+        """
+        Send the raw line to the serial stream
+
+        This will wait, until the preceding message has been processed.
+        """
+        if not self.ready:
+            print("Waiting for client to become ready")
+            await self.ready_event.wait()
+            print("Client is now ready")
+
+        if self.echo_allowed and "CONFIG ECHO" not in msg:
+            await self.send_raw("CONFIG ECHO OFF")
+
+        # The queue is the message stack that has to be processed
+        self._queue.append(msg)
+        while self._queue:
+            self.response_event.clear()
+            # Await, if there was another message in the pipeline that has not
+            # yet been processed
+            if not force:
+                while self.response_pending:
+                    print("before sending '{}' we need a response for '{}'".format(msg, self.last_command))
+                    await self.response_event.wait()
+                    self.response_event.clear()
+            print("Sending", msg)
+            # If the queue has been processed by somebody else in the meantime
+            if not self._queue:
+                break
+            self.response_pending = True
+            next_msg = self._queue.pop(0)
+
+            # Now actually send the data
+            self.last_command = next_msg
+            self.transport.write(next_msg.encode('ascii'))
+            #append a newline if the sender hasnt already
+            if next_msg[-1] != '\n':
+                self.transport.write(b'\n')
+
+            await self.response_event.wait()
+            self.response_event.clear()
+            try:
+                if str.startswith(self.last_result, "ERROR"):
+                    raise RupprechtCommandError(msg)
+                elif str.startswith(self.last_result, "OK"):
+                    if self.last_command == "CONFIG ECHO OFF":
+                        self.echo_allowed = False
+                    self.response_pending = False
+                    self.response_event.set()
+                    return True
+                else:
+                    if not expect_response:
+                        self.response_pending = False
+                        return True
+                    #raise RupprechtCommandError("Unknown result code: {}".format(self.last_result))
+            except Exception as e:
+                print("Exception while parsing response", msg, e)
+                break
+
+    def received(self, msg):
+        msg = ''.join(msg).strip()
+        if not msg:
+            return
+        print("received \033[03m{}\033[0m".format(msg))
+        if str.startswith(msg, "BUTTON"):
+            try:
+                self.buttons = json.loads(msg[len("BUTTON"):])
+                asyncio.gather(*[b(self.buttons) for b in self.button_callbacks], loop=self.loop)
+            except json.decoder.JSONDecodeError:
+                print("Invalid json received:", msg)
+        elif msg == "READY" and not self.ready:
+            self.ready = True
+            self.ready_event.set()
+        else:
+            self.last_result = msg
+            self.response_event.set()
+
+    async def help(self):
+        await self.send_raw("HELP")
+
+    async def config(self, key, value):
+        await self.send_raw("CONFIG {} {}".format(key, value))
+
+    async def text(self, msg):
+        await self.send_raw("TEXT {}".format(msg), expect_response=False)
+
+    async def button(self):
+        await self.send_raw("BUTTON", expect_response=False)
+
+    async def light(self, raw_msg):
+        await self.send_raw("LIGHT {}".format(raw_msg), expect_response=False)
diff --git a/rupprecht/test.py b/rupprecht/test.py
new file mode 100644
index 0000000..4fc1a6c
--- /dev/null
+++ b/rupprecht/test.py
@@ -0,0 +1,39 @@
+import asyncio
+from hauptbahnhof import Hauptbahnhof
+
+from rupprecht.rupprecht import Rupprecht
+
+
+messages = asyncio.Queue()
+async def on_message(client, message, _):
+    print("Got message: %s"%message)
+    await messages.put(message)
+
+
+async def test(loop):
+    testbf = Hauptbahnhof(loop=loop)
+    await asyncio.sleep(2)
+    # Now everythin should be set up
+
+    await testbf.publish("/haspa/led", {'rupprecht-table':1023})
+
+    # This module is not sending any status back
+    try:
+        await testbf.teardown()
+    except asyncio.CancelledError:
+        pass
+
+def main():
+    loop = asyncio.get_event_loop()
+    lib = Rupprecht(loop=loop)
+
+    result = loop.run_until_complete(test(loop))
+    loop.run_until_complete(lib.teardown())
+
+    if result:
+        exit(0)
+    else:
+        exit(1)
+
+if __name__=="__main__":
+    main()
diff --git a/util/rupprechtemulator.py b/util/rupprechtemulator.py
new file mode 100644
index 0000000..1d1be59
--- /dev/null
+++ b/util/rupprechtemulator.py
@@ -0,0 +1,9 @@
+import time
+
+while True:
+    time.sleep(30)
+    print("READY")
+    for i in range(10):
+        time.sleep(30)
+        print("OK")
+
diff --git a/util/start_rupprecht_emulator.sh b/util/start_rupprecht_emulator.sh
new file mode 100755
index 0000000..c070986
--- /dev/null
+++ b/util/start_rupprecht_emulator.sh
@@ -0,0 +1 @@
+socat -d PTY,link=/tmp/rupprechtemulator,echo=0 "EXEC:python3 rupprechtemulator.py ...,pty,raw",echo=0
-- 
GitLab