ab7b997c3e747fb1dafc0929c878c6b932a6d5cd
[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":
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                 except Exception as e:
148                   logger.exception(e)
149                   continue
150                 await self.commands_to_send_queue.put(mtrf_command)
151                  
152
153     async def send_command_to_noolite(self):
154         last_command_send_time = 0
155         while True:
156             logger.info('Waiting commands to send...')
157             payload = await self.commands_to_send_queue.get()
158             logger.info('Get command from queue: {}'.format(payload))
159
160             # Формируем и отправляем команду к noolite
161             noolite_cmd = self.payload_to_noolite_command(payload)
162
163             if time.time() - last_command_send_time < self.commands_delay:
164                 logger.info('Wait before send next command: {}'.format(
165                     self.commands_delay - (time.time() - last_command_send_time)))
166                 await asyncio.sleep(self.commands_delay - (time.time() - last_command_send_time))
167
168             try:
169                 await self.noolite_serial.send_command(**noolite_cmd)
170             except TypeError as e:
171                 logger.exception(str(e))
172             last_command_send_time = time.time()
173
174     async def input_serial_data(self, command):
175         logger.info('Pub command: {}'.format(command))
176         command = self.noolite_response_to_payload(command.to_list())
177         try:
178           topic = "%s/%s/%s" % (self.write_topic, command['ch'], command['id'])
179         except:
180           topic = "%s/%s" % (self.write_topic, command['ch'])  
181         await self.mqtt_client.publish(topic=topic, message=json.dumps(command).encode())
182
183     @staticmethod        
184     def payload_to_noolite_command(payload):
185         return payload
186
187     @staticmethod
188     def noolite_response_to_payload(payload):
189
190         message = {}
191
192         try:
193           mode = [ 'TX', 'RX', 'TX-F', 'RX-F', 'SERVICE', 'FIRMWARE' ] [payload[1]]
194           message['mode'] = mode
195         finally:
196           None  
197
198         try:
199           message['ctr'] = [ 'OK', 'NORESP', 'ERROR', 'BOUND' ] [payload[2]]
200         finally:
201           None  
202
203         ch = payload[4]
204         message['ch'] = ch
205
206         cmd = payload[5]
207         message['cmd'] = cmd
208
209         fmt = payload[6]
210         message['fmt'] = fmt
211
212         data = payload[7:11]
213         message['data'] = data
214
215         if payload[1] >= 2:
216           message['id'] = '%0.2X%0.2X%0.2X%0.2X' % (payload[11], payload[12], payload[13], payload[14])
217
218         if cmd == 0:
219           message['command'] = 'OFF'
220         elif cmd == 1: 
221           message['command'] = 'BRIGHT_DOWN'
222         elif cmd == 2: 
223           message['command'] = 'ON'
224         elif cmd == 3: 
225           message['command'] = 'BRIGHT_UP'
226         elif cmd == 4: 
227           message['command'] = 'SWITCH'
228         elif cmd == 5: 
229           message['command'] = 'SWITCH'
230         elif cmd == 5: 
231           message['command'] = 'BRIGHT_BACK'
232         elif cmd == 5: 
233           message['command'] = 'BRIGHT_BACK'
234         elif cmd == 6: 
235           message['command'] = 'SET_BRIGHTNESS'
236         elif cmd == 7: 
237           message['command'] = 'LOAD_PRESET'
238         elif cmd == 8: 
239           message['command'] = 'SAVE_PRESET'
240         elif cmd == 9: 
241           message['command'] = 'UNBIND'
242         elif cmd == 10: 
243           message['command'] = 'STOP_REG'
244 #        elif cmd == 11: 
245 #          message['command'] = 'BRIGHTNESS_STEP_DOWN'
246 #        elif cmd == 12: 
247 #          message['command'] = 'BRIGHTNESS_STEP_UP'
248 #        elif cmd == 13: 
249 #          message['command'] = 'BRIGHT_REG'
250         elif cmd == 15: 
251           message['command'] = 'BIND'
252         elif cmd == 16: 
253           message['command'] = 'ROLL_COLOUR'
254         elif cmd == 17: 
255           message['command'] = 'SWITCH_COLOUR'
256         elif cmd == 18: 
257           message['command'] = 'SWITCH_MODE'
258         elif cmd == 19: 
259           message['command'] = 'SPEED_MODE_BACK'
260         elif cmd == 20: 
261           message['command'] = 'BATTERY_LOW'
262         elif cmd == 21: 
263           message['command'] = 'SENS_TEMP_HUMI'
264           t = data[0] + 256*(data[1] % 16)
265           if (data[1] % 16) // 8:
266             t = -(4096 - t )
267           t = t / 10  
268           message['t'] = t
269           dev_type = (data[1] // 16) % 8
270           try:
271             message['dev_type'] = [ 'RESERVED', 'PT112', 'PT111' ][dev_type]
272           finally:
273             None  
274           message['dev_battery_low'] = (data[1] // 128)
275           if dev_type == 2:
276             h = data[2]
277             message['h'] = h
278           message['aux'] = data[3]  
279         elif cmd == 25: 
280           message['command'] = 'TEMPORARY_ON'
281           if fmt == 5:
282             message['delay'] = data[0] * 5
283           elif fmt == 6:
284             message['delay'] = data[0] * 5 + data[1]*5*256
285         elif cmd == 26: 
286           message['command'] = 'MODES'
287         elif cmd == 128: 
288           message['command'] = 'READ_STATE'
289         elif cmd == 129: 
290           message['command'] = 'WRITE_STATE'
291         elif cmd == 130: 
292           message['command'] = 'SEND_STATE'
293           dev_type = data[0]
294           if dev_type == 5:
295             message['dev_type'] = 'SLU-1-300'
296           message['dev_firmware'] = data[1]
297           if fmt == 0:
298             dev_state = data[2] % 16
299             try:
300               message['dev_state'] = [ 'OFF', 'ON', 'TEMPORARY_ON' ][dev_state]
301               message['POWER'] = message['dev_state']
302             finally:
303               None
304             dev_mode = data[2] // 128
305             if dev_mode:
306               message['dev_binding'] = 'ON'
307             message['brightness'] = data[3]
308             message['DIMMER'] = int(round(data[3]*100/255))
309           elif fmt == 1:
310             message['dev_aux'] = data[2]
311             message['dev_legacy'] = data[3]
312           elif fmt == 2:
313             message['dev_free'] = data[3]
314             message['dev_free_legacy'] = data[2]    
315         elif cmd == 131: 
316           message['command'] = 'SERVICE'
317         elif cmd == 132: 
318           message['command'] = 'CLEAR_MEMORY'
319           
320         return message
321