RCOutlets

RCOutlets Documentation

Version

0.3.0

Author

Alexander Keil

Contact

alex@akeil.net

Date

2014-01-04

Abstract

Control radio controlled outlets from a computer with Arduino.

Hardware

Required Parts

  • Radio Controlled Outlets

  • Arduino

  • RF Transmitter

  • 2 LED's, 2 Resistors, wires

RC Outlets

The radio controlled outlet comes as a set with three outlet adapters and a handset remote control. The adapters are numbers (1, 2, 3) and the remote has three corresponding pairs of "on"/"off" buttons, one for each outlet (6 buttons total).

There is a rocker switch on the remote and a small dial on the adapter that lets you set one of for "groups" (A-D). This allows to use more than one set at the same time. The adapter "1" will respond to the "1 on" button on the remote only if the remote is set to the same channel as the adapter.

Group

Numbers

1

2

3

A

A1

A2

A3

B

B1

B2

B3

C

C1

C2

C3

D

D1

D2

D3

Thus, up to 12 outlets can be controlled with a total of 24 commands (12x "on", 12x "off").

RF Transmitter

Aside from the Arduino, a 433.92 MHz RF Transmitter is required. It does not really matter, which exact RF module is used, as long as it send on 433.92 MHz and runs on 5V. It would even be possible (and much cheaper) to use the RF module from the handheld remote.

In this case an Aurel TX-SAW Mid/5V is used.

assets/transmitter.jpg

The Aurel Transmitter is (for example) available at Conrad (German site), the manufacturer has a user manual [PDF] and a datasheet can also be found.

The Aurel TX-SAW Mid/5V transmitter module has 6 pins which are used as follows:

assets/aurel-tx-saw-5v.svg

PINs on the Aurel TX-SAW Mid/5V Transmitter Module

Pin

Name

Description

1

Tx Data

Data input, connect to TRANSMITTER_PIN on the Arduino.

2

GND

Connect to ground.

7

GND

Connect to ground.

8

RF out

RF output connected to antenna.

9

GND

Connect to ground.

10

V+

Connection to 5V supply of Arduino.

You can use a single GND connection for Pins 2,7 and 9. A piece of wire constitutes a sufficient antenna.

Status LED's

We will also connect two status LED's:

Green

Lights when a command is received over serial and while the command is transmitted over wireless.

Red

Lights for some time if there was an error (e.g. a bad command was received).

Setup

Breadboard
assets/RCOutlets_breadboard.svg

Breadboard setup for the RCOutlets

Schematic
assets/RCOutlets_schematic.svg

Wiring for the RCOutlets

Arduino Sketch

Protocol

We use plain text messages to be sent as commands to the Arduino. the messages are fixed length, each terminated by a newline. Each message consists of four characters:

Index

Meaning

Values

0

Group

A,B,C,D

1

Device

1,2,3

2

Command

0, 1

3

Terminate

\n

Example:

> A10   # group A, switch 1, "off"
> A21   # group A, switch 2, "on"
> B11   # group B, switch 1, "on"

Additionally, a simple handshake mechanism is required. When the serial connection to the Arduino is opened, the Arduino will restart and not be ready for a few seconds. All data that is sent before the board is ready will be lost.

The handshake scheme is that the "client" (our Python script) sends an ASCII ENQ character and the "server" replies with a single ASCII ACK to signal that it is ready.

When initializing the connection, we will keep sending ENQ until we receive an ACK (or until a timeout runs out). After the ACK is received, we can start sending commands to control the power outlets.

In pseudo code:

while True:
    serial.write(ENQ)
    if serial.read(1) == ACK:
        break

Code

Luckily, a project called RCSwitch provides us with a library that can handle the interaction with the RF module and also knows how to encode the command which are sent to the power outlets.

Thus, the only thing left is interpreting input received over serial and deciding which commands to send.

Python Client

The Python module uses pySerial to communicate with the Arduino board.

The script can be used as a library module in another Python script or it can be run as a standalone command line app.

Usage

The following interface is supported:

with RCOutlets(options) as outlets:
    # by group and number
    outlets.get('A1').on()
    outlets.get('A0').off()

    # with aliases/pattern matching
    for switch in outlets.mget('kitchen B*'):
        switch.off()

Sample command line usage:

