RCOutlets

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:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
#!/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())