f02018c6b867d97c38f2ec23497faa8fe0bda24f
[mqtt-noolite.git] / nmd / nl_mqtt.py
1 import time
2 import json
3 import logging
4 import asyncio
5
6 from hbmqtt.client import MQTTClient
7 from hbmqtt.mqtt.constants import QOS_0, QOS_1, QOS_2
8
9 from .nl_serial import NooliteSerial
10 from .utils import Singleton
11
12 from uuid import uuid1
13 from socket import gethostname
14
15 logger = logging.getLogger(__name__)
16
17 client_id = gethostname()+'-'+str(uuid1())
18
19 INPUT_TOPIC = '%s/send'
20 OUTPUT_TOPIC = '%s/receive'
21
22 class MqttDriver(metaclass=Singleton):
23     def __init__(self, mtrf_tty_name, loop, mqtt_uri='mqtt://127.0.0.1/', mqtt_topic='noolite', commands_delay=0.1):
24         self.mqtt_client = MQTTClient(client_id=client_id,config={'auto_reconnect': True})
25         self.mqtt_uri = mqtt_uri
26         self.commands_delay = commands_delay
27         self.noolite_serial = NooliteSerial(loop=loop, tty_name=mtrf_tty_name,
28                                             input_command_callback_method=self.input_serial_data)
29         self.commands_to_send_queue = asyncio.Queue()
30         self.read_topic = INPUT_TOPIC % mqtt_topic
31         self.subscriptions = [
32             ( self.read_topic+'/#', QOS_0),
33         ]
34         self.write_topic = OUTPUT_TOPIC % mqtt_topic
35         loop.create_task(self.send_command_to_noolite())
36
37     async def run(self):
38         await self.mqtt_client.connect(self.mqtt_uri)
39         await self.mqtt_client.subscribe(self.subscriptions)
40
41         while True:
42             logger.info('Waiting messages from mqtt...')
43             message = await self.mqtt_client.deliver_message()
44
45             topic = message.topic
46             payload = message.publish_packet.payload.data
47
48             logger.info('In message: {}\n{}'.format(topic, payload))
49
50             if topic.startswith(self.read_topic):
51               subtopic = topic[len(self.read_topic)+1:]
52               print(subtopic)
53
54               if subtopic == '':
55                 try:
56                   payload = json.loads(payload.decode())
57                 except Exception as e:
58                   logger.exception(e)
59                   continue
60                 await self.commands_to_send_queue.put(payload)
61               else:
62                 try:
63                   address = subtopic.split('/')
64                   if len(address)==2:
65                     channel = int(address[0])
66                     command = address[1]
67                     id = None
68                   elif len(address)==3:
69                     channel = int(address[0])
70                     command = address[2]
71                     id = address[1]
72                   elif len(address)==1:
73                     command = address[0]
74                     channel = None
75                     id = None
76
77                   command = command.lower()  
78
79                   print("%s: %s (%s)" % (command,channel,id))
80
81                   mtrf_command = { "ch": channel }
82                   if id == '.' or id == 'TX-F':
83                     mtrf_command["mode"] = 2
84                   elif id =='TX':
85                     mtrf_command["mode"] = 0
86                   elif id =='RX':
87                     mtrf_command["mode"] = 1
88                   elif id =='RX-F':
89                     mtrf_command["mode"] = 3
90                   elif id:
91                     mtrf_command["mode"] = 2
92                     mtrf_command["ctr"] = 8
93                     mtrf_command["id0"] = int(id[0:2],16)
94                     mtrf_command["id1"] = int(id[2:4],16)
95                     mtrf_command["id2"] = int(id[4:6],16)
96                     mtrf_command["id3"] = int(id[6:8],16)
97                   else:
98                     mtrf_command["mode"] = 0                    
99
100                   if command == "power":
101                     payload = payload.decode('utf-8').lower()
102                     print( "command: POWER " + payload )
103                     if payload == "off" or payload == "0":
104                       mtrf_command["cmd"] = 0
105                     else:  
106                       mtrf_command["cmd"] = 2
107
108                   elif command == "on":
109                     mtrf_command["cmd"] = 2
110
111                   elif command == "off":
112                     mtrf_command["cmd"] = 0
113
114                   elif command == "brightness":
115                     mtrf_command["cmd"] = 6
116                     mtrf_command["d0"] = int(float(payload))
117                       
118                   elif command == "dimmer":
119                     mtrf_command["cmd"] = 6
120                     mtrf_command["d0"] = int(round(float(payload)*255/100))
121
122                   elif command == "state":
123                     mtrf_command["cmd"] = 128
124
125                   elif command == "load_preset":
126                     mtrf_command["cmd"] = 7
127
128                   elif command == "save_preset":
129                     mtrf_command["cmd"] = 8
130
131                   elif command == "temp_on":
132                     delay = (int(payload) + 3)//5
133                     mtrf_command["cmd"] = 25
134                     mtrf_command["d0"] = delay % 256
135                     mtrf_command["d1"] = delay // 256
136                     mtfr_command["fmt"] = 6
137
138                   elif command == "bind":
139                     mtrf_command["cmd"] = 15
140
141                   elif command == "unbind":
142                     mtrf_command["cmd"] = 9
143
144                   elif command == "service":
145                     mtrf_command["cmd"] = 131
146
147                   elif command == "start_bind":
148                     mtrf_command["ctr"] = 3
149                     mtrf_command["cmd"] = 0
150                     
151                   elif command == "stop_bind":
152                     mtrf_command["ctr"] = 4
153                     mtrf_command["cmd"] = 0
154
155                   elif command == "clear_bind":
156                     mtrf_command["ctr"] = 5
157                     mtrf_command["cmd"] = 0
158
159                   elif command == "unbind_addr":
160                     mtrf_command["ctr"] = 7
161                     mtrf_command["cmd"] = 0
162
163                 except Exception as e:
164                   logger.exception(e)
165                   continue
166                 await self.commands_to_send_queue.put(mtrf_command)
167                  
168
169     async def send_command_to_noolite(self):
170         last_command_send_time = 0
171         while True:
172             logger.info('Waiting commands to send...')
173             payload = await self.commands_to_send_queue.get()
174             logger.info('Get command from queue: {}'.format(payload))
175
176             # Формируем и отправляем команду к noolite
177             noolite_cmd = self.payload_to_noolite_command(payload)
178
179             if time.time() - last_command_send_time < self.commands_delay:
180                 logger.info('Wait before send next command: {}'.format(
181                     self.commands_delay - (time.time() - last_command_send_time)))
182                 await asyncio.sleep(self.commands_delay - (time.time() - last_command_send_time))
183
184             try:
185                 await self.noolite_serial.send_command(**noolite_cmd)
186             except TypeError as e:
187                 logger.exception(str(e))
188             last_command_send_time = time.time()
189
190     async def input_serial_data(self, command):
191         logger.info('Pub command: {}'.format(command))
192         command = self.noolite_response_to_payload(command.to_list())
193         try:
194           topic = "%s/%s/%s" % (self.write_topic, command['ch'], command['id'])
195         except:
196           topic = "%s/%s" % (self.write_topic, command['ch'])  
197         await self.mqtt_client.publish(topic=topic, message=json.dumps(command).encode())
198
199     @staticmethod        
200     def payload_to_noolite_command(payload):
201         return payload
202
203     @staticmethod
204     def noolite_response_to_payload(payload):
205
206         message = {}
207
208         try:
209           mode = [ 'TX', 'RX', 'TX-F', 'RX-F', 'SERVICE', 'FIRMWARE' ] [payload[1]]
210           message['mode'] = mode
211         finally:
212           None  
213
214         try:
215           message['ctr'] = [ 'OK', 'NORESP', 'ERROR', 'BOUND' ] [payload[2]]
216         finally:
217           None  
218
219         ch = payload[4]
220         message['ch'] = ch
221
222         cmd = payload[5]
223         message['cmd'] = cmd
224
225         fmt = payload[6]
226         message['fmt'] = fmt
227
228         data = payload[7:11]
229         message['data'] = data
230
231         if payload[1] >= 2:
232           message['id'] = '%0.2X%0.2X%0.2X%0.2X' % (payload[11], payload[12], payload[13], payload[14])
233
234         if cmd == 0:
235           message['command'] = 'OFF'
236         elif cmd == 1: 
237           message['command'] = 'BRIGHT_DOWN'
238         elif cmd == 2: 
239           message['command'] = 'ON'
240         elif cmd == 3: 
241           message['command'] = 'BRIGHT_UP'
242         elif cmd == 4: 
243           message['command'] = 'SWITCH'
244         elif cmd == 5: 
245           message['command'] = 'SWITCH'
246         elif cmd == 5: 
247           message['command'] = 'BRIGHT_BACK'
248         elif cmd == 5: 
249           message['command'] = 'BRIGHT_BACK'
250         elif cmd == 6: 
251           message['command'] = 'SET_BRIGHTNESS'
252         elif cmd == 7: 
253           message['command'] = 'LOAD_PRESET'
254         elif cmd == 8: 
255           message['command'] = 'SAVE_PRESET'
256         elif cmd == 9: 
257           message['command'] = 'UNBIND'
258         elif cmd == 10: 
259           message['command'] = 'STOP_REG'
260 #        elif cmd == 11: 
261 #          message['command'] = 'BRIGHTNESS_STEP_DOWN'
262 #        elif cmd == 12: 
263 #          message['command'] = 'BRIGHTNESS_STEP_UP'
264 #        elif cmd == 13: 
265 #          message['command'] = 'BRIGHT_REG'
266         elif cmd == 15: 
267           message['command'] = 'BIND'
268         elif cmd == 16: 
269           message['command'] = 'ROLL_COLOUR'
270         elif cmd == 17: 
271           message['command'] = 'SWITCH_COLOUR'
272         elif cmd == 18: 
273           message['command'] = 'SWITCH_MODE'
274         elif cmd == 19: 
275           message['command'] = 'SPEED_MODE_BACK'
276         elif cmd == 20: 
277           message['command'] = 'BATTERY_LOW'
278         elif cmd == 21: 
279           message['command'] = 'SENS_TEMP_HUMI'
280           t = data[0] + 256*(data[1] % 16)
281           if (data[1] % 16) // 8:
282             t = -(4096 - t )
283           t = t / 10  
284           message['t'] = t
285           dev_type = (data[1] // 16) % 8
286           try:
287             message['dev_type'] = [ 'RESERVED', 'PT112', 'PT111' ][dev_type]
288           finally:
289             None  
290           message['dev_battery_low'] = (data[1] // 128)
291           if dev_type == 2:
292             h = data[2]
293             message['h'] = h
294           message['aux'] = data[3]  
295         elif cmd == 25: 
296           message['command'] = 'TEMPORARY_ON'
297           if fmt == 5:
298             message['delay'] = data[0] * 5
299           elif fmt == 6:
300             message['delay'] = data[0] * 5 + data[1]*5*256
301         elif cmd == 26: 
302           message['command'] = 'MODES'
303         elif cmd == 128: 
304           message['command'] = 'READ_STATE'
305         elif cmd == 129: 
306           message['command'] = 'WRITE_STATE'
307         elif cmd == 130: 
308           message['command'] = 'SEND_STATE'
309           dev_type = data[0]
310           if dev_type == 5:
311             message['dev_type'] = 'SLU-1-300'
312           message['dev_firmware'] = data[1]
313           if fmt == 0:
314             dev_state = data[2] % 16
315             try:
316               message['dev_state'] = [ 'OFF', 'ON', 'TEMPORARY_ON' ][dev_state]
317               message['POWER'] = message['dev_state']
318             finally:
319               None
320             dev_mode = data[2] // 128
321             if dev_mode:
322               message['dev_binding'] = 'ON'
323             message['brightness'] = data[3]
324             message['DIMMER'] = int(round(data[3]*100/255))
325           elif fmt == 1:
326             message['dev_aux'] = data[2]
327             message['dev_legacy'] = data[3]
328           elif fmt == 2:
329             message['dev_free'] = data[3]
330             message['dev_free_legacy'] = data[2]    
331         elif cmd == 131: 
332           message['command'] = 'SERVICE'
333         elif cmd == 132: 
334           message['command'] = 'CLEAR_MEMORY'
335           
336         return message
337