$ outlet A1 on
$ outlet A0 off
$ outlet desk on
$ outlet 'kitchen B*' off

Configuration

If the file ~/.config/rcoutlets.conf exists, values from that file are used in the command-line program.

This is how a sample config could look like:

[rcoutlets]
port = /dev/ttyACM0
timeout = 2
groups = A B
handshake_timeout = 4

[alias]
desk = A1
kitchen = B1 B2 C1

The configuration file can map alias names to switch-identifiers. An alias can refer to several switches and it is possible to define groups of switches. If a command is issued to a group, all switches in that group receive that command.

The Daemon

When the serial connection to the Arduino is initialized, the board is reset and it takes some time for it to come up again. For this reason, the Python script connects to the Arduino via a handshake. It keeps sending a Handshake Request until it receives the proper Handshake Response. Only after the handshake completed successfully, we can start sending commands.

This works well but since the handshake takes about two seconds, a simple run-once script will be unresponsive. It has to initialize the connection each time it is invoked, so every command is delayed by the time it takes to build up the collection.

For this reason, we need a daemon which runs constantly and keeps the connection open. Our command line script will then not connect to the Arduino but instead send its command to the daemon process which relays it over serial.

Requirements:

Reconnect

The daemon process might lose its connection to the Arduino, for example if the board is disconnected. The daemon should be able to detect a lost connection and try to rebuild it.

Start on request

Start the daemon only when its (first) required. This is a task for the OS. Connect lazily, i.e. when the first command is sent. This is implemented in the daemon.

The same script is used for:

  • run the daemon

  • run as a client for the daemon

  • run standalone with a new connection for each command

The python script will support a command line option to run as a daemon. If called with this option, the script starts the daemon and then exits.

$ rcoutlets --daemon

Here is an example implementation of running a daemon in PYthon.

Pidfile

By default, it is /var/run/rcoutlets/rcoutlets.pid. It is located in a separate directory to allow non-root users to write it without granting write permissions on /var/run/.

Prepare like so (as root):

# mkdir /var/run/rcoutlets
# chown username:group /var/run/rcoutlets

Where username and group are the user and group that will start the rcoutlets service.

Com

The controller script used on the console to issue individual commands must of course be able to communicate its commands to the daemon.

For this, a small TCP server is implemented, which receives command-strings and sends them through the serial connection. The daemon process simply starts the server.

The command-script will either use a direct serial connection or a TCP client to send its commands.

Android App

Create an Android app to control the lights.

Functions

Have two buttons for each switch, one for on and one for off.

Also, have named groups that combine several switches. would be nice if user could define these groups.

GUI-Layout

Have several tabs, one for each group of switches. That is four tabs. Each tab has 5x2 buttons: "on" and "off" buttons for all four switches plus additional buttons for controlling all switches of the group.

Add an additional tab with favorites. Would require to disable tabs if a group is not defined.

Note

Question

  • How do dynamically add components to the UI?

  • How do we add/remove (or hide/show) tabs?

+--------+--------+--------+-------+
| Tab A  | Tab B  | Tab C  | Tab D |
+--------+        +--------+-------+
|                                  |
|   A*  <on>    <off>              |
| - - - - - - - - - - - - - - - - -|
|                                  |
|   A1  <on>    <off>              |
|                                  |
|   A2  <on>    <off>              |
|                                  |
|   A3  <on>    <off>              |
|                                  |
|   A4  <on>    <off>              |
|                                  |
+----------------------------------+

TCP Client

A TCP Client is required to communicate with the Python server. The TCP client only has a single business method sendCommand which is used to send commands to the server.

The TCP client takes the following configuration options:

host

Hostname or IP address of the server.

port

Port number of the server.

timeout

Connection timeout for talking to the server.

Note

Question

Do we have to tear down the TCP connection when the application is moved to the "background" Or when the application quits?

Does it make sense to keep the connection open? Or is it better to open a new connection for each call?

Project Setup

Install the SDK. Apparently the Eclipse Plugin is not strictly required, there is a command line tool.

Manifest File

A manifest file lists all components of the app?

<!-- AndroidManifest.xml -->
<manifest>
    <uses-sdk android:minSdkVersion="8" ... />
</manifest>

Directory Structure:

src/
res/
    drawable-hdpi/
    layout/
    values/
    ...
Activity

The main Activity goes into the src/ folder.

