main.py 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151
  1. #!/usr/bin/env python3
  2. import argparse
  3. import asyncio
  4. import signal
  5. import json
  6. import aiomqtt
  7. from bleak import BleakScanner
  8. from bleak.exc import BleakError, BleakDeviceNotFoundError
  9. from src.bleclient import BleClient
  10. send_config = True
  11. reconnect_interval = 5 # In seconds
  12. async def mqtt_publish(details: dict[str, any], client: aiomqtt.Client):
  13. global send_config
  14. # Define the base topic for MQTT Discovery
  15. base_topic = "homeassistant"
  16. # Define the device information
  17. device_info = {
  18. "identifiers": ["solarlife_mppt_ble"],
  19. "name": "Solarlife MPPT",
  20. "manufacturer": "Solarlife",
  21. }
  22. # Publish each item in the details dictionary to its own MQTT topic
  23. for key, value in details.items():
  24. state_topic = f"{base_topic}/sensor/solarlife/{key}/state"
  25. topic = f"{base_topic}/sensor/solarlife/{key}/config"
  26. # Create the MQTT Discovery payload
  27. payload = {
  28. "name": f"Solarlife {key.replace('_', ' ').title()}",
  29. "device": device_info,
  30. "unique_id": f"solarlife_{key}",
  31. "state_topic": state_topic,
  32. "unit_of_measurement": BleClient.get_unit_of_measurement(key)
  33. }
  34. if "daily_energy" in key:
  35. payload['device_class'] = "energy"
  36. payload['state_class'] = "total_increasing"
  37. elif "total_energy" in key:
  38. payload['device_class'] = "energy"
  39. payload['state_class'] = "total"
  40. elif "voltage" in key:
  41. payload['device_class'] = "voltage"
  42. payload['state_class'] = "measurement"
  43. elif "current" in key:
  44. payload['device_class'] = "current"
  45. payload['state_class'] = "measurement"
  46. elif "power" in key:
  47. payload['device_class'] = "power"
  48. payload['state_class'] = "measurement"
  49. elif "temperature" in key:
  50. payload['device_class'] = "temperature"
  51. payload['state_class'] = "measurement"
  52. elif key == "battery_percentage":
  53. payload['device_class'] = "battery"
  54. payload['state_class'] = "measurement"
  55. # Publish the MQTT Discovery payload
  56. if send_config:
  57. print(f"Publishing MQTT Discovery payload for {key}")
  58. await client.publish(topic, payload=json.dumps(payload), retain=True)
  59. # Publish the entity state
  60. await client.publish(state_topic, payload=str(value))
  61. send_config = False
  62. async def main(address, host, port, username, password):
  63. async def run_mppt():
  64. while True:
  65. try:
  66. async with aiomqtt.Client(hostname=host, port=port, username=username, password=password) as client:
  67. print(f"Connecting to MQTT broker at {host}:{port}")
  68. while True:
  69. try:
  70. async with BleClient(address) as mppt:
  71. while True:
  72. details = await mppt.request_details()
  73. if details:
  74. print(f"Battery: {details['battery_percentage']}% ({details['battery_voltage']}V)")
  75. await mqtt_publish(details, client)
  76. else:
  77. print("No values recieved")
  78. await asyncio.sleep(20.0)
  79. except BleakDeviceNotFoundError:
  80. print(f"BLE device with address {address} was not found")
  81. await asyncio.sleep(5)
  82. except BleakError as e:
  83. print(f"BLE error occurred: {e}")
  84. await asyncio.sleep(5)
  85. except aiomqtt.MqttError as error:
  86. print(f'Error "{error}". Reconnecting in {reconnect_interval} seconds.')
  87. await asyncio.sleep(reconnect_interval)
  88. except asyncio.CancelledError:
  89. raise # Re-raise the CancelledError to stop the task
  90. except Exception as e:
  91. print(f"An error occurred during BLE communication: {e}")
  92. await asyncio.sleep(5) # Wait for 5 seconds before retrying
  93. try:
  94. loop = asyncio.get_running_loop()
  95. task = loop.create_task(run_mppt())
  96. # Setup signal handler to cancel the task on termination
  97. for signame in {'SIGINT', 'SIGTERM'}:
  98. loop.add_signal_handler(getattr(signal, signame),
  99. task.cancel)
  100. await task # Wait for the task to complete
  101. except asyncio.CancelledError:
  102. pass # Task was cancelled, no need for an error message
  103. async def list_services(address):
  104. async with BleClient(address) as mppt:
  105. mppt.list_services()
  106. async def scan_for_devices():
  107. devices = await BleakScanner.discover()
  108. if not devices:
  109. print("No BLE devices found.")
  110. else:
  111. print("Available BLE devices:")
  112. for device in devices:
  113. print(f"{device.address} - {device.name}")
  114. return devices
  115. if __name__ == '__main__':
  116. parser = argparse.ArgumentParser(description='Solarlife MPPT BLE Client')
  117. parser.add_argument('address', help='BLE device address')
  118. parser.add_argument('--host', help='MQTT broker host', default='localhost')
  119. parser.add_argument('--port', help='MQTT broker port', default=1883, type=int)
  120. parser.add_argument('--username', help='MQTT username')
  121. parser.add_argument('--password', help='MQTT password')
  122. parser.add_argument('--list-services', help='List GATT services', action='store_true')
  123. parser.add_argument('--scan', help='Scan for bluetooth devices', action='store_true')
  124. args = parser.parse_args()
  125. if args.scan:
  126. asyncio.run(scan_for_devices())
  127. elif args.list_services:
  128. asyncio.run(list_services(args.address))
  129. else:
  130. asyncio.run(main(args.address, args.host, args.port, args.username, args.password))