main.py 5.2 KB

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