Forked from https://bitbucket.org/AlekseevAV/noolite-mtrf-to-mqtt/
authorRoman Bazalevskiy <rvb@rvb.name>
Sun, 1 Apr 2018 15:54:26 +0000 (18:54 +0300)
committerRoman Bazalevskiy <rvb@rvb.name>
Sun, 1 Apr 2018 15:54:26 +0000 (18:54 +0300)
Добавлен разбор возврата от MTRF-64 и человекочитаемые топики MQTT

19 files changed:
Dockerfile [new file with mode: 0644]
MANIFEST.in [new file with mode: 0644]
README.md [new file with mode: 0644]
nmd/__init__.py [new file with mode: 0644]
nmd/main.py [new file with mode: 0644]
nmd/nl_mqtt.py [new file with mode: 0644]
nmd/nl_sensors.py [new file with mode: 0644]
nmd/nl_serial.py [new file with mode: 0644]
nmd/systemd_script/create_service.sh [new file with mode: 0644]
nmd/systemd_script/noolite_mtrf_mqtt.service [new file with mode: 0644]
nmd/utils.py [new file with mode: 0644]
noolite_mtrf_mqtt.egg-info/PKG-INFO [new file with mode: 0644]
noolite_mtrf_mqtt.egg-info/SOURCES.txt [new file with mode: 0644]
noolite_mtrf_mqtt.egg-info/dependency_links.txt [new file with mode: 0644]
noolite_mtrf_mqtt.egg-info/entry_points.txt [new file with mode: 0644]
noolite_mtrf_mqtt.egg-info/requires.txt [new file with mode: 0644]
noolite_mtrf_mqtt.egg-info/top_level.txt [new file with mode: 0644]
requirements.txt [new file with mode: 0644]
setup.py [new file with mode: 0644]

