Добавлена нечувствительность к регистру ID/режимов устройств. Доработано протоколиров...
[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: {} -> {}'.format(topic, payload.decode('utf-8')))
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                   mtrf_command = None
64                   address = subtopic.split('/')
65                   if len(address)==2:
66                     channel = int(address[0])
67                     command = address[1]
68                     id = None
69                   elif len(address)==3:
70                     channel = int(address[0])
71                     command = address[2]
72                     id = address[1]
73                   elif len(address)==1:
74                     command = address[0]
75                     channel = None
76                     id = None
77                   command = command.lower()  
78                   id = id.upper()
79                   print("%s: %s (%s)" % (command,channel,id))
80                   if command == "on":
81                     if id == '.' or id == 'TX-F':
82                       mtrf_command = { "mode": 2, "ch": channel, "cmd": 2, "ctr": 1 }
83                     elif id:
84                       mtrf_command = { "mode": 2, "ch": channel, "cmd": 2, "id0": int(id[0:2],16), "id1": int(id[2:4],16), "id2": int(id[4:6],16), "id3": int(id[6:8],16), "ctr": 8 }
85                     elif id == 'TX':
86                       mtrf_command = { "mode": 0, "ch": channel, "cmd": 2 }
87                     else:
88                       mtrf_command = { "mode": 0, "ch": channel, "cmd": 2 }
89                   elif command == "off":
90                     if id == '.' or id == 'TX-F':
91                       mtrf_command = { "mode": 2, "ch": channel, "cmd": 0, "ctr": 1 }
92                     elif id:
93                       mtrf_command = { "mode": 2, "ch": channel, "cmd": 0, "id0": int(id[0:2],16), "id1": int(id[2:4],16), "id2": int(id[4:6],16), "id3": int(id[6:8],16), "ctr": 8 }
94                     elif id == 'TX':
95                       mtrf_command = { "mode": 0, "ch": channel, "cmd": 0 }
96                     else:
97                       mtrf_command = { "mode": 0, "ch": channel, "cmd": 0 }
98                   elif command == "brightness":
99                     brightness = int(payload)
100                     if id == '.' or id == 'TX-F':
101                       mtrf_command = { "mode": 2, "ch": channel, "cmd": 6, "d0": brightness, "ctr": 1 }
102                     elif id:
103                       mtrf_command = { "mode": 2, "ch": channel, "cmd": 6, "d0": brightness, "id0": int(id[0:2],16), "id1": int(id[2:4],16), "id2": int(id[4:6],16), "id3": int(id[6:8],16), "ctr": 8 }
104                     elif id == 'TX':
105                       mtrf_command = { "mode": 0, "ch": channel, "cmd": 6, "d0": brightness }
106                     else:
107                       mtrf_command = { "mode": 0, "ch": channel, "cmd": 6, "d0": brightness }
108                   elif command == "state":
109                     if id == '.' or id == 'TX-F':
110                       mtrf_command = { "mode": 2, "ch": channel, "cmd": 128, "ctr": 1 }
111                     elif id:
112                       mtrf_command = { "mode": 2, "ch": channel, "cmd": 128, "id0": int(id[0:2],16), "id1": int(id[2:4],16), "id2": int(id[4:6],16), "id3": int(id[6:8],16), "ctr": 8 }
113                   elif command == "load_preset":
114                     if id == '.' or id == 'TX-F':
115                       mtrf_command = { "mode": 2, "ch": channel, "cmd": 7, "ctr": 1 }
116                     elif id:
117                       mtrf_command = { "mode": 2, "ch": channel, "cmd": 7, "id0": int(id[0:2],16), "id1": int(id[2:4],16), "id2": int(id[4:6],16), "id3": int(id[6:8],16), "ctr": 8 }
118                     elif id == 'TX':
119                       mtrf_command = { "mode": 0, "ch": channel, "cmd": 7 }
120                     else:
121                       mtrf_command = { "mode": 0, "ch": channel, "cmd": 7 }
122                   elif command == "save_preset":
123                     if id == '.' or id == 'TX-F':
124                       mtrf_command = { "mode": 2, "ch": channel, "cmd": 8, "ctr": 1 }
125                     elif id:
126                       mtrf_command = { "mode": 2, "ch": channel, "cmd": 8, "id0": int(id[0:2],16), "id1": int(id[2:4],16), "id2": int(id[4:6],16), "id3": int(id[6:8],16), "ctr": 8 }
127                     elif id == 'TX':
128                       mtrf_command = { "mode": 0, "ch": channel, "cmd": 8 }
129                     else:
130                       mtrf_command = { "mode": 0, "ch": channel, "cmd": 8 }
131                   elif command == "temp_on":
132                     delay = (int(payload) + 3)//5
133                     d0 = delay % 256
134                     d1 = delay // 256
135                     if id == '.' or id == 'TX-F':
136                       mtrf_command = { "mode": 2, "ch": channel, "cmd": 25, "fmt": 6, "d0": d0, "d1": d1, "ctr": 1 }
137                     elif id:
138                       mtrf_command = { "mode": 2, "ch": channel, "cmd": 25, "fmt": 6, "d0": d0, "d1": d1, "id0": int(id[0:2],16), "id1": int(id[2:4],16), "id2": int(id[4:6],16), "id3": int(id[6:8],16), "ctr": 8 }
139                     elif id == 'TX':
140                       mtrf_command = { "mode": 0, "ch": channel, "cmd": 25, "fmt": 6, "d0": d0, "d1": d1 }
141                     else:
142                       mtrf_command = { "mode": 0, "ch": channel, "cmd": 25, "fmt": 6, "d0": d0, "d1": d1 }
143                   elif command == "bind":
144                     if id == 'TX-F':
145                       mtrf_command = { "mode": 2, "ch": channel, "cmd": 15 }
146                     elif id == 'TX':
147                       mtrf_command = { "mode": 0, "ch": channel, "cmd": 15 }
148                     elif id == 'RX':
149                       mtrf_command = { "mode": 1, "ch": channel, "cmd": 15 }
150                     elif id == 'RX-F':
151                       mtrf_command = { "mode": 3, "ch": channel, "cmd": 15 }
152                     else:
153                       mtrf_command = { "mode": 0, "ch": channel, "cmd": 15 }
154                   elif command == "unbind":
155                     if id == '.' or id == 'TX-F':
156                       mtrf_command = { "mode": 2, "ch": channel, "cmd": 9 }
157                     elif id == 'TX':
158                       mtrf_command = { "mode": 0, "ch": channel, "cmd": 9 }
159                     elif id == 'RX':
160                       mtrf_command = { "mode": 1, "ch": channel, "cmd": 9, "ctr": 5 }
161                     elif id == 'RX-F':
162                       mtrf_command = { "mode": 3, "ch": channel, "cmd": 9, "ctr": 5 }
163                     else:
164                       mtrf_command = { "mode": 0, "ch": channel, "cmd": 9 }
165                   elif command == "service":
166                     if id == '.' or id == 'TX-F':
167                       mtrf_command = { "mode": 2, "ch": channel, "cmd": 131, "d0": 1 }
168                     elif id:
169                       mtrf_command = { "mode": 2, "ch": channel, "cmd": 131, "d0": 1, "id0": int(id[0:2],16), "id1": int(id[2:4],16), "id2": int(id[4:6],16), "id3": int(id[6:8],16), "ctr": 8 }
170
171                   if mtrf_command:
172                     await self.commands_to_send_queue.put(mtrf_command)
173
174                 except Exception as e:
175                   logger.exception(e)
176                   continue
177                  
178
179     async def send_command_to_noolite(self):
180         last_command_send_time = 0
181         while True:
182             logger.info('Waiting commands to send...')
183             payload = await self.commands_to_send_queue.get()
184             logger.info('Get command from queue: {}'.format(payload))
185
186             # Формируем и отправляем команду к noolite
187             noolite_cmd = self.payload_to_noolite_command(payload)
188
189             if time.time() - last_command_send_time < self.commands_delay:
190                 logger.info('Wait before send next command: {}'.format(
191                     self.commands_delay - (time.time() - last_command_send_time)))
192                 await asyncio.sleep(self.commands_delay - (time.time() - last_command_send_time))
193
194             try:
195                 await self.noolite_serial.send_command(**noolite_cmd)
196             except TypeError as e:
197                 logger.exception(str(e))
198             last_command_send_time = time.time()
199
200     async def input_serial_data(self, command):
201         command = self.noolite_response_to_payload(command.to_list())
202         logger.info('Pub command: {}'.format(command))
203         try:
204           topic = "%s/%s/%s" % (self.write_topic, command['ch'], command['id'])
205         except:
206           topic = "%s/%s" % (self.write_topic, command['ch'])  
207         await self.mqtt_client.publish(topic=topic, message=json.dumps(command).encode())
208
209     @staticmethod        
210     def payload_to_noolite_command(payload):
211         return payload
212
213     @staticmethod
214     def noolite_response_to_payload(payload):
215
216         message = {}
217
218         try:
219           mode = [ 'TX', 'RX', 'TX-F', 'RX-F', 'SERVICE', 'FIRMWARE' ] [payload[1]]
220           message['mode'] = mode
221         finally:
222           None  
223
224         try:
225           message['ctr'] = [ 'OK', 'NORESP', 'ERROR', 'BOUND' ] [payload[2]]
226         finally:
227           None  
228
229         ch = payload[4]
230         message['ch'] = ch
231
232         cmd = payload[5]
233         message['cmd'] = cmd
234
235         fmt = payload[6]
236         message['fmt'] = fmt
237
238         data = payload[7:11]
239         message['data'] = data
240
241         if payload[1] >= 2:
242           message['id'] = '%0.2X%0.2X%0.2X%0.2X' % (payload[11], payload[12], payload[13], payload[14])
243
244         if cmd == 0:
245           message['command'] = 'OFF'
246         elif cmd == 1: 
247           message['command'] = 'BRIGHT_DOWN'
248         elif cmd == 2: 
249           message['command'] = 'ON'
250         elif cmd == 3: 
251           message['command'] = 'BRIGHT_UP'
252         elif cmd == 4: 
253           message['command'] = 'SWITCH'
254         elif cmd == 5: 
255           message['command'] = 'SWITCH'
256         elif cmd == 5: 
257           message['command'] = 'BRIGHT_BACK'
258         elif cmd == 5: 
259           message['command'] = 'BRIGHT_BACK'
260         elif cmd == 6: 
261           message['command'] = 'SET_BRIGHTNESS'
262         elif cmd == 7: 
263           message['command'] = 'LOAD_PRESET'
264         elif cmd == 8: 
265           message['command'] = 'SAVE_PRESET'
266         elif cmd == 9: 
267           message['command'] = 'UNBIND'
268         elif cmd == 10: 
269           message['command'] = 'STOP_REG'
270 #        elif cmd == 11: 
271 #          message['command'] = 'BRIGHTNESS_STEP_DOWN'
272 #        elif cmd == 12: 
273 #          message['command'] = 'BRIGHTNESS_STEP_UP'
274 #        elif cmd == 13: 
275 #          message['command'] = 'BRIGHT_REG'
276         elif cmd == 15: 
277           message['command'] = 'BIND'
278         elif cmd == 16: 
279           message['command'] = 'ROLL_COLOUR'
280         elif cmd == 17: 
281           message['command'] = 'SWITCH_COLOUR'
282         elif cmd == 18: 
283           message['command'] = 'SWITCH_MODE'
284         elif cmd == 19: 
285           message['command'] = 'SPEED_MODE_BACK'
286         elif cmd == 20: 
287           message['command'] = 'BATTERY_LOW'
288         elif cmd == 21: 
289           message['command'] = 'SENS_TEMP_HUMI'
290           t = data[0] + 256*(data[1] % 16)
291           if (data[1] % 16) // 8:
292             t = -(4096 - t )
293           t = t / 10  
294           message['t'] = t
295           dev_type = (data[1] // 16) % 8
296           try:
297             message['dev_type'] = [ 'RESERVED', 'PT112', 'PT111' ][dev_type]
298           finally:
299             None  
300           message['dev_battery_low'] = (data[1] // 128)
301           if dev_type == 2:
302             h = data[2]
303             message['h'] = h
304           message['aux'] = data[3]  
305         elif cmd == 25: 
306           message['command'] = 'TEMPORARY_ON'
307           if fmt == 5:
308             message['delay'] = data[0] * 5
309           elif fmt == 6:
310             message['delay'] = data[0] * 5 + data[1]*5*256
311         elif cmd == 26: 
312           message['command'] = 'MODES'
313         elif cmd == 128: 
314           message['command'] = 'READ_STATE'
315         elif cmd == 129: 
316           message['command'] = 'WRITE_STATE'
317         elif cmd == 130: 
318           message['command'] = 'SEND_STATE'
319           dev_type = data[0]
320           if dev_type == 5:
321             message['dev_type'] = 'SLU-1-300'
322           message['dev_firmware'] = data[1]
323           if fmt == 0:
324             dev_state = data[2] % 16
325             try:
326               message['dev_state'] = [ 'OFF', 'ON', 'TEMPORARY_ON' ][dev_state]
327             finally:
328               None
329             dev_mode = data[2] // 128
330             if dev_mode:
331               message['dev_binding'] = 'ON'
332             message['brightness'] = data[3]
333           elif fmt == 1:
334             message['dev_aux'] = data[2]
335             message['dev_legacy'] = data[3]
336           elif fmt == 2:
337             message['dev_free'] = data[3]
338             message['dev_free_legacy'] = data[2]    
339         elif cmd == 131: 
340           message['command'] = 'SERVICE'
341         elif cmd == 132: 
342           message['command'] = 'CLEAR_MEMORY'
343           
344         return message
345