main.py 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131
  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. async def details_handler(details: dict[str, any]):
  68. if details:
  69. print(f"Battery: {details['battery_percentage']}% ({details['battery_voltage']}V)")
  70. await mqtt_publish(details, client)
  71. else:
  72. print("No values recieved")
  73. while True:
  74. try:
  75. async with BleClient(address) as mppt:
  76. mppt.on_details_received = details_handler
  77. while True:
  78. await mppt.request_details()
  79. await asyncio.sleep(20.0)
  80. except BleakDeviceNotFoundError:
  81. print(f"BLE device with address {address} was not found")
  82. await asyncio.sleep(5) # Wait for 5 seconds before retrying
  83. except BleakError as e:
  84. print(f"BLE error occurred: {e}")
  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. if __name__ == '__main__':
  104. parser = argparse.ArgumentParser(description='Solarlife MPPT BLE Client')
  105. parser.add_argument('address', help='BLE device address')
  106. parser.add_argument('--host', help='MQTT broker host', default='localhost')
  107. parser.add_argument('--port', help='MQTT broker port', default=1883, type=int)
  108. parser.add_argument('--username', help='MQTT username')
  109. parser.add_argument('--password', help='MQTT password')
  110. args = parser.parse_args()
  111. asyncio.run(main(args.address, args.host, args.port, args.username, args.password))