main.py 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143
  1. #!/usr/bin/env python3
  2. import argparse
  3. import asyncio
  4. import signal
  5. import traceback
  6. import aiomqtt
  7. from bleak import BleakScanner
  8. from bleak.exc import BleakError, BleakDeviceNotFoundError
  9. from src.homeassistant import MqttSensor
  10. from src.bleclient import BleClient, Result
  11. from src.variables import variables, VariableContainer, battery_and_load_parameters, switches
  12. request_interval = 20 # In seconds
  13. reconnect_interval = 5 # In seconds
  14. ble_lock = asyncio.Lock()
  15. async def request_and_publish_details(sensor: MqttSensor, address: str) -> None:
  16. async with ble_lock:
  17. try:
  18. async with BleClient(address) as mppt:
  19. details = await mppt.request_details()
  20. if details:
  21. print(f"Battery: {details['battery_percentage'].value}% ({details['battery_voltage'].value}V)")
  22. await sensor.publish(details)
  23. else:
  24. print("No values recieved")
  25. except (BleakError, asyncio.TimeoutError) as e:
  26. print(f"Got {type(e).__name__} while fetching details: {e}")
  27. async def request_and_publish_parameters(sensor: MqttSensor, address: str) -> None:
  28. async with ble_lock:
  29. async with BleClient(address) as mppt:
  30. parameters = await mppt.request_parameters()
  31. if parameters:
  32. await sensor.publish(parameters)
  33. async def subscribe_and_watch(sensor: MqttSensor, address: str):
  34. parameters = battery_and_load_parameters[:12] + switches
  35. await sensor.subscribe(parameters)
  36. await sensor.store_config(switches)
  37. while True:
  38. command = await sensor.get_command()
  39. print(f"Received command to set {command.name} to '{command.value}'")
  40. async with ble_lock:
  41. try:
  42. async with BleClient(address) as mppt:
  43. results = await mppt.write([command])
  44. await sensor.publish(results)
  45. except (BleakError, asyncio.TimeoutError) as e:
  46. print(f"Get {type(e).__name__} while writing command: {e}")
  47. async def run_mppt(sensor: MqttSensor, address: str):
  48. loop = asyncio.get_event_loop()
  49. task = loop.create_task(subscribe_and_watch(sensor, address))
  50. try:
  51. await request_and_publish_parameters(sensor, address)
  52. while True:
  53. await request_and_publish_details(sensor, address)
  54. await asyncio.sleep(request_interval)
  55. if task.done() and task.exception():
  56. break
  57. except (asyncio.TimeoutError, BleakDeviceNotFoundError, BleakError) as e:
  58. print(f"{type(e).__name__} occurred: {e}")
  59. finally:
  60. task.cancel()
  61. try:
  62. await task
  63. except asyncio.CancelledError:
  64. pass
  65. print("BLE session ended.")
  66. async def run_mqtt(address, host, port, username, password):
  67. while True:
  68. try:
  69. async with MqttSensor(hostname=host, port=port, username=username, password=password) as sensor:
  70. print(f"Connected to MQTT broker at {host}:{port}")
  71. while True:
  72. await run_mppt(sensor, address)
  73. await asyncio.sleep(reconnect_interval)
  74. except aiomqtt.MqttError as error:
  75. print(f'Error "{error}". Reconnecting in {reconnect_interval} seconds.')
  76. except asyncio.CancelledError:
  77. raise # Re-raise the CancelledError to stop the task
  78. except Exception as e:
  79. print(traceback.format_exc())
  80. await asyncio.sleep(reconnect_interval)
  81. async def main(*args):
  82. try:
  83. loop = asyncio.get_running_loop()
  84. task = loop.create_task(run_mqtt(*args))
  85. # Setup signal handler to cancel the task on termination
  86. for signame in {'SIGINT', 'SIGTERM'}:
  87. loop.add_signal_handler(getattr(signal, signame),
  88. task.cancel)
  89. await task # Wait for the task to complete
  90. except asyncio.CancelledError:
  91. pass # Task was cancelled, no need for an error message
  92. async def list_services(address):
  93. async with BleClient(address) as mppt:
  94. await mppt.list_services()
  95. async def scan_for_devices():
  96. devices = await BleakScanner.discover()
  97. if not devices:
  98. print("No BLE devices found.")
  99. else:
  100. print("Available BLE devices:")
  101. for device in devices:
  102. print(f"{device.address} - {device.name}")
  103. return devices
  104. if __name__ == '__main__':
  105. parser = argparse.ArgumentParser(description='Solarlife MPPT BLE Client')
  106. parser.add_argument('address', help='BLE device address')
  107. parser.add_argument('--host', help='MQTT broker host', default='localhost')
  108. parser.add_argument('--port', help='MQTT broker port', default=1883, type=int)
  109. parser.add_argument('--username', help='MQTT username')
  110. parser.add_argument('--password', help='MQTT password')
  111. parser.add_argument('--list-services', help='List GATT services', action='store_true')
  112. parser.add_argument('--scan', help='Scan for bluetooth devices', action='store_true')
  113. args = parser.parse_args()
  114. if args.scan:
  115. asyncio.run(scan_for_devices())
  116. elif args.list_services:
  117. asyncio.run(list_services(args.address))
  118. else:
  119. asyncio.run(main(args.address, args.host, args.port, args.username, args.password))