--- /dev/null
+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
--- /dev/null
+include README.md
+recursive-include nmd/systemd_script *
\ No newline at end of file
--- /dev/null
+# 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 - топик, куда публикуются все принятые сообщения с адаптера
--- /dev/null
+from .nl_mqtt import MqttDriver
\ No newline at end of file
--- /dev/null
+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()
--- /dev/null
+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
+
--- /dev/null
+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
--- /dev/null
+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
--- /dev/null
+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
--- /dev/null
+[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
--- /dev/null
+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]
--- /dev/null
+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
--- /dev/null
+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
--- /dev/null
+[console_scripts]
+noolite_mtrf_mqtt = nmd.main:run
+
--- /dev/null
+hbmqtt
+pyserial
--- /dev/null
+hbmqtt==0.9
+pyserial==3.4
--- /dev/null
+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'],
+ }
+)