diff --git a/config.example.json b/config.example.json new file mode 100644 index 00000000..f0a749f8 --- /dev/null +++ b/config.example.json @@ -0,0 +1,68 @@ +{ + "matrix": { + "width": 64, + "height": 32, + "brightness": 100 + }, + "display": { + "enabled": true, + "display_durations": { + "clock": 15, + "weather": 15, + "stocks": 45, + "calendar": 30, + "stock_news": 30 + } + }, + "clock": { + "enabled": true, + "format": "%I:%M:%S %p" + }, + "weather": { + "enabled": true, + "api_key": "your_openweather_api_key", + "update_interval": 300, + "units": "imperial" + }, + "location": { + "lat": 0.0, + "lon": 0.0, + "city": "Your City" + }, + "stocks": { + "enabled": true, + "symbols": ["AAPL", "GOOGL", "MSFT"], + "update_interval": 300 + }, + "stock_news": { + "enabled": true, + "symbols": ["AAPL", "GOOGL", "MSFT"], + "update_interval": 900, + "max_headlines": 3 + }, + "calendar": { + "enabled": true, + "credentials_file": "credentials.json", + "token_file": "token.json", + "update_interval": 300, + "max_events": 3 + }, + "mqtt": { + "broker": "homeassistant.local", + "port": 1883, + "username": "your_mqtt_username", + "password": "your_mqtt_password", + "client_id": "led_matrix", + "use_tls": false, + "topics": [ + "homeassistant/sensor/#", + "homeassistant/binary_sensor/#", + "homeassistant/switch/#" + ], + "display": { + "scroll_speed": 0.1, + "message_timeout": 60, + "max_messages": 5 + } + } +} \ No newline at end of file diff --git a/credentials.json b/credentials.json new file mode 100755 index 00000000..a8a84e05 --- /dev/null +++ b/credentials.json @@ -0,0 +1 @@ +{"installed":{"client_id":"440006573525-kg1idpl6fj3i17eg6mlucaj10uk4341v.apps.googleusercontent.com","project_id":"homeassistant-309617","auth_uri":"https://accounts.google.com/o/oauth2/auth","token_uri":"https://oauth2.googleapis.com/token","auth_provider_x509_cert_url":"https://www.googleapis.com/oauth2/v1/certs","client_secret":"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"}} \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index e4bb92d9..ade03043 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,3 @@ -rpi-rgb-led-matrix==1.0.0 Pillow==10.0.0 pytz==2023.3 requests==2.31.0 diff --git a/src/mqtt_manager.py b/src/mqtt_manager.py new file mode 100644 index 00000000..f7fe048b --- /dev/null +++ b/src/mqtt_manager.py @@ -0,0 +1,199 @@ +import paho.mqtt.client as mqtt +import json +from typing import Dict, Any, Optional, List, Callable +from queue import Queue +import threading +import time + +class MQTTManager: + def __init__(self, config: Dict[str, Any], display_manager): + """Initialize MQTT Manager with configuration.""" + self.config = config.get('mqtt', {}) + self.display_manager = display_manager + self.client = None + self.connected = False + self.message_queue = Queue() + self.subscriptions = {} + self.last_update = 0 + self.current_messages = {} + + # MQTT Configuration + self.broker = self.config.get('broker', 'localhost') + self.port = self.config.get('port', 1883) + self.username = self.config.get('username') + self.password = self.config.get('password') + self.client_id = self.config.get('client_id', 'led_matrix') + + # Display Configuration + self.scroll_speed = self.config.get('scroll_speed', 0.1) + self.message_timeout = self.config.get('message_timeout', 60) + self.max_messages = self.config.get('max_messages', 5) + + # Initialize MQTT client + self._setup_mqtt() + + # Start message processing thread + self.running = True + self.process_thread = threading.Thread(target=self._process_messages) + self.process_thread.daemon = True + self.process_thread.start() + + def _setup_mqtt(self): + """Setup MQTT client and callbacks.""" + self.client = mqtt.Client(client_id=self.client_id) + + # Set up callbacks + self.client.on_connect = self._on_connect + self.client.on_message = self._on_message + self.client.on_disconnect = self._on_disconnect + + # Set up authentication if configured + if self.username and self.password: + self.client.username_pw_set(self.username, self.password) + + # Set up TLS if configured + if self.config.get('use_tls', False): + self.client.tls_set() + + def _on_connect(self, client, userdata, flags, rc): + """Callback for when the client connects to the broker.""" + if rc == 0: + print("Connected to MQTT broker") + self.connected = True + # Subscribe to configured topics + for topic in self.config.get('topics', []): + self.subscribe(topic) + else: + print(f"Failed to connect to MQTT broker with code: {rc}") + + def _on_message(self, client, userdata, message): + """Callback for when a message is received.""" + try: + payload = message.payload.decode() + try: + # Try to parse as JSON + payload = json.loads(payload) + except json.JSONDecodeError: + # If not JSON, use as plain text + pass + + self.message_queue.put({ + 'topic': message.topic, + 'payload': payload, + 'timestamp': time.time() + }) + except Exception as e: + print(f"Error processing message: {e}") + + def _on_disconnect(self, client, userdata, rc): + """Callback for when the client disconnects from the broker.""" + print("Disconnected from MQTT broker") + self.connected = False + if rc != 0: + print(f"Unexpected disconnection. Attempting to reconnect...") + self._reconnect() + + def _reconnect(self): + """Attempt to reconnect to the MQTT broker.""" + while not self.connected and self.running: + try: + self.client.connect(self.broker, self.port) + self.client.loop_start() + break + except Exception as e: + print(f"Reconnection failed: {e}") + time.sleep(5) + + def start(self): + """Start the MQTT client connection.""" + try: + self.client.connect(self.broker, self.port) + self.client.loop_start() + except Exception as e: + print(f"Failed to connect to MQTT broker: {e}") + self._reconnect() + + def stop(self): + """Stop the MQTT client and message processing.""" + self.running = False + if self.client: + self.client.loop_stop() + self.client.disconnect() + + def subscribe(self, topic: str, qos: int = 0): + """Subscribe to an MQTT topic.""" + if self.client: + self.client.subscribe(topic, qos) + print(f"Subscribed to topic: {topic}") + + def _process_messages(self): + """Process messages from the queue and update display.""" + while self.running: + try: + # Remove expired messages + current_time = time.time() + self.current_messages = { + topic: msg for topic, msg in self.current_messages.items() + if current_time - msg['timestamp'] < self.message_timeout + } + + # Process new messages + while not self.message_queue.empty(): + message = self.message_queue.get() + self.current_messages[message['topic']] = message + + # Update display if we have messages + if self.current_messages: + self._update_display() + + time.sleep(0.1) + except Exception as e: + print(f"Error in message processing: {e}") + + def _update_display(self): + """Update the LED matrix display with current messages.""" + try: + # Create a new image for drawing + image = Image.new('RGB', (self.display_manager.matrix.width, self.display_manager.matrix.height)) + draw = ImageDraw.Draw(image) + + # Sort messages by timestamp (newest first) + messages = sorted( + self.current_messages.values(), + key=lambda x: x['timestamp'], + reverse=True + )[:self.max_messages] + + # Calculate display layout + section_height = self.display_manager.matrix.height // min(len(messages), self.max_messages) + + # Draw each message + for i, message in enumerate(messages): + y = i * section_height + + # Format message based on type + if isinstance(message['payload'], dict): + display_text = self._format_dict_message(message['payload']) + else: + display_text = str(message['payload']) + + # Draw message text + draw.text((1, y + 1), display_text, + font=self.display_manager.small_font, + fill=(255, 255, 255)) + + # Update the display + self.display_manager.image = image + self.display_manager.update_display() + + except Exception as e: + print(f"Error updating display: {e}") + + def _format_dict_message(self, payload: Dict[str, Any]) -> str: + """Format a dictionary payload for display.""" + # Handle Home Assistant state messages + if 'state' in payload: + return f"{payload.get('state', '')}" + + # Handle other dictionary messages + return json.dumps(payload, separators=(',', ':')) \ No newline at end of file