GUI

Android GUI's are defined statically in an XML file like this:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android">
    <Button
        android:id="id"
        android:text="text"
        android:onClick="handleClick"
    />
</LinearLayout>

GUI elements can be connected to controller code in the layout definition. In the Activity implement a method handleClick(View view), in the layout XML assign onClick="handleClick" on a button.

Code

Arduino-Sketch

The complete Arduino sketch looks like this:

/*
* RCOutlets
* Send commands to radio controlled power outlets.
*
* Requires The RCSwitch library
* https://code.google.com/p/rc-switch/
*
* Version 0.2.0
* Alexander Keil <alex@akeil.net>
*/
#include <RCSwitch.h>
const int ERROR_LED_PIN = 7;
const int BUSY_LED_PIN = 6;
const int TRANSMITTER_PIN = 4;
const int HEARTBEAT = 1;
const int INDICATOR_DURATION = 1000;
const int BAUDRATE = 9600;
const char HANDSHAKE_REQUEST = 5;    // ascii ENQ
const char HANDSHAKE_RESPONSE = 6;   // ascii ACK
const char COMMAND_TERMINATOR = 10;  // ascii newline
RCSwitch outlets = RCSwitch();
void setup() {
    Serial.begin(BAUDRATE);
    pinMode(ERROR_LED_PIN, OUTPUT);
    pinMode(BUSY_LED_PIN, OUTPUT);
    pinMode(TRANSMITTER_PIN, OUTPUT);
    outlets.enableTransmit(TRANSMITTER_PIN);
}
void loop() {
    delay(HEARTBEAT);
    updateBusyLED(false);
    updateErrorLED(false);
}
/*
* Handles incoming data over serial.
*
* Recognizes command-messages constructed from:
*  - `group`   A-D
*  - `number`  1-3
*  - `state`   0 or 1
*
* If the COMMAND_TERMINATOR is received and if a valid command
* was recognized that command is applied via `sendCommand()`.
*
* If the HANDSHAKE_REQUEST is received, replies with the HANDSHAKE_RESPONSE.
*/
void serialEvent() {
    static int _pos = 0;
    static char _group = 0;
    static int _number = 0;
    static bool _state = false;
    static bool _error = false;
    char received = Serial.read();
    // handshake
    if (received == HANDSHAKE_REQUEST ) {
        Serial.write(HANDSHAKE_RESPONSE);
    // apply command and reset
    } else if (received == COMMAND_TERMINATOR) {
        if ( _pos == 3 and not _error) {
            sendCommand(_group, _number, _state);
        }
        _group = 0;
        _number = 0;
        _state = false;
        _error = false;
        _pos = 0;
    // build command
    } else {
        switch (_pos) {
            case 0:
                if (received >= 65 and received <= 68) {
                    _group = received;
                } else {
                    _error = true;
                }
                break;
            case 1:
                if (received >= 49 and received <= 51) {
                    _number = received - 48;
                } else {
                    _error = true;
                }
                break;
            case 2:
                if (received == 48) {
                    _state = false;
                } else if (received == 49) {
                    _state = true;
                } else{
                    _error = true;
                }
                break;
        }
        _pos++;
        updateErrorLED(_error);
    }
}
/*
* Send a command over the RF Transmitter.
* Parameters:
*   group    Group identifier          A, B, C or D
*   number   Deivce label              1, 2 or 3
*   state    On (true) or Off (false)
*/
void sendCommand(char group, int number, bool state) {
    updateBusyLED(true);
    if (state){
        outlets.switchOn(group, number);
    } else {
        outlets.switchOff(group, number);
    }
}
/*
* Update BUSY_LED_PIN.
* Remebers the time when it was last turned on and passes it to `updateLED`.
*/
void updateBusyLED(bool turnOnNow) {
    static unsigned long lastSet;
    if (turnOnNow ){
        lastSet = millis();
    }
    updateLED(BUSY_LED_PIN, turnOnNow, lastSet);
}
/*
* Update ERROR_LED_PIN.
* Remebers the time when it was last turned on and passes it to `updateLED`.
*/
void updateErrorLED(bool turnOnNow) {
    static unsigned long lastSet;
    if (turnOnNow ){
        lastSet = millis();
    }
   updateLED(ERROR_LED_PIN, turnOnNow, lastSet);
}
/*
* Update the state of the given `pin`.
*
* Set it to HIGH always if `turnOnNow` is true.
*
* Set it to LOW only if `turnOnNow` is false
* *and* if the time indicated by `lastSet` is at least INDICATOR_DURATION ago.
*/
void updateLED(int pin, bool turnOnNow, unsigned long lastSet) {
    if (turnOnNow) {
        digitalWrite(pin, HIGH);
    } else {
        if( millis() - lastSet > INDICATOR_DURATION) {
            digitalWrite(pin, LOW);
        }
    }
}

Python Script

The complete script looks like this:

#!/usr/bin/python
# -*- coding: utf-8 -*-
'''
Command line app to control RC outlets via an arduino.
The command line interface accepts user input which is
translated into commands for the Arduino. The translated commands
are sent to the Arduino board over a serial connection.
Requires an Arduino running the ``RCOutlets`` sketch to
actually communicate with the RC Outlets over radio.
This script also offers a small TCP server which listens
for commands and forwards them to the Arduino.
The script can be used in these modes:
Without Server
    For each invocation of the script, a new serial connection
    is build and closed when the script exits
Client for the Server
    When the server is running, the script can act as a client
    for the server process. Instead of creating a new serial connection
    for every command, the serial connection is held by the server
    and the script sends its commands to the server.
As a Server
    The script can be used to start the server as a daemon process.
    It can also be used to control the server.
'''
from __future__ import print_function
from __future__ import with_statement
import sys
import os
import argparse
import time
import re
import logging
import atexit
from signal import SIGTERM
try:
    import configparser
except ImportError:
    import ConfigParser as configparser
import socket
import serial
from serial import SerialException
__version__ = '0.3.1'
__author__ = 'akeil'
log = logging.getLogger('rcoutlets')
logging.basicConfig(level=logging.INFO)
# Command line exit codes ----------------------------------------------------
EXIT_OK = 0
EXIT_ERROR = 1
EXIT_COM_ERROR = 2
EXIT_SWITCH_ERROR = 3
# Default Config -------------------------------------------------------------
CFG_DEFAULT_SECTION = 'rcoutlets'
DEFAULT_CONFIG = {
    'baudrate': '9600',
    'port': '/dev/ttyACM0',
    'timeout': '1.0',
    'groups': 'A B C D',
    'handshake_timeout': '5',
    'pidfile': '/var/run/rcoutlets/rcoutlets.pid',
    'use_tcp': '1',
    'tcp_port': '5656',
    'tcp_host': '127.0.0.1',
}
def _cfg_bool(strvalue):
    if not strvalue:
        return False
    elif strvalue.lower() in ('false', 'no', '0'):
        return False
    else:
        return True
CONFIG_TYPES = {
    'baudrate': int,
    'timeout': float,
    'handshake_timeout': float,
    'tcp_port': int,
    'use_tcp': _cfg_bool,
}
# Communicaton over Serial and TCP -------------------------------------------
HANDSHAKE_REQUEST = '\5'.encode('ascii')   # ascii ENQ
HANDSHAKE_RESPONSE = '\6'.encode('ascii')  # ascii ACK
STATE_ON = '1'
STATE_OFF = '0'
# Exceptions -----------------------------------------------------------------
class ComError(Exception):
    pass
class UnknownSwitchError(Exception):
    pass
# Command line app -----------------------------------------------------------
def main(argv=None):
    if argv is None:
        argv = sys.argv[1:]
    parser = _setup_parser()
    args = parser.parse_args(argv)
    config_path = os.path.expanduser('~/.config/rcoutlets.conf')
    cfg = _read_config(config_path)
    options = _merge_options(cfg, args)
    try:
        options.func(options, cfg)
    except (ComError, SerialException) as e:
        print('Communications Error: {}'.format(e))
        return EXIT_COM_ERROR
    except UnknownSwitchError as e:
        print('Unknown switch: {}'.format(e))
        return EXIT_SWITCH_ERROR
    except Exception as e:
        print('General error: {}'.format(e))
        log.exception(e)
        return EXIT_ERROR
    else:
        return EXIT_OK
def _client(options, cfg):
    state = True if options.command == 'on' else False
    if options.use_tcp:
        log.info('Using TCP {o.tcp_host}:{o.tcp_port}'.format(o=options))
        com_adapter = TCPCom(
            options.tcp_host,
            options.tcp_port,
        )
    else:
        log.info('Using serial {o.port}@{o.baudrate}'.format(o=options))
        com_adapter = SerialCom(
            options.port,
            options.baudrate,
            options.timeout,
            options.handshake_timeout
        )
    with RCOutlets(com_adapter, options, cfg) as outlets:
        for switch in outlets.mget(options.name):
            switch.set(state)
def _server(options, unused_cfg):
    if options.action == 'start':
        com_adapter = SerialCom(
            options.port,
            options.baudrate,
            options.timeout,
            options.handshake_timeout
        )
        log.info('Starting TCP server on port {}.'.format(options.tcp_port))
        server = TCPServer(options.tcp_port, com_adapter)
        if options.no_daemon:
            server.serve_forever()
        else:
            start_daemon(server.serve_forever, options.pidfile)
    elif options.action == 'stop':
        stop_daemon(options.pidfile)
        log.info('TCP server stopped.')
    else:
        raise ValueError('Unknown server action {!r}'.format(options.action))
def _merge_options(cfg, args):
    '''Merge Config-settings and command line options.
    Return a new Namespace object which combines both sources.
    '''
    opts = argparse.Namespace()
    no_conversion = lambda x: x
    for name in cfg.options(CFG_DEFAULT_SECTION):
        value = cfg.get(CFG_DEFAULT_SECTION, name)
        convert = CONFIG_TYPES.get(name, no_conversion)
        setattr(opts, name, convert(value))
        log.debug('{}: {} ({})'.format(name, getattr(opts, name), value))
    for name, value in vars(args).items():
        if value is not None or name not in vars(opts):
            setattr(opts, name, value)
    return opts
def _setup_parser():
    parser = argparse.ArgumentParser(
        prog='rcoutlets',
        description=('Send commands to an Arduino'
         ' to control RC power outlets.'
        ' Requires that the Arduino is connected via serial'
        ' and runs a compatible programm.'
        )
    )
    subs = parser.add_subparsers()
    common = argparse.ArgumentParser(add_help=False)
    common.add_argument(
        '-p', '--port',
        help=('Identifier of the serial port'
            ' on which the Arduino is connected.'),
    )
    common.add_argument(
        '--tcp-port',
        type=int,
        help=('Set the TCP port for server or client.')
    )
    client = subs.add_parser(
        'client',
        parents=[common,],
        help='Control the RCOutlets.'
    )
    client.add_argument(
        'name',
        nargs='?',
        help=('Name or identifier of the switch to control.'
            ' Valid names are A1-D3.'
            ' Can also be the name of an alias that is defined'
            ' in the configuration file.'
            ' Wildcards ("*") are allowed.'
        ),
    )
    client.add_argument(
        'command',
        nargs='?',
        choices=('on', 'off'),
        default='on',
        help=('The command that should be send to the switch(es).'
            ' Defaults to "on" if none is specified.'),
    )
    client.set_defaults(func=_client)
    server = subs.add_parser(
        'server',
        parents=[common,],
        help='Control the TCP server.'
    )
    server.add_argument(
        'action',
        choices=('start', 'stop'),
        help=('Start or stop the TCP server.'),
    )
    server.add_argument(
        '--no-daemon',
        action='store_true',
        help=('Keep in foreground, do not fork.'),
    )
    server.set_defaults(func=_server)
    return parser
def _read_config(path):
    cfg = configparser.ConfigParser()
    s = CFG_DEFAULT_SECTION
    cfg.add_section(s)
    for key, value in DEFAULT_CONFIG.items():
        cfg.set(s, key, value)
    cfg.read(path)
    return cfg
# Daemon ---------------------------------------------------------------------
def start_daemon(daemon_func, pidfile, stdin=None, stdout=None, stderr=None):
    '''Run the given callable in a daemon process,
    detached from the calling process.
    :param callable daemon_func:
        A function or any other *callable* that is the implementation
        for the daemon.
        The ``daemon_func`` is called once inside the daemon process
        and the daemon will terminate when the function returns.
    :param str pidfile:
        Where to look for this daemons ``pidfile``.
        The PID of the daemon process is written to that file and the file
        is used to determine whether an instance of this daemon
        is already running.
    :param str stdin:
        Where to redirect the daemons ``stdin``. Defaults to ``/dev/null``.
    :param str stdout:
        Where to redirect the daemons ``stdout``. Defaults to ``/dev/null``.
    :param str stderr:
        Where to redirect the daemons ``stderr``. Defaults to ``/dev/null``.
    :raises:
        ValueError if a ``pidfile`` already exists
        and indicates that there is already a running instance of this daemon.
    :raises:
        IOError if the ``pidfile`` is not writeable.
    '''
    stdin = stdin or '/dev/null'
    stdout = stdout or '/dev/null'
    stderr = stderr or '/dev/null'
    # check if an instance is already running
    # if so, exit with error
    existing_pid = _read_pidfile(pidfile)
    if existing_pid:
        raise ValueError('Found existing pidfile with pid {}. Daemon already running?'.format(existing_pid))
    # first fork
    try:
        pid = os.fork()
        if pid > 0:
            sys.exit(0)  # exit parent process
    except OSError as e:
        print('fork failed: [{}] {!r}'.format(e.errno, e.strerror))
        sys.exit(1)
    # decouple from parent env
    os.chdir('/')
    os.setsid()
    os.umask(0)
    # second fork
    try:
        pid = os.fork()
        if pid > 0:
            sys.exit(0)  # exit 2d parent
    except OSError as e:
        print('fork failed: [{}] {!r}'.format(e.errno, e.strerror))
        sys.exit(1)
    # create the pidfile
    pid = str(os.getpid())
    try:
        with open(pidfile, 'w+') as f:
            f.write('{}\n'.format(pid))
    except IOError as e:
        print('Failed to write pidfile at: {!r}'.format(pidfile))
        raise
    print('forked to {}, pidfile is {!r}'.format(pid, pidfile))
    # remove pidfile when complete
    def delpid():
        os.remove(pidfile)
    atexit.register(delpid)
    # redirect output
    sys.stdout.flush()
    sys.stderr.flush()
    si = open(stdin, 'r')
    so = open(stdout, 'a+')
    se = open(stderr, 'a+')
    os.dup2(si.fileno(), sys.stdin.fileno())
    os.dup2(so.fileno(), sys.stdout.fileno())
    os.dup2(se.fileno(), sys.stderr.fileno())
    try:
        daemon_func()
        sys.exit(0)
    except Exception:
        sys.exit(1)
def stop_daemon(pidfile):
    '''Stop the daemon process.
    Attempts to kill the process indicated by ``pidfile``.
    Does nothing if the pidfile is empty or does not exist
    (i.e. if we can assume that there is no daemon running).
    :param str pidfile:
        The path to the pidfile
    :raises:
        ValueError is raised if we fail to kill the daemon process.
    '''
    pid = _read_pidfile(pidfile)
    if not pid:
        print('pidfile does not exist or is empty. Daemon not running?')
        return
    else:
        try:
            while True:
                os.kill(pid, SIGTERM)
        except OSError as e:
            errorstr = str(e)
            if errorstr.find('No such process') > 0:
                # clean up
                if os.path.exists(pidfile):
                    os.remove(pidfile)
            else:
                raise ValueError('Failed to kill daemon process: {!r}'.format(errorstr))
def _read_pidfile(pidfile_path):
    try:
        with open(pidfile_path) as fp:
            pid = int(fp.read().strip())
    except IOError:
        pid = None
    return pid
# Models ---------------------------------------------------------------------
class RCOutlets(object):
    '''Communicates with the Arduino via a serial connection
    or communicates over TCP with the Server.
    Either use the `send_command` method to control the outlets
    or get references to a individual :class:`Switch`
    with :method:`get` or :method:`mget`.
    RCOutlets can be used as a context manager like this:
    .. code:: python
        with RCOutlets() as outlets:
            outlets.send_command('A10')
        # is equivalent to:
        outlets = RCOutlets()
        try:
            outlets.begin()
            outlets.send_command('A10')
        finally:
            outlets.end()
    Whether an immediate serial connection or the TCP-relay
    is used is determined by passing the respective ``com_adapter``.
    '''
    def __init__(self, com_adapter, options, cfg):
        self._switches = {}
        used_groups = options.groups.split()
        for group in used_groups:
            for number in range(1, 4):
                key = '{}{}'.format(group, number)
                self._switches[key] = Switch(group, number, self)
        self.alias = {}
        try:
            for aliasname in cfg.options('alias'):
                switchkeys = cfg.get('alias', aliasname).split()
                self.alias[aliasname] = [self._switches[key] for key in switchkeys]
        except configparser.NoSectionError:
            pass  # no sections defined
        self._com = com_adapter
        # patterns for mget()
        self._number_wildcard = re.compile('^[A-D]\*$')
        self._group_wildcard = re.compile('^\*[1-3]$')
        self._switch_name = re.compile('^[A-D][1-3]$')
    def __enter__(self):
        self.begin()
        return self
    def __exit__(self, exc_type, exc_val, exc_tb):
        suppress_exc = False
        self.end()
        return suppress_exc
    def begin(self):
        '''Open the Serial connection
        and wait for the Arduino to be ready.
        After this method exits, commands can be send.
        Raises an error if no connection can be established
        within `handshake_timeout` seconds.
        This method does not have to be called when using
        RCOutlets as the *context manager* in a ``with`` statement.
        '''
        self._com.open()
    def end(self):
        '''Close the connection to the Arduino.
        No more commands can be send after this method was called,
        unless `begin` is called.
        This method does not have to be called when using
        RCOutlets as the *context manager* in a ``with`` statement.
        '''
        self._com.close()
    def send_command(self, cmd):
        self._com.send(cmd)
    def get(self, name):
        '''Get a single :class:`Switch` by its name.'''
        try:
            return self._switches[name]
        except KeyError:
            raise UnknownSwitchError(
                'No switch with name {!r}.'.format(name))
    def mget(self, expr):
        '''Get a list of switches, whose names match ``expr``.
        ``expr`` is a string that contains one or more
        switch identifiers (A1-D3) and/or alias names separated by whitespace.
        The ``expr`` argument can contain a "*" wildcard
        which matches as follows:
        A* ... D*
            matches all switches in a group
        *1 ... *3
            matches switch n in all groups
        any*
            matches groups that start with "any"
        *thing
            matches groups ending in "thing"
        *text*
            matches groups starting with, ending in or containing "text"
        For example::
            "A* dining" -> A1, A2, A3 + members of alias "dining"
            "A1 B2 D2"  -> these three switches
            "B* *2"     -> B1, B2, B3 and A2, B2, C2, D2
            "*"         -> Everything.
        '''
        # helper, finds and yield switches by a single pattern
        def _find(searchstr):
            if searchstr == '*':
                for switch in self._switches.values():
                    yield switch
            elif self._number_wildcard.match(searchstr):  # e.g. A*
                group = searchstr[0]
                for number in range(1,3):
                    yield self.get('{}{}'.format(group, number))
            elif self._group_wildcard.match(searchstr):  # e.g. *1
                number = searchstr[1]
                for group in ('A', 'B', 'C', 'D'):
                    yield self.get('{}{}'.format(group, number))
            else:
                try:
                    # exact switch name?
                    yield self._switches[searchstr]
                except KeyError:
                    try:
                        # excat alias?
                        for switch in self.alias[searchstr]:
                            yield switch
                    except KeyError:
                        # alias by pattern?
                        pattern = '^{}$'.format(
                            searchstr.replace('*', '[a-zA-Z0-9]*?'))
                        regex = re.compile(pattern)
                        for alias, switches in self.alias.items():
                            if regex.match(alias):
                                for switch in switches:
                                    yield switch
        # helper, splits expr to separate patterns and _finds() them all
        def _generate_list():
            for part in expr.split():
                for switch in _find(part):
                    yield switch
        return [switch for switch in _generate_list()]
class Switch(object):
    _COMMAND_TEMPLATE = '{s.group}{s.number}{state}'
    def __init__(self, group, number, control):
        self.group = group
        self.number = number
        self._control = control
    def _send_command(self, state):
        log.info('Switch {}{} {}.'.format(
            self.group, self.number,
            'ON' if state else 'OFF'
        ))
        state_command = STATE_ON if state else STATE_OFF
        cmd = Switch._COMMAND_TEMPLATE.format(s=self, state=state_command)
        self._control.send_command(cmd)
    def on(self):
        self._send_command(True)
    def off(self):
        self._send_command(False)
    def set(self, state):
        self._send_command(bool(state))
class SerialCom(object):
    '''Sends commands over the serial port.
    Can be used by RCOutlets to communicate directly over serial
    or by the TCPServer to relay commands received via TCP
    to the serial port.
    '''
    def __init__(self, port, baudrate, timeout, handshake_timeout):
        self._ser = serial.Serial()
        self._ser.port = port
        self._ser.baudrate = baudrate
        self._ser.timeout = timeout
        self.handshake_timeout = handshake_timeout
    def open(self):
        '''Open the Serial connection
        and wait for the Arduino to be ready.
        After this method exits, commands can be send.
        Raises an error if no connection can be established
        within `handshake_timeout` seconds.
        This method does not have to be called when using
        RCOutlets as the *context manager* in a ``with`` statement.
        '''
        log.info('Open connection over serial.')
        self._ser.open()
        log.info('Waiting for handshake...')
        self._handshake()
        log.info('Connection established.')
    def _handshake(self):
        '''Arduino will reset when a serial connection is opened.
        Wait for it to come up.
        '''
        handshake_done = False
        started = time.time()
        while not handshake_done:
            self._ser.write(HANDSHAKE_REQUEST)
            response = self._ser.read(1)
            handshake_done = response == HANDSHAKE_RESPONSE
            time.sleep(0.01)
            time_passed = time.time() - started
            if time_passed > self.handshake_timeout:
                raise ComError((
                        'Handshake timed out'
                        ' after {} seconds.'
                    ).format(int(time_passed)))
    def close(self):
        '''Close the connection to the Arduino.
        No more commands can be send after this method was called,
        unless `begin` is called.
        This method does not have to be called when using
        RCOutlets as the *context manager* in a ``with`` statement.
        '''
        self._ser.close()
    def send(self, cmd):
        '''Send the given ``cmd`` string.
        the string will be ascii encoded.
        :raises:
            ComError if the connection fails.
        '''
        clean_cmd = cmd.strip()
        ascii_cmd = '{}\n'.format(clean_cmd).encode('ascii')
        log.info('Send command [serial]: {!r}'.format(ascii_cmd))
        try:
            self._ser.write(ascii_cmd)
        except ValueError as e:  # port not open
            raise ComError(str(e))
# TCP Server and Client ------------------------------------------------------
class TCPServer():
    '''A TCP server which forwards commands to the serial port.
    The server holds a single serial connection and forwards each command
    it received 1:1 to that connection.
    Commands are simple strings, terminated by newlines.
    :var int port:
        The port on which the server should listen.
    :var object com_adapter:
        The receiver for relayed commands.
        Should be an instance of :class:`SerialCom`.
    '''
    def __init__(self, port, com_adapter):
        host = ''
        self.address = (host, port)
        self.bufsize = 16
        self._com = com_adapter
    def serve_forever(self):
        self._com.open()
        srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        srv.bind((self.address))
        max_connections = 1
        srv.listen(max_connections)
        print('Started TCP Server at address {}'.format(self.address))
        while True:
            # wait for incoming connection
            connection, client_address = srv.accept()
            log.info('Connected to {}'.format(client_address))
            try:
                self.forward(connection)
            finally:
                connection.close()
        if self._com:
            self._com.close()
        srv.close()
    def forward(self, connection):
        received = ''
        while True:
            parts = connection.recv(self.bufsize).decode('ascii')
            if parts:
                log.info('Received [TCP]: {!r}'.format(parts))
                received += parts
            else:  # no more incoming data
                break
        for cmd in received.splitlines():
            self._com.send(cmd)
class TCPCom(object):
    '''Can acts as a ``com_adapter`` for RCOutlets to send commands
    over TCP instead of the serial port.
    Intended as a client for the TCPServer.'''
    def __init__(self, host, port):
        self.host = host
        self.port = port
        self.address = (self.host, self.port)
        self.timeout = 5.0  # seconds
        self.cli = None
    def open(self):
        self.cli = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.cli.settimeout(self.timeout)
        self.cli.connect((self.address))
    def close(self):
        self.cli.close()
    def send(self, cmd):
        # catch timeouts?
        clean_cmd = cmd.strip()
        log.info('Send command [TCP]: {!r}'.format(clean_cmd))
        self.cli.sendall('{}\n'.format(clean_cmd).encode('ascii'))
if __name__ == '__main__':
    sys.exit(main())