diff --git a/Dockerfile b/Dockerfile
new file mode 100644 (file)
index 0000000..e49f27d
--- /dev/null
@@ -0,0 +1,26 @@
+FROM alpine:latest
+
+MAINTAINER Aleksandr Alekseev
+
+ENV mtrf_serial_port_env=/dev/tty.mtrf_serial_port \
+    mqtt_scheme=mqtt \
+    mqtt_host=127.0.0.1 \
+    mqtt_port=__EMPTY__ \
+    mqtt_user=__EMPTY__ \
+    mqtt_password=__EMPTY__ \
+    commands_delay=0.1 \
+    logging_level=INFO
+
+RUN apk --update add python3 && rm -rf /var/cache/apk/*
+
+RUN pip3 install --upgrade noolite-mtrf-mqtt
+
+CMD noolite_mtrf_mqtt \
+        --mtrf-serial-port=$mtrf_serial_port_env \
+        --mqtt-scheme=$mqtt_scheme \
+        --mqtt-host=$mqtt_host \
+        --mqtt-port=$mqtt_port \
+        --mqtt-user=$mqtt_user \
+        --mqtt-password=$mqtt_password \
+        --commands-delay=$commands_delay \
+        --logging-level=$logging_level
diff --git a/MANIFEST.in b/MANIFEST.in
new file mode 100644 (file)
index 0000000..cd14874
--- /dev/null
@@ -0,0 +1,2 @@
+include README.md
+recursive-include nmd/systemd_script *
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644 (file)
index 0000000..849e0d9
--- /dev/null
+++ b/README.md
@@ -0,0 +1,52 @@
+# NooLite MTRF MQTT
+
+Ретранслятор сообщений с последовательного порта MTRF в MQTT сообщения
+
+## Установка
+
+Для установки проекта нужен Python 3.5+ и pip
+
+### Из репозитория
+
+В системе должны быть установлены:
+
+- pip для третий версии python
+
+- git
+
+```bash
+$ pip3 install git+https://bitbucket.org/AlekseevAV/noolite-mtrf-to-mqtt
+```
+
+К примеру установка проекта на ArchLinux будет выглядеть следующим образом:
+```bash
+# Устанавливаем необходимые пакеты
+$ pacman -S python python-pip git
+# Устанавливаем noolite_api
+$ pip3 install git+https://bitbucket.org/AlekseevAV/noolite-mtrf-to-mqtt
+```
+
+### Из исходников
+
+```bash
+# Клонируем репозиторий
+$ git clone https://bitbucket.org/AlekseevAV/noolite-mtrf-to-mqtt
+
+# Заходим в созданную папку репозитория
+$ cd noolite-mtrf-to-mqtt
+
+# Устанавливаем сервер
+$ python setup.py install
+```
+
+## Запуск
+
+```
+$ noolite_mtrf_mqtt
+```
+
+## Работа
+
+MQTT топики для работы:
+- noolite/mtrf/send - топик для отправки сообщений на адаптер
+- noolite/mtrf/receive - топик, куда публикуются все принятые сообщения с адаптера
diff --git a/nmd/__init__.py b/nmd/__init__.py
new file mode 100644 (file)
index 0000000..1180dfe
--- /dev/null
@@ -0,0 +1 @@
+from .nl_mqtt import MqttDriver
\ No newline at end of file
diff --git a/nmd/main.py b/nmd/main.py
new file mode 100644 (file)
index 0000000..c554b90
--- /dev/null
@@ -0,0 +1,63 @@
+import signal
+import asyncio
+import logging
+import argparse
+
+from nmd import MqttDriver
+
+logger = logging.getLogger(__name__)
+
+
+def get_args():
+    parser = argparse.ArgumentParser(prog='noolite_mtrf_mqtt', description='NooLite MTRF to MQTT')
+    parser.add_argument('--mtrf-serial-port', '-msp', required=True, type=str, help='MTRF-32-USB port name')
+    parser.add_argument('--mqtt-scheme', '-ms', default='mqtt', type=str, help='MQTT scheme')
+    parser.add_argument('--mqtt-host', '-mh', default='127.0.0.1', type=str, help='MQTT host')
+    parser.add_argument('--mqtt-port', '-mp', type=str, help='MQTT port')
+    parser.add_argument('--mqtt-user', '-mu', type=str, help='MQTT user')
+    parser.add_argument('--mqtt-password', '-mpass', type=str, help='MQTT password')
+    parser.add_argument('--mqtt-topic', '-mt', type=str, default='noolite', help='MQTT topic root')
+    parser.add_argument('--commands-delay', '-cd', type=float, default=0.1,
+                        help='Delay between sending commands to MTRF')
+    parser.add_argument('--logging-level', '-ll', default='INFO', type=str, help='Logging level')
+    return parser.parse_args()
+
+
+def ask_exit():
+    for task in asyncio.Task.all_tasks():
+        task.cancel()
+
+
+def run():
+    args = get_args()
+    logging.basicConfig(level=args.logging_level)
+
+    loop = asyncio.get_event_loop()
+    for sig in (signal.SIGINT, signal.SIGTERM):
+        loop.add_signal_handler(sig, ask_exit)
+
+    for can_be_empty_arg in ['mqtt_port', 'mqtt_user', 'mqtt_password']:
+        if getattr(args, can_be_empty_arg, None) == '__EMPTY__':
+            setattr(args, can_be_empty_arg, '')
+
+    mqtt_uri = '{scheme}://{user}{password}@{host}{port}'.format(
+        scheme=args.mqtt_scheme,
+        user=args.mqtt_user or '',
+        password=':{}'.format(args.mqtt_password) if args.mqtt_password else '',
+        host=args.mqtt_host,
+        port=':{}'.format(args.mqtt_port) if args.mqtt_port else '',
+    )
+
+    md = MqttDriver(mtrf_tty_name=args.mtrf_serial_port, loop=loop, mqtt_uri=mqtt_uri, mqtt_topic=args.mqtt_topic,
+                    commands_delay=args.commands_delay)
+
+    try:
+        loop.run_until_complete(md.run())
+    except asyncio.CancelledError as e:
+        logger.debug(e)
+    finally:
+        loop.close()
+
+
+if __name__ == '__main__':
+    run()
diff --git a/nmd/nl_mqtt.py b/nmd/nl_mqtt.py
new file mode 100644 (file)
index 0000000..d0b26b3
--- /dev/null
@@ -0,0 +1,301 @@
+import time
+import json
+import logging
+import asyncio
+
+from hbmqtt.client import MQTTClient
+from hbmqtt.mqtt.constants import QOS_0, QOS_1, QOS_2
+
+from .nl_serial import NooliteSerial
+from .utils import Singleton
+
+from uuid import uuid1
+from socket import gethostname
+
+logger = logging.getLogger(__name__)
+
+client_id = gethostname()+'-'+str(uuid1())
+
+INPUT_TOPIC = '%s/send'
+OUTPUT_TOPIC = '%s/receive'
+
+class MqttDriver(metaclass=Singleton):
+    def __init__(self, mtrf_tty_name, loop, mqtt_uri='mqtt://127.0.0.1/', mqtt_topic='noolite', commands_delay=0.1):
+        self.mqtt_client = MQTTClient(client_id=client_id,config={'auto_reconnect': True})
+        self.mqtt_uri = mqtt_uri
+        self.commands_delay = commands_delay
+        self.noolite_serial = NooliteSerial(loop=loop, tty_name=mtrf_tty_name,
+                                            input_command_callback_method=self.input_serial_data)
+        self.commands_to_send_queue = asyncio.Queue()
+        self.read_topic = INPUT_TOPIC % mqtt_topic
+        self.subscriptions = [
+            ( self.read_topic+'/#', QOS_0),
+        ]
+        self.write_topic = OUTPUT_TOPIC % mqtt_topic
+        loop.create_task(self.send_command_to_noolite())
+
+    async def run(self):
+        await self.mqtt_client.connect(self.mqtt_uri)
+        await self.mqtt_client.subscribe(self.subscriptions)
+
+        while True:
+            logger.info('Waiting messages from mqtt...')
+            message = await self.mqtt_client.deliver_message()
+
+            topic = message.topic
+            payload = message.publish_packet.payload.data
+
+            logger.info('In message: {}\n{}'.format(topic, payload))
+
+            if topic.startswith(self.read_topic):
+              subtopic = topic[len(self.read_topic)+1:]
+              print(subtopic)
+
+              if subtopic == '':
+                try:
+                  payload = json.loads(payload.decode())
+                except Exception as e:
+                  logger.exception(e)
+                  continue
+                await self.commands_to_send_queue.put(payload)
+              else:
+                try:
+                  address = subtopic.split('/')
+                  if len(address)==2:
+                    channel = int(address[0])
+                    command = address[1]
+                    id = None
+                  elif len(address)==3:
+                    channel = int(address[0])
+                    command = address[2]
+                    id = address[1]
+                  elif len(address)==1:
+                    command = address[0]
+                    channel = None
+                    id = None
+                  command = command.lower()  
+                  print("%s: %s (%s)" % (command,channel,id))
+                  if command == "on":
+                    if id == '.':
+                      mtrf_command = { "mode": 2, "ch": channel, "cmd": 2 }
+                    elif id:
+                      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 }
+                    else:
+                      mtrf_command = { "mode": 0, "ch": channel, "cmd": 2 }
+                  elif command == "off":
+                    if id == '.':
+                      mtrf_command = { "mode": 2, "ch": channel, "cmd": 0 }
+                    elif id:
+                      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 }
+                    else:
+                      mtrf_command = { "mode": 0, "ch": channel, "cmd": 0 }
+                  elif command == "brightness":
+                    brightness = int(payload)
+                    if id == '.':
+                      mtrf_command = { "mode": 2, "ch": channel, "cmd": 6, "d0": brightness }
+                    elif id:
+                      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 }
+                    else:
+                      mtrf_command = { "mode": 0, "ch": channel, "cmd": 6, "d0": brightness }
+                  elif command == "state":
+                    if id == '.':
+                      mtrf_command = { "mode": 2, "ch": channel, "cmd": 128 }
+                    elif id:
+                      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 }
+                  elif command == "load_preset":
+                    if id == '.':
+                      mtrf_command = { "mode": 2, "ch": channel, "cmd": 7 }
+                    elif id:
+                      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 }
+                    else:
+                      mtrf_command = { "mode": 0, "ch": channel, "cmd": 7 }
+                  elif command == "save_preset":
+                    if id == '.':
+                      mtrf_command = { "mode": 2, "ch": channel, "cmd": 8 }
+                    elif id:
+                      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 }
+                    else:
+                      mtrf_command = { "mode": 0, "ch": channel, "cmd": 8 }
+                  elif command == "temp_on":
+                    delay = (int(payload) + 3)//5
+                    d0 = delay % 256
+                    d1 = delay // 256
+                    if id == '.':
+                      mtrf_command = { "mode": 2, "ch": channel, "cmd": 25, "fmt": 6, "d0": d0, "d1": d1 }
+                    elif id:
+                      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 }
+                    else:
+                      mtrf_command = { "mode": 0, "ch": channel, "cmd": 25, "fmt": 6, "d0": d0, "d1": d1 }
+                except Exception as e:
+                  logger.exception(e)
+                  continue
+                await self.commands_to_send_queue.put(mtrf_command)
+                 
+
+    async def send_command_to_noolite(self):
+        last_command_send_time = 0
+        while True:
+            logger.info('Waiting commands to send...')
+            payload = await self.commands_to_send_queue.get()
+            logger.info('Get command from queue: {}'.format(payload))
+
+            # Формируем и отправляем команду к noolite
+            noolite_cmd = self.payload_to_noolite_command(payload)
+
+            if time.time() - last_command_send_time < self.commands_delay:
+                logger.info('Wait before send next command: {}'.format(
+                    self.commands_delay - (time.time() - last_command_send_time)))
+                await asyncio.sleep(self.commands_delay - (time.time() - last_command_send_time))
+
+            try:
+                await self.noolite_serial.send_command(**noolite_cmd)
+            except TypeError as e:
+                logger.exception(str(e))
+            last_command_send_time = time.time()
+
+    async def input_serial_data(self, command):
+        logger.info('Pub command: {}'.format(command))
+        command = self.noolite_response_to_payload(command.to_list())
+        try:
+          topic = "%s/%s/%s" % (self.write_topic, command['ch'], command['id'])
+        except:
+          topic = "%s/%s" % (self.write_topic, command['ch'])  
+        await self.mqtt_client.publish(topic=topic, message=json.dumps(command).encode())
+
+    @staticmethod        
+    def payload_to_noolite_command(payload):
+        return payload
+
+    @staticmethod
+    def noolite_response_to_payload(payload):
+
+        message = {}
+
+        try:
+          mode = [ 'TX', 'RX', 'TX-F', 'RX-F', 'SERVICE', 'FIRMWARE' ] [payload[1]]
+          message['mode'] = mode
+        finally:
+          None  
+
+        try:
+          message['ctr'] = [ 'OK', 'NORESP', 'ERROR', 'BOUND' ] [payload[2]]
+        finally:
+          None  
+
+        ch = payload[4]
+        message['ch'] = ch
+
+        cmd = payload[5]
+        message['cmd'] = cmd
+
+        fmt = payload[6]
+        message['fmt'] = fmt
+
+        data = payload[7:11]
+        message['data'] = data
+
+        if payload[1] >= 2:
+          message['id'] = '%0.2X%0.2X%0.2X%0.2X' % (payload[11], payload[12], payload[13], payload[14])
+
+        if cmd == 0:
+          message['command'] = 'OFF'
+        elif cmd == 1: 
+          message['command'] = 'BRIGHT_DOWN'
+        elif cmd == 2: 
+          message['command'] = 'ON'
+        elif cmd == 3: 
+          message['command'] = 'BRIGHT_UP'
+        elif cmd == 4: 
+          message['command'] = 'SWITCH'
+        elif cmd == 5: 
+          message['command'] = 'SWITCH'
+        elif cmd == 5: 
+          message['command'] = 'BRIGHT_BACK'
+        elif cmd == 5: 
+          message['command'] = 'BRIGHT_BACK'
+        elif cmd == 6: 
+          message['command'] = 'SET_BRIGHTNESS'
+        elif cmd == 7: 
+          message['command'] = 'LOAD_PRESET'
+        elif cmd == 8: 
+          message['command'] = 'SAVE_PRESET'
+        elif cmd == 9: 
+          message['command'] = 'UNBIND'
+        elif cmd == 10: 
+          message['command'] = 'STOP_REG'
+#        elif cmd == 11: 
+#          message['command'] = 'BRIGHTNESS_STEP_DOWN'
+#        elif cmd == 12: 
+#          message['command'] = 'BRIGHTNESS_STEP_UP'
+#        elif cmd == 13: 
+#          message['command'] = 'BRIGHT_REG'
+        elif cmd == 15: 
+          message['command'] = 'BIND'
+        elif cmd == 16: 
+          message['command'] = 'ROLL_COLOUR'
+        elif cmd == 17: 
+          message['command'] = 'SWITCH_COLOUR'
+        elif cmd == 18: 
+          message['command'] = 'SWITCH_MODE'
+        elif cmd == 19: 
+          message['command'] = 'SPEED_MODE_BACK'
+        elif cmd == 20: 
+          message['command'] = 'BATTERY_LOW'
+        elif cmd == 21: 
+          message['command'] = 'SENS_TEMP_HUMI'
+          t = data[0] + 256*(data[1] % 16)
+          if (data[1] % 16) // 8:
+            t = -(4096 - t )
+          t = t / 10  
+          message['t'] = t
+          dev_type = (data[1] // 16) % 8
+          try:
+            message['dev_type'] = [ 'RESERVED', 'PT112', 'PT111' ][dev_type]
+          finally:
+            None  
+          message['dev_battery_low'] = (data[1] // 128)
+          if dev_type == 2:
+            h = data[2]
+            message['h'] = h
+          message['aux'] = data[3]  
+        elif cmd == 25: 
+          message['command'] = 'TEMPORARY_ON'
+          if fmt == 5:
+            message['delay'] = data[0] * 5
+          elif fmt == 6:
+            message['delay'] = data[0] * 5 + data[1]*5*256
+        elif cmd == 26: 
+          message['command'] = 'MODES'
+        elif cmd == 128: 
+          message['command'] = 'READ_STATE'
+        elif cmd == 129: 
+          message['command'] = 'WRITE_STATE'
+        elif cmd == 130: 
+          message['command'] = 'SEND_STATE'
+          dev_type = data[0]
+          if dev_type == 5:
+            message['dev_type'] = 'SLU-1-300'
+          message['dev_firmware'] = data[1]
+          if fmt == 0:
+            dev_state = data[2] % 16
+            try:
+              message['dev_state'] = [ 'OFF', 'ON', 'TEMPORARY_ON' ][dev_state]
+            finally:
+              None
+            dev_mode = data[2] // 128
+            if dev_mode:
+              message['dev_binding'] = 'ON'
+            message['brightness'] = data[3]
+          elif fmt == 1:
+            message['dev_aux'] = data[2]
+            message['dev_legacy'] = data[3]
+          elif fmt == 2:
+            message['dev_free'] = data[3]
+            message['dev_free_legacy'] = data[2]    
+        elif cmd == 131: 
+          message['command'] = 'SERVICE'
+        elif cmd == 132: 
+          message['command'] = 'CLEAR_MEMORY'
+          
+        return message
+        
diff --git a/nmd/nl_sensors.py b/nmd/nl_sensors.py
new file mode 100644 (file)
index 0000000..764bbc4
--- /dev/null
@@ -0,0 +1,173 @@
+import time
+import asyncio
+
+from ..noolite_mqtt import NooLiteMqtt
+
+
+class NooLiteSensor:
+    def __init__(self, channel, loop):
+        self.channel = channel
+        self.battery_status = None
+        self.last_update = time.time()
+        self.loop = loop
+        self.noolite_mqtt = NooLiteMqtt()
+
+
+class TempHumSensor(NooLiteSensor):
+    def __init__(self, channel, loop):
+        super().__init__(channel, loop)
+        self.sensor_type = None
+        self.temp = None
+        self.hum = None
+        self.analog_sens = None
+
+    def __str__(self):
+        return 'Ch: {}, battery: {}, temp: {}, hum: {}'.format(self.channel, self.battery_status, self.temp, self.hum)
+
+    def to_json(self):
+        json_data = {
+            'type': self.sensor_type,
+            'temperature': self.temp,
+            'humidity': self.hum,
+            'battery': self.battery_status,
+            'analog_sens': self.analog_sens
+        }
+        return json_data
+
+    async def read_response_data(self, response):
+        temp_bits = '{:08b}'.format(response.d1)[4:] + '{:08b}'.format(response.d0)
+
+        # Тип датчика:
+        #   000-зарезервировано
+        #   001-датчик температуры (PT112)
+        #   010-датчик температуры/влажности (PT111)
+        self.sensor_type = '{:08b}'.format(response.d1)[1:4]
+
+        # Если первый бит 0 - температура считается выше нуля
+        if temp_bits[0] == '0':
+            self.temp = int(temp_bits, 2) / 10.
+        # Если 1 - ниже нуля. В этом случае необходимо от 4096 отнять полученное значение
+        elif temp_bits[0] == '1':
+            self.temp = -((4096 - int(temp_bits, 2)) / 10.)
+
+        # Если датчик PT111 (с влажностью), то получаем влажность из 3 байта данных
+        if self.sensor_type == '010':
+            self.hum = response.d2
+
+        # Состояние батареи: 1-разряжена, 0-заряд батареи в норме
+        self.battery_status = int('{:08b}'.format(response.d1)[0])
+
+        # Значение, считываемое с аналогового входа датчика; 8 бит; (по умолчанию = 255)
+        self.analog_sens = response.d3
+
+        self.last_update = time.time()
+
+        accessories = self.noolite_mqtt.accessory_filter(channel=self.channel)
+
+        for accessory_name, accessory_data in accessories.items():
+
+            accessory_characteristics = []
+            [accessory_characteristics.extend(list(characteristics.keys())) for characteristics in
+             accessory_data['characteristics'].values()]
+
+            if self.temp is not None and 'CurrentTemperature' in accessory_characteristics:
+                await self.noolite_mqtt.characteristic_set(
+                    accessory_name=accessory_name,
+                    characteristic='CurrentTemperature',
+                    value=self.temp
+                )
+
+            if self.hum is not None and 'CurrentRelativeHumidity' in accessory_characteristics:
+                await self.noolite_mqtt.characteristic_set(
+                    accessory_name=accessory_name,
+                    characteristic='CurrentRelativeHumidity',
+                    value=self.hum
+                )
+
+
+class MotionSensor(NooLiteSensor):
+    def __init__(self, channel, loop):
+        super().__init__(channel, loop)
+        self.active_time = None
+
+    def __str__(self):
+        return 'Ch: {}, battery: {}, active_time: {}'.format(self.channel, self.battery_status, self.active_time)
+
+    async def read_response_data(self, response):
+        self.active_time = response.d0 * 5
+
+        # Состояние батареи: 1-разряжена, 0-заряд батареи в норме
+        self.battery_status = int('{:08b}'.format(response.d1)[0])
+
+        self.last_update = time.time()
+        await self.set_active_state(self.active_time)
+
+    async def set_active_state(self, delay):
+
+        accessory = await self.noolite_mqtt.accessory_get(channel=self.channel)
+
+        for accessory_name, accessory_data in accessory.items():
+            await self.noolite_mqtt.characteristic_set(
+                accessory_name=accessory_name,
+                characteristic='MotionDetected',
+                value=True
+            )
+
+            # Выключаем активное состояние
+            await asyncio.sleep(delay)
+            await self.noolite_mqtt.characteristic_set(
+                accessory_name=accessory_name,
+                characteristic='MotionDetected',
+                value=False
+            )
+
+    def is_active(self):
+        return self.last_update + self.active_time >= time.time()
+
+    def to_json(self):
+        json_data = {
+            'battery': self.battery_status,
+            'state': 1 if self.is_active() else 0
+        }
+        return json_data
+
+
+class SensorManager:
+    def __init__(self, nl_mqtt):
+        self.nl_mqtt = nl_mqtt
+        self.sensors = []
+        self.sensors_map = {
+            'temp': TempHumSensor,
+            'hum': TempHumSensor,
+            'motion': MotionSensor
+        }
+
+    async def new_data(self, response):
+        if response.cmd == 21:
+            sensor = self.get_sensor(response.ch, 'temp') or TempHumSensor(channel=response.ch, loop=self.loop)
+        elif response.cmd == 25:
+            sensor = self.get_sensor(response.ch, 'motion') or MotionSensor(channel=response.ch, loop=self.loop)
+        else:
+            print('Unknown response: {}'.format(response))
+            return
+
+        await sensor.read_response_data(response=response)
+
+        if sensor not in self.sensors:
+            self.sensors.append(sensor)
+        print(sensor)
+
+    def get_sensor(self, channel, sensor_type):
+        sensor_type = self.sensors_map[sensor_type]
+        for sensor in self.sensors:
+            if sensor.channel == channel and isinstance(sensor, sensor_type):
+                return sensor
+        return None
+
+    def get_multiple_sensors(self, channels, sensor_type):
+        sensors = []
+        sensor_type = self.sensors_map[sensor_type]
+        for sensor in self.sensors:
+            if sensor.channel in channels and isinstance(sensor, sensor_type):
+                sensors.append(sensor)
+        return sensors
diff --git a/nmd/nl_serial.py b/nmd/nl_serial.py
new file mode 100644 (file)
index 0000000..122182a
--- /dev/null
@@ -0,0 +1,220 @@
+import time
+import logging
+
+import serial
+
+from .utils import Singleton
+
+# Logging config
+logger = logging.getLogger(__name__)
+formatter = logging.Formatter('%(asctime)s - %(filename)s:%(lineno)s - %(levelname)s - %(message)s')
+
+
+class NooLiteMessage:
+    def __init__(self, ch, cmd, mode, id0, id1, id2, id3):
+        self.mode = mode
+        self.ch = ch
+        self.cmd = cmd
+        self.id0 = id0
+        self.id1 = id1
+        self.id2 = id2
+        self.id3 = id3
+
+    def __eq__(self, other):
+        return all([
+            self.mode == other.mode,
+            self.ch == other.ch,
+            self.id0 == other.id0,
+            self.id1 == other.id1,
+            self.id2 == other.id2,
+            self.id3 == other.id3
+        ])
+
+    def is_id(self):
+        return any([
+            self.id0 != 0,
+            self.id1 != 0,
+            self.id2 != 0,
+            self.id3 != 0
+        ])
+
+
+class NooLiteResponse(NooLiteMessage):
+    def __init__(self, st, mode, ctr, togl, ch, cmd, fmt, d0, d1, d2, d3, id0, id1, id2, id3, crc, sp):
+        super().__init__(ch, cmd, mode, id0, id1, id2, id3)
+        self.st = st
+        self.ctr = ctr
+        self.togl = togl
+        self.fmt = fmt
+        self.d0 = d0
+        self.d1 = d1
+        self.d2 = d2
+        self.d3 = d3
+        self.crc = crc
+        self.sp = sp
+        self.time = time.time()
+
+    def __str__(self):
+        return '{}'.format(self.to_list())
+
+    def __repr__(self):
+        return '{}'.format(self.to_list())
+
+    @property
+    def success(self):
+        return self.ctr == 0
+
+    def to_list(self):
+        return [
+            self.st,
+            self.mode,
+            self.ctr,
+            self.togl,
+            self.ch,
+            self.cmd,
+            self.fmt,
+            self.d0,
+            self.d1,
+            self.d2,
+            self.d3,
+            self.id0,
+            self.id1,
+            self.id2,
+            self.id3,
+            self.crc,
+            self.sp
+        ]
+
+
+class NooLiteCommand(NooLiteMessage):
+    def __init__(self, ch, cmd, mode=0, ctr=0, res=0, fmt=0, d0=0, d1=0, d2=0, d3=0, id0=0, id1=0, id2=0, id3=0):
+        super().__init__(ch, cmd, mode, id0, id1, id2, id3)
+        self.st = 171
+        self.ctr = ctr
+        self.res = res
+        self.fmt = fmt
+        self.d0 = d0
+        self.d1 = d1
+        self.d2 = d2
+        self.d3 = d3
+        self.sp = 172
+
+    def __str__(self):
+        return '{}'.format(self.to_list())
+
+    def __repr__(self):
+        return '{}'.format(self.to_list())
+
+    @property
+    def crc(self):
+        crc = sum([
+            self.st,
+            self.mode,
+            self.ctr,
+            self.res,
+            self.ch,
+            self.cmd,
+            self.fmt,
+            self.d0,
+            self.d1,
+            self.d2,
+            self.d3,
+            self.id0,
+            self.id1,
+            self.id2,
+            self.id3,
+        ])
+        return crc if crc < 256 else divmod(crc, 256)[1]
+
+    def to_list(self):
+        return [
+            self.st,
+            self.mode,
+            self.ctr,
+            self.res,
+            self.ch,
+            self.cmd,
+            self.fmt,
+            self.d0,
+            self.d1,
+            self.d2,
+            self.d3,
+            self.id0,
+            self.id1,
+            self.id2,
+            self.id3,
+            self.crc,
+            self.sp
+        ]
+
+    def to_bytes(self):
+        return bytearray([
+            self.st,
+            self.mode,
+            self.ctr,
+            self.res,
+            self.ch,
+            self.cmd,
+            self.fmt,
+            self.d0,
+            self.d1,
+            self.d2,
+            self.d3,
+            self.id0,
+            self.id1,
+            self.id2,
+            self.id3,
+            self.crc,
+            self.sp
+        ])
+
+    def description(self):
+        return {
+            'ST':   {'value': self.st, 'description': 'Стартовый байт'},
+            'MODE': {'value': self.mode, 'description': 'Режим работы адаптера'},
+            'CTR':  {'value': self.ctr, 'description': 'Управление адаптером'},
+            'RES':  {'value': self.res, 'description': 'Зарезервирован, не используется'},
+            'CH':   {'value': self.ch, 'description': 'Адрес канала, ячейки привязки'},
+            'CMD':  {'value': self.cmd, 'description': 'Команда'},
+            'FMT':  {'value': self.fmt, 'description': 'Формат'},
+            'DATA': {'value': [self.d0, self.d1, self.d2, self.d3], 'description': 'Данные'},
+            'ID':   {'value': [self.id0, self.id1, self.id2, self.id3], 'description': 'Адрес блока'},
+            'CRC':  {'value': self.crc, 'description': 'Контрольная сумма'},
+            'SP':   {'value': self.sp, 'description': 'Стоповый байт'}
+        }
+
+
+class NooliteSerial(metaclass=Singleton):
+    def __init__(self, loop, tty_name, input_command_callback_method):
+        self.tty = self._get_tty(tty_name)
+        self.responses = []
+        self.input_command_callback_method = input_command_callback_method
+        self.loop = loop
+
+        # Если приходят данные на адаптер, то они обрабатываются в этом методе
+        self.loop.add_reader(self.tty.fd, self.inf_reading)
+
+    def inf_reading(self):
+        while self.tty.in_waiting >= 17:
+            in_bytes = self.tty.read(17)
+            resp = NooLiteResponse(*list(in_bytes))
+            logger.debug('Incoming command: {}'.format(resp))
+            self.loop.create_task(self.input_command_callback_method(resp))
+
+    async def send_command(self, ch, cmd, mode=0, ctr=0, res=0, fmt=0, d0=0, d1=0, d2=0, d3=0, id0=0, id1=0, id2=0, id3=0):
+        command = NooLiteCommand(ch, cmd, mode, ctr, res, fmt, d0, d1, d2, d3, id0, id1, id2, id3)
+
+        # Write
+        logger.info('> {}'.format(command))
+        before = time.time()
+        self.tty.write(command.to_bytes())
+        logger.info('Time to write: {}'.format(time.time() - before))
+
+    @staticmethod
+    def _get_tty(tty_name):
+        serial_port = serial.Serial(tty_name, timeout=2)
+        if not serial_port.is_open:
+            serial_port.open()
+        serial_port.flushInput()
+        serial_port.flushOutput()
+        return serial_port
diff --git a/nmd/systemd_script/create_service.sh b/nmd/systemd_script/create_service.sh
new file mode 100644 (file)
index 0000000..47353af
--- /dev/null
@@ -0,0 +1,6 @@
+SYSTEMD_SCRIPT_DIR=$( cd  $(dirname "${BASH_SOURCE:=$0}") && pwd)
+cp -f "$SYSTEMD_SCRIPT_DIR/noolite_mtrf_mqtt.service" /lib/systemd/system
+chown root:root /lib/systemd/system/noolite_mtrf_mqtt.service
+
+systemctl daemon-reload
+systemctl enable noolite_mtrf_mqtt.service
\ No newline at end of file
diff --git a/nmd/systemd_script/noolite_mtrf_mqtt.service b/nmd/systemd_script/noolite_mtrf_mqtt.service
new file mode 100644 (file)
index 0000000..bf3295f
--- /dev/null
@@ -0,0 +1,10 @@
+[Unit]
+Description=NooLite MTRF serial to mqtt adapter
+After=mosquitto.service
+
+[Service]
+Type=idle
+ExecStart=/usr/local/bin/noolite_mtrf_mqtt --mtrf-serial-port /dev/ttyUSB0
+
+[Install]
+WantedBy=multi-user.target
diff --git a/nmd/utils.py b/nmd/utils.py
new file mode 100644 (file)
index 0000000..3776cb9
--- /dev/null
@@ -0,0 +1,7 @@
+class Singleton(type):
+    _instances = {}
+
+    def __call__(cls, *args, **kwargs):
+        if cls not in cls._instances:
+            cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs)
+        return cls._instances[cls]
diff --git a/noolite_mtrf_mqtt.egg-info/PKG-INFO b/noolite_mtrf_mqtt.egg-info/PKG-INFO
new file mode 100644 (file)
index 0000000..1fe2260
--- /dev/null
@@ -0,0 +1,62 @@
+Metadata-Version: 1.0
+Name: noolite-mtrf-mqtt
+Version: 0.1.3
+Summary: NooLite MTRF serial port to MQTT messages
+Home-page: https://bitbucket.org/AlekseevAV/noolite-mtrf-to-mqtt
+Author: UNKNOWN
+Author-email: UNKNOWN
+License: UNKNOWN
+Description: # NooLite MTRF MQTT
+        
+        Ретранслятор сообщений с последовательного порта MTRF в MQTT сообщения
+        
+        ## Установка
+        
+        Для установки проекта нужен Python 3.5+ и pip
+        
+        ### Из репозитория
+        
+        В системе должны быть установлены:
+        
+        - pip для третий версии python
+        
+        - git
+        
+        ```bash
+        $ pip3 install git+https://bitbucket.org/AlekseevAV/noolite-mtrf-to-mqtt
+        ```
+        
+        К примеру установка проекта на ArchLinux будет выглядеть следующим образом:
+        ```bash
+        # Устанавливаем необходимые пакеты
+        $ pacman -S python python-pip git
+        # Устанавливаем noolite_api
+        $ pip3 install git+https://bitbucket.org/AlekseevAV/noolite-mtrf-to-mqtt
+        ```
+        
+        ### Из исходников
+        
+        ```bash
+        # Клонируем репозиторий
+        $ git clone https://bitbucket.org/AlekseevAV/noolite-mtrf-to-mqtt
+        
+        # Заходим в созданную папку репозитория
+        $ cd noolite-mtrf-to-mqtt
+        
+        # Устанавливаем сервер
+        $ python setup.py install
+        ```
+        
+        ## Запуск
+        
+        ```
+        $ noolite_mtrf_mqtt
+        ```
+        
+        ## Работа
+        
+        MQTT топики для работы:
+        - noolite/mtrf/send - топик для отправки сообщений на адаптер
+        - noolite/mtrf/receive - топик, куда публикуются все принятые сообщения с адаптера
+        
+Platform: UNKNOWN
diff --git a/noolite_mtrf_mqtt.egg-info/SOURCES.txt b/noolite_mtrf_mqtt.egg-info/SOURCES.txt
new file mode 100644 (file)
index 0000000..12fb64d
--- /dev/null
@@ -0,0 +1,17 @@
+MANIFEST.in
+README.md
+setup.py
+nmd/__init__.py
+nmd/main.py
+nmd/nl_mqtt.py
+nmd/nl_sensors.py
+nmd/nl_serial.py
+nmd/utils.py
+nmd/systemd_script/create_service.sh
+nmd/systemd_script/noolite_mtrf_mqtt.service
+noolite_mtrf_mqtt.egg-info/PKG-INFO
+noolite_mtrf_mqtt.egg-info/SOURCES.txt
+noolite_mtrf_mqtt.egg-info/dependency_links.txt
+noolite_mtrf_mqtt.egg-info/entry_points.txt
+noolite_mtrf_mqtt.egg-info/requires.txt
+noolite_mtrf_mqtt.egg-info/top_level.txt
\ No newline at end of file
diff --git a/noolite_mtrf_mqtt.egg-info/dependency_links.txt b/noolite_mtrf_mqtt.egg-info/dependency_links.txt
new file mode 100644 (file)
index 0000000..8b13789
--- /dev/null
@@ -0,0 +1 @@
+
diff --git a/noolite_mtrf_mqtt.egg-info/entry_points.txt b/noolite_mtrf_mqtt.egg-info/entry_points.txt
new file mode 100644 (file)
index 0000000..1068950
--- /dev/null
@@ -0,0 +1,3 @@
+[console_scripts]
+noolite_mtrf_mqtt = nmd.main:run
+
diff --git a/noolite_mtrf_mqtt.egg-info/requires.txt b/noolite_mtrf_mqtt.egg-info/requires.txt
new file mode 100644 (file)
index 0000000..e36a5a7
--- /dev/null
@@ -0,0 +1,2 @@
+hbmqtt
+pyserial
diff --git a/noolite_mtrf_mqtt.egg-info/top_level.txt b/noolite_mtrf_mqtt.egg-info/top_level.txt
new file mode 100644 (file)
index 0000000..5d389f5
--- /dev/null
@@ -0,0 +1 @@
+nmd
diff --git a/requirements.txt b/requirements.txt
new file mode 100644 (file)
index 0000000..2163a75
--- /dev/null
@@ -0,0 +1,2 @@
+hbmqtt==0.9
+pyserial==3.4
diff --git a/setup.py b/setup.py
new file mode 100644 (file)
index 0000000..2c565b1
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,29 @@
+from setuptools import setup, find_packages
+
+
+try:
+    from pypandoc import convert
+except ImportError:
+    import io
+
+    def convert(filename, fmt):
+        with io.open(filename, encoding='utf-8') as fd:
+            return fd.read()
+
+
+setup(
+    name='noolite-mtrf-mqtt',
+    description='NooLite MTRF serial port to MQTT messages',
+    url='https://bitbucket.org/AlekseevAV/noolite-mtrf-to-mqtt',
+    version='0.1.3',
+    long_description=convert('README.md', 'rst'),
+    packages=find_packages(),
+    include_package_data=True,
+    install_requires=[
+        'hbmqtt',
+        'pyserial',
+    ],
+    entry_points={
+        'console_scripts': ['noolite_mtrf_mqtt=nmd.main:run'],
+    }
+)