From f7d019c1ffa7d028d901b9fae1170b0778d5c605 Mon Sep 17 00:00:00 2001 From: Roman Bazalevskiy Date: Sun, 1 Apr 2018 18:54:26 +0300 Subject: [PATCH] Forked from https://bitbucket.org/AlekseevAV/noolite-mtrf-to-mqtt/ MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit Добавлен разбор возврата от MTRF-64 и человекочитаемые топики MQTT --- Dockerfile | 26 ++ MANIFEST.in | 2 + README.md | 52 +++ nmd/__init__.py | 1 + nmd/main.py | 63 ++++ nmd/nl_mqtt.py | 301 ++++++++++++++++++ nmd/nl_sensors.py | 173 ++++++++++ nmd/nl_serial.py | 220 +++++++++++++ nmd/systemd_script/create_service.sh | 6 + nmd/systemd_script/noolite_mtrf_mqtt.service | 10 + nmd/utils.py | 7 + noolite_mtrf_mqtt.egg-info/PKG-INFO | 62 ++++ noolite_mtrf_mqtt.egg-info/SOURCES.txt | 17 + .../dependency_links.txt | 1 + noolite_mtrf_mqtt.egg-info/entry_points.txt | 3 + noolite_mtrf_mqtt.egg-info/requires.txt | 2 + noolite_mtrf_mqtt.egg-info/top_level.txt | 1 + requirements.txt | 2 + setup.py | 29 ++ 19 files changed, 978 insertions(+) create mode 100644 Dockerfile create mode 100644 MANIFEST.in create mode 100644 README.md create mode 100644 nmd/__init__.py create mode 100644 nmd/main.py create mode 100644 nmd/nl_mqtt.py create mode 100644 nmd/nl_sensors.py create mode 100644 nmd/nl_serial.py create mode 100644 nmd/systemd_script/create_service.sh create mode 100644 nmd/systemd_script/noolite_mtrf_mqtt.service create mode 100644 nmd/utils.py create mode 100644 noolite_mtrf_mqtt.egg-info/PKG-INFO create mode 100644 noolite_mtrf_mqtt.egg-info/SOURCES.txt create mode 100644 noolite_mtrf_mqtt.egg-info/dependency_links.txt create mode 100644 noolite_mtrf_mqtt.egg-info/entry_points.txt create mode 100644 noolite_mtrf_mqtt.egg-info/requires.txt create mode 100644 noolite_mtrf_mqtt.egg-info/top_level.txt create mode 100644 requirements.txt create mode 100644 setup.py diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..e49f27d --- /dev/null +++ b/Dockerfile @@ -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 index 0000000..cd14874 --- /dev/null +++ b/MANIFEST.in @@ -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 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 index 0000000..1180dfe --- /dev/null +++ b/nmd/__init__.py @@ -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 index 0000000..c554b90 --- /dev/null +++ b/nmd/main.py @@ -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 index 0000000..d0b26b3 --- /dev/null +++ b/nmd/nl_mqtt.py @@ -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 index 0000000..764bbc4 --- /dev/null +++ b/nmd/nl_sensors.py @@ -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 index 0000000..122182a --- /dev/null +++ b/nmd/nl_serial.py @@ -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 index 0000000..47353af --- /dev/null +++ b/nmd/systemd_script/create_service.sh @@ -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 index 0000000..bf3295f --- /dev/null +++ b/nmd/systemd_script/noolite_mtrf_mqtt.service @@ -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 index 0000000..3776cb9 --- /dev/null +++ b/nmd/utils.py @@ -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 index 0000000..1fe2260 --- /dev/null +++ b/noolite_mtrf_mqtt.egg-info/PKG-INFO @@ -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 index 0000000..12fb64d --- /dev/null +++ b/noolite_mtrf_mqtt.egg-info/SOURCES.txt @@ -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 index 0000000..8b13789 --- /dev/null +++ b/noolite_mtrf_mqtt.egg-info/dependency_links.txt @@ -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 index 0000000..1068950 --- /dev/null +++ b/noolite_mtrf_mqtt.egg-info/entry_points.txt @@ -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 index 0000000..e36a5a7 --- /dev/null +++ b/noolite_mtrf_mqtt.egg-info/requires.txt @@ -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 index 0000000..5d389f5 --- /dev/null +++ b/noolite_mtrf_mqtt.egg-info/top_level.txt @@ -0,0 +1 @@ +nmd diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..2163a75 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +hbmqtt==0.9 +pyserial==3.4 diff --git a/setup.py b/setup.py new file mode 100644 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'], + } +) -- 2.34.1