File: pilight.py

package info (click to toggle)
python-pilight 0.1.1-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 148 kB
  • sloc: python: 398; makefile: 4
file content (190 lines) | stat: -rw-r--r-- 7,943 bytes parent folder | download | duplicates (2)
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
"""This module implements a client to interface the pilight-daemon.

More information about pilight is here: https://www.pilight.org/.
"""

import threading
import socket
import json
import logging


class Client(threading.Thread):

    """This client interfaces with the pilight-daemon (https://www.pilight.org/).

    Sending and receiving codes is implemented in an asychronous way.
    A callback function can be defined that reacts on received data.

    All pilight-send commands can be used by this client. Documentation
    can be found here https://wiki.pilight.org/doku.php/psend.
    Also check https://manual.pilight.org/en/api.

    :param host: Address where the pilight-daemon intance runs
    :param port: Port of the pilight-daemon on the host
    :param timeout: Time until a time out exception is raised when connecting
    :param recv_ident: The identification of the receiver to sucribe
    to the pilight-daemon topics (https://manual.pilight.org/en/api)
    :param recv_codes_only: If True: only call the callback function when the
    pilight-daemon received a code, not for status messages etc.
    :param veto_repeats: If True: only call the callback function when the
    pilight-daemon received a new code, not the same code repeated.
    Repeated codes happen quickly when a button is pressed.
    """

    # pylint: disable=too-many-arguments, too-many-instance-attributes

    def __init__(self, host='127.0.0.1', port=5000, timeout=1,
                 recv_ident=None, recv_codes_only=True, veto_repeats=True):
        """Initialize the pilight client.

        The readout thread is not started automatically.
        """
        threading.Thread.__init__(self)
        self.daemon = True
        self._stop_thread = threading.Event()
        self._lock = threading.Lock()
        self.recv_codes_only = recv_codes_only
        self.veto_repeats = veto_repeats

        # Identify client (https://manual.pilight.org/en/api)
        client_identification_sender = {
            "action": "identify",
            "options": {
                # To get CPU load and RAM of pilight daemon, is neverless
                # ignored by daemon ...
                "core": 0,
                "receiver": 0,  # To receive the RF data received by pilight
                "config": 0
            }
        }

        if recv_ident:
            client_identification_receiver = recv_ident
        else:
            client_identification_receiver = {
                "action": "identify",
                "options": {
                    "core": 0,  # To get CPU load and RAM of pilight daemon
                    # To receive the RF data received by pilight
                    "receiver": 1,
                    "config": 0,
                    "forward": 0
                }
            }

        # Open 2 socket connections, one for sending one for receiving data
        # That is the simplest approach to allow asynchronus communication with
        # the pilight daemon
        self.receive_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.send_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

        # Timeout to allow receiver thread termination and to restrict blocking
        # connection time
        self.receive_socket.settimeout(timeout)
        self.send_socket.settimeout(timeout)

        self.receive_socket.connect((host, port))
        self.send_socket.connect((host, port))

        # Identify this clients sockets at the pilight-deamon
        self.receive_socket.send(
            json.dumps(client_identification_receiver).encode())
        answer_1 = json.loads(self.receive_socket.recv(1024).decode())
        self.send_socket.send(
            json.dumps(client_identification_sender).encode())
        answer_2 = json.loads(self.send_socket.recv(1024).decode())

        # Check connections are acknowledged
        if ('success' not in answer_1['status'] or
                'success' not in answer_2['status']):
            raise IOError(
                'Connection to the pilight daemon failed. Reply %s, %s',
                answer_1, answer_2)

        self.callback = None

    def set_callback(self, function):
        """Function to be called when data is received."""
        self.callback = function

    def stop(self):
        """Called to stop the reveiver thread."""
        self._stop_thread.set()
        # f you want to close the connection in a timely fashion,
        # call shutdown() before close().
        with self._lock:  # Receive thread might use the socket
            self.receive_socket.shutdown(socket.SHUT_RDWR)
            self.receive_socket.close()

        self.send_socket.shutdown(socket.SHUT_RDWR)
        self.send_socket.close()

    def run(self):  # Thread for receiving data from pilight
        """Receiver thread function called on Client.start()."""
        logging.debug('Pilight receiver thread started')
        if not self.callback:
            raise RuntimeError('No callback function set, cancel readout thread')

        def handle_messages(messages):
            """Call callback on each receive message."""
            for message in messages:  # Loop over received  messages
                if message:  # Can be empty due to splitlines
                    message_dict = json.loads(message.decode())
                    if self.recv_codes_only:
                        # Filter: Only use receiver messages
                        if 'receiver' in message_dict['origin']:
                            if self.veto_repeats:
                                if message_dict['repeats'] == 1:
                                    self.callback(message_dict)
                            else:
                                self.callback(message_dict)
                    else:
                        self.callback(message_dict)

        while not self._stop_thread.isSet():
            try:  # Read socket in a non blocking call and interpret data
                # Sometimes more than one JSON object is in the stream thus
                # split at \n
                with self._lock:
                    messages = self.receive_socket.recv(1024).splitlines()
                handle_messages(messages)
            except (socket.timeout, ValueError):  # No data
                pass
        logging.debug('Pilight receiver thread stopped')

    def send_code(self, data, acknowledge=True):
        """Send a RF code known to the pilight-daemon.

        For protocols look at https://manual.pilight.org/en/api.
        When acknowledge is set, it is checked if the code was issued.
        :param data: Dictionary with the data
        :param acknowledge: Raise IO exception if the code is not
        send by the pilight-deamon
        """
        if "protocol" not in data:
            raise ValueError(
                'Pilight data to send does not contain a protocol info. '
                'Check the pilight-send doku!', str(data))

        # Create message to send
        message = {
            "action": "send",  # Tell pilight daemon to send the data
            "code": data,
        }

        # If connection is closed IOError is raised
        self.send_socket.sendall(json.dumps(message).encode())

        if acknowledge:  # Check if command is acknowledged by pilight daemon
            messages = self.send_socket.recv(1024).splitlines()
            received = False
            for message in messages:  # Loop over received messages
                if message:  # Can be empty due to splitlines
                    acknowledge_message = json.loads(message.decode())
                    # Filter correct message
                    if ('status' in acknowledge_message and
                            acknowledge_message['status'] == 'success'):
                        received = True
            if not received:
                raise IOError('Send code failed. Code: %s', str(data))