diff --git a/examples/mqtt_example.py b/examples/mqtt_example.py index 7314b3a..a3cd5f5 100644 --- a/examples/mqtt_example.py +++ b/examples/mqtt_example.py @@ -20,69 +20,62 @@ MQTT_CONFIG = { "client_id": "esp32_example", "broker": "mqtt.fritz.box", # Replace with your MQTT broker address "port": 1883, - "username": "geeky", # Replace with your MQTT username - "password": "geeky", # Replace with your MQTT password + "username": "geeky", # Replace with your MQTT username + "password": "geeky", # Replace with your MQTT password "topic_data": "esp32/example/data", - "topic_control": "esp32/example/control" + "topic_control": "esp32/example/control", } + def main(): print("Starting MQTT Example") - + # Create and connect MQTT client client = ESP32MQTTClient( MQTT_CONFIG["client_id"], MQTT_CONFIG["broker"], MQTT_CONFIG["port"], MQTT_CONFIG["username"], - MQTT_CONFIG["password"] + MQTT_CONFIG["password"], ) - + # Connect to the broker if not client.connect(): print("Failed to connect to MQTT broker") return - + print("Connected to MQTT broker") - + try: # Subscribe to the control topic client.subscribe(MQTT_CONFIG["topic_control"]) print(f"Subscribed to {MQTT_CONFIG['topic_control']}") - + # Publish some data - data = { - "temperature": 25.5, - "humidity": 60.2, - "timestamp": time.time() - } - + data = {"temperature": 25.5, "humidity": 60.2, "timestamp": time.time()} + print(f"Publishing data to {MQTT_CONFIG['topic_data']}") - client.publish( - MQTT_CONFIG["topic_data"], - json.dumps(data), - retain=True - ) + client.publish(MQTT_CONFIG["topic_data"], json.dumps(data), retain=True) # Read from the control topic with a timeout print(f"Waiting for messages on {MQTT_CONFIG['topic_control']} (timeout: 10s)") message = client.read_topic(MQTT_CONFIG["topic_control"], 10) - + if message: # Process the message if isinstance(message, bytes): - message = message.decode('utf-8') - + message = message.decode("utf-8") + try: control_data = json.loads(message) print(f"Received control message: {control_data}") - + # Example of processing a command if "command" in control_data: command = control_data["command"] value = control_data.get("value") print(f"Processing command: {command} with value: {value}") - + # Here you would handle different commands if command == "set_led": print(f"Setting LED to {value}") @@ -94,14 +87,15 @@ def main(): print(f"Received non-JSON message: {message}") else: print("No control message received within timeout period") - + # Disconnect from the broker client.disconnect() print("Disconnected from MQTT broker") - + except Exception as e: print(f"Error in MQTT example: {e}") client.disconnect() + if __name__ == "__main__": main() diff --git a/src/esp_sensors/config.py b/src/esp_sensors/config.py index 8d067e9..f3eaecf 100644 --- a/src/esp_sensors/config.py +++ b/src/esp_sensors/config.py @@ -63,9 +63,10 @@ DEFAULT_CONFIG = { "ssid": "", "password": "", "timeout": 10, - } + }, } + class Config: """ Configuration class to manage loading and saving configuration settings. @@ -124,7 +125,8 @@ class Config: self.update_configs(config) return save_config_to_file(config, self.config_path) -def load_config(config_path: str = DEFAULT_CONFIG_PATH) : + +def load_config(config_path: str = DEFAULT_CONFIG_PATH): """ Load configuration from a JSON file. @@ -146,9 +148,7 @@ def load_config(config_path: str = DEFAULT_CONFIG_PATH) : return DEFAULT_CONFIG -def get_sensor_config( - sensor_type: str, config: dict | None = None -) -> dict: +def get_sensor_config(sensor_type: str, config: dict | None = None) -> dict: """ Get configuration for a specific sensor type. @@ -170,9 +170,7 @@ def get_sensor_config( return sensor_config -def get_display_config( - display_type: str, config: dict | None = None -) -> dict: +def get_display_config(display_type: str, config: dict | None = None) -> dict: """ Get configuration for a specific display type. @@ -285,7 +283,9 @@ def save_config_to_file(config: dict, config_path: str = DEFAULT_CONFIG_PATH) -> return False -def check_and_update_config_from_mqtt(mqtt_client, mqtt_config: dict, current_config: dict) -> dict: +def check_and_update_config_from_mqtt( + mqtt_client, mqtt_config: dict, current_config: dict +) -> dict: """ Check for configuration updates from MQTT and update local configuration if needed. diff --git a/src/esp_sensors/dht22.py b/src/esp_sensors/dht22.py index 8e4060f..042aa84 100644 --- a/src/esp_sensors/dht22.py +++ b/src/esp_sensors/dht22.py @@ -74,7 +74,9 @@ class DHT22Sensor(TemperatureSensor, HumiditySensor): self.name = ( name if name is not None else sensor_config.get("name", "DHT22 Sensor") ) - self.id = sensor_config.get("id", "dht22_" + self.name.lower().replace(" ", "_")) + self.id = sensor_config.get( + "id", "dht22_" + self.name.lower().replace(" ", "_") + ) self.pin = pin if pin is not None else sensor_config.get("pin", 0) self.interval = ( interval if interval is not None else sensor_config.get("interval", 60) diff --git a/src/esp_sensors/mqtt.py b/src/esp_sensors/mqtt.py index 19a1e96..8777b7f 100644 --- a/src/esp_sensors/mqtt.py +++ b/src/esp_sensors/mqtt.py @@ -10,12 +10,27 @@ This module uses the MQTTClient class from mqtt_client.py for the core MQTT impl import time import json from .mqtt_client import ( - MQTTClient, MQTTException, - CONNECT, CONNACK, PUBLISH, PUBACK, SUBSCRIBE, SUBACK, - UNSUBSCRIBE, UNSUBACK, PINGREQ, PINGRESP, DISCONNECT, - CONN_ACCEPTED, CONN_REFUSED_PROTOCOL, CONN_REFUSED_IDENTIFIER, - CONN_REFUSED_SERVER, CONN_REFUSED_USER_PASS, CONN_REFUSED_AUTH, - MQTT_PROTOCOL_LEVEL, MQTT_CLEAN_SESSION + MQTTClient, + MQTTException, + CONNECT, + CONNACK, + PUBLISH, + PUBACK, + SUBSCRIBE, + SUBACK, + UNSUBSCRIBE, + UNSUBACK, + PINGREQ, + PINGRESP, + DISCONNECT, + CONN_ACCEPTED, + CONN_REFUSED_PROTOCOL, + CONN_REFUSED_IDENTIFIER, + CONN_REFUSED_SERVER, + CONN_REFUSED_USER_PASS, + CONN_REFUSED_AUTH, + MQTT_PROTOCOL_LEVEL, + MQTT_CLEAN_SESSION, ) @@ -70,7 +85,9 @@ class ESP32MQTTClient: bool: True if connection was successful, False otherwise """ try: - print(f"[ESP32MQTT] Connecting to {self.server}:{self.port} as {self.client_id}") + print( + f"[ESP32MQTT] Connecting to {self.server}:{self.port} as {self.client_id}" + ) # Create our custom MQTT client self.client = MQTTClient( self.client_id, @@ -79,7 +96,7 @@ class ESP32MQTTClient: self.user, self.password, self.keepalive, - self.ssl + self.ssl, ) # Set up callback to store received messages @@ -178,8 +195,8 @@ class ESP32MQTTClient: topic (bytes): The topic the message was received on msg (bytes): The message payload """ - topic_str = topic.decode('utf-8') if isinstance(topic, bytes) else topic - msg_str = msg.decode('utf-8') if isinstance(msg, bytes) else msg + topic_str = topic.decode("utf-8") if isinstance(topic, bytes) else topic + msg_str = msg.decode("utf-8") if isinstance(msg, bytes) else msg print(f"[ESP32MQTT] Message received on {topic_str}: {msg_str}") @@ -202,7 +219,11 @@ class ESP32MQTTClient: return None # Clear any previous message for this topic - topic_str = topic if isinstance(topic, str) else topic.decode('utf-8') if isinstance(topic, bytes) else str(topic) + topic_str = ( + topic + if isinstance(topic, str) + else topic.decode("utf-8") if isinstance(topic, bytes) else str(topic) + ) if topic_str in self.received_messages: del self.received_messages[topic_str] @@ -227,7 +248,9 @@ class ESP32MQTTClient: self.connected = False return None - print(f"[ESP32MQTT] No message received on {topic_str} after {wait_time} seconds") + print( + f"[ESP32MQTT] No message received on {topic_str} after {wait_time} seconds" + ) return None @@ -259,7 +282,9 @@ def setup_mqtt(mqtt_config: dict) -> ESP32MQTTClient | MQTTClient | None: if use_esp32_client: # Use the new ESP32MQTTClient - client = ESP32MQTTClient(client_id, broker, port, username, password, keepalive, ssl) + client = ESP32MQTTClient( + client_id, broker, port, username, password, keepalive, ssl + ) # Try to connect if client.connect(): @@ -268,13 +293,15 @@ def setup_mqtt(mqtt_config: dict) -> ESP32MQTTClient | MQTTClient | None: print("Failed to connect using ESP32MQTTClient") return client - # print("Failed to connect using ESP32MQTTClient, falling back to basic MQTTClient") - # # Fall back to basic client - # use_esp32_client = False + # print("Failed to connect using ESP32MQTTClient, falling back to basic MQTTClient") + # # Fall back to basic client + # use_esp32_client = False if not use_esp32_client: # Use the basic MQTTClient for backward compatibility - client = MQTTClient(client_id, broker, port, username, password, keepalive, ssl) + client = MQTTClient( + client_id, broker, port, username, password, keepalive, ssl + ) # Try to connect client.connect() @@ -350,7 +377,9 @@ def get_data_topic(mqtt_config): return mqtt_config.get("topic_data_prefix", "/homecontrol/device/data") -def subscribe_to_config(client: ESP32MQTTClient | MQTTClient | None, mqtt_config: dict) -> bool: +def subscribe_to_config( + client: ESP32MQTTClient | MQTTClient | None, mqtt_config: dict +) -> bool: """ Subscribe to the configuration topic. @@ -381,7 +410,9 @@ def subscribe_to_config(client: ESP32MQTTClient | MQTTClient | None, mqtt_config return False -def check_config_update(client: ESP32MQTTClient | MQTTClient | None, mqtt_config: dict, current_config: dict) -> dict: +def check_config_update( + client: ESP32MQTTClient | MQTTClient | None, mqtt_config: dict, current_config: dict +) -> dict: """ Check for configuration updates from MQTT. @@ -406,12 +437,18 @@ def check_config_update(client: ESP32MQTTClient | MQTTClient | None, mqtt_config topic_config = mqtt_config.get("topic_config") wait_time = mqtt_config.get("config_wait_time", 1.0) - print(f"Using ESP32MQTTClient to read from config topic with wait time: {wait_time}s") + print( + f"Using ESP32MQTTClient to read from config topic with wait time: {wait_time}s" + ) config_msg = client.read_topic(topic_config, wait_time) if config_msg: try: - msg_str = config_msg.decode('utf-8') if isinstance(config_msg, bytes) else config_msg + msg_str = ( + config_msg.decode("utf-8") + if isinstance(config_msg, bytes) + else config_msg + ) received_config = json.loads(msg_str) except Exception as e: print(f"Error parsing configuration message: {e}") @@ -423,15 +460,21 @@ def check_config_update(client: ESP32MQTTClient | MQTTClient | None, mqtt_config try: # Verify that the topic matches our expected topic expected_topic = mqtt_config.get("topic_config") - topic_str = topic.decode('utf-8') if isinstance(topic, bytes) else topic + topic_str = ( + topic.decode("utf-8") if isinstance(topic, bytes) else topic + ) if topic_str != expected_topic: - print(f"Ignoring message from topic {topic_str} - not matching our config topic {expected_topic}") + print( + f"Ignoring message from topic {topic_str} - not matching our config topic {expected_topic}" + ) return # Parse the message as JSON - msg_str = msg.decode('utf-8') if isinstance(msg, bytes) else msg + msg_str = msg.decode("utf-8") if isinstance(msg, bytes) else msg config_data = json.loads(msg_str) - print(f"Received configuration from MQTT: version {config_data.get('version', 0)}") + print( + f"Received configuration from MQTT: version {config_data.get('version', 0)}" + ) received_config = config_data except Exception as e: print(f"Error parsing configuration message: {e}") @@ -456,8 +499,12 @@ def check_config_update(client: ESP32MQTTClient | MQTTClient | None, mqtt_config print("done waiting for configuration updates") # If we received a configuration and its version is newer, return it - if received_config and received_config.get("version", 0) > current_config.get("version", 0): - print(f"Found newer configuration (version {received_config.get('version')})") + if received_config and received_config.get("version", 0) > current_config.get( + "version", 0 + ): + print( + f"Found newer configuration (version {received_config.get('version')})" + ) return received_config return current_config diff --git a/src/esp_sensors/mqtt_client.py b/src/esp_sensors/mqtt_client.py index 2450e6a..35bd23b 100644 --- a/src/esp_sensors/mqtt_client.py +++ b/src/esp_sensors/mqtt_client.py @@ -37,18 +37,21 @@ CONN_REFUSED_SERVER = 3 CONN_REFUSED_USER_PASS = 4 CONN_REFUSED_AUTH = 5 + class MQTTException(Exception): """MQTT Exception class for handling MQTT-specific errors""" + pass + class MQTTClient: """ A basic MQTT client implementation from scratch. - + This class implements the MQTT protocol directly using socket communication. It provides functionality for connecting to an MQTT broker, publishing messages, subscribing to topics, and receiving messages. - + Attributes: client_id (str): Unique identifier for this client server (str): MQTT broker address @@ -64,6 +67,7 @@ class MQTTClient: subscriptions (dict): Dictionary of subscribed topics last_ping (float): Timestamp of the last ping """ + def __init__( self, client_id, @@ -76,7 +80,7 @@ class MQTTClient: ): """ Initialize the MQTT client. - + Args: client_id (str): Unique identifier for this client server (str): MQTT broker address @@ -103,7 +107,7 @@ class MQTTClient: def _generate_packet_id(self): """ Generate a unique packet ID for MQTT messages. - + Returns: int: A unique packet ID between 1 and 65535 """ @@ -113,10 +117,10 @@ class MQTTClient: def _encode_length(self, length): """ Encode the remaining length field in the MQTT packet. - + Args: length (int): The length to encode - + Returns: bytearray: The encoded length """ @@ -134,25 +138,25 @@ class MQTTClient: def _encode_string(self, string): """ Encode a string for MQTT packet. - + Args: string (str or bytes): The string to encode - + Returns: bytearray: The encoded string """ if isinstance(string, str): - string = string.encode('utf-8') + string = string.encode("utf-8") return bytearray(struct.pack("!H", len(string)) + string) - def _send_packet(self, packet_type, payload=b''): + def _send_packet(self, packet_type, payload=b""): """ Send an MQTT packet to the broker. - + Args: packet_type (int): The MQTT packet type payload (bytes): The packet payload - + Raises: MQTTException: If the client is not connected or sending fails """ @@ -180,13 +184,13 @@ class MQTTClient: def _recv_packet(self, timeout=1.0): """ Receive an MQTT packet from the broker. - + Args: timeout (float): Socket timeout in seconds - + Returns: tuple: (packet_type, payload) or (None, None) if no packet received - + Raises: MQTTException: If the client is not connected or receiving fails """ @@ -213,7 +217,7 @@ class MQTTClient: break # Read the payload - payload = self.sock.recv(remaining_length) if remaining_length else b'' + payload = self.sock.recv(remaining_length) if remaining_length else b"" return packet_type[0], payload except socket.timeout: @@ -225,17 +229,19 @@ class MQTTClient: def connect(self): """ Connect to the MQTT broker. - + Returns: int: 0 if successful, otherwise an error code - + Raises: MQTTException: If connection fails """ # Create socket try: self.sock = socket.socket() - print(f"[MQTT] Connecting to Socket {self.server}:{self.port} as {self.client_id}") + print( + f"[MQTT] Connecting to Socket {self.server}:{self.port} as {self.client_id}" + ) self.sock.connect((self.server, self.port)) print(f"[MQTT] Connected to {self.server}:{self.port}") except Exception as e: @@ -306,7 +312,7 @@ class MQTTClient: def ping(self): """ Send PINGREQ to keep the connection alive. - + Raises: MQTTException: If no PINGRESP is received """ @@ -321,13 +327,13 @@ class MQTTClient: def publish(self, topic, msg, retain=False, qos=0): """ Publish a message to a topic. - + Args: topic (str or bytes): The topic to publish to msg (str or bytes): The message to publish retain (bool): Whether the message should be retained by the broker qos (int): Quality of Service level (0 or 1) - + Raises: MQTTException: If the client is not connected or publishing fails """ @@ -340,16 +346,16 @@ class MQTTClient: # Convert topic and message to bytes if they're not already if isinstance(topic, str): - topic = topic.encode('utf-8') + topic = topic.encode("utf-8") if isinstance(msg, str): - msg = msg.encode('utf-8') + msg = msg.encode("utf-8") # Construct PUBLISH packet packet_type = PUBLISH if retain: packet_type |= 0x01 if qos: - packet_type |= (qos << 1) + packet_type |= qos << 1 # Payload: topic + message payload = self._encode_string(topic) @@ -375,11 +381,11 @@ class MQTTClient: def subscribe(self, topic, qos=0): """ Subscribe to a topic. - + Args: topic (str or bytes): The topic to subscribe to qos (int): Quality of Service level - + Raises: MQTTException: If the client is not connected or subscription fails """ @@ -392,7 +398,7 @@ class MQTTClient: # Convert topic to bytes if it's not already if isinstance(topic, str): - topic = topic.encode('utf-8') + topic = topic.encode("utf-8") # Generate packet ID pid = self._generate_packet_id() @@ -411,7 +417,7 @@ class MQTTClient: raise MQTTException(f"No SUBACK received: {packet_type}") # Store subscription - topic_str = topic.decode('utf-8') if isinstance(topic, bytes) else topic + topic_str = topic.decode("utf-8") if isinstance(topic, bytes) else topic self.subscriptions[topic_str] = qos return @@ -419,7 +425,7 @@ class MQTTClient: def set_callback(self, callback): """ Set callback for received messages. - + Args: callback (callable): Function to call when a message is received. The callback should accept two parameters: @@ -430,7 +436,7 @@ class MQTTClient: def check_msg(self): """ Check for pending messages from the broker. - + This method should be called regularly to process incoming messages. If a callback is set, it will be called with the topic and message. """ @@ -455,21 +461,21 @@ class MQTTClient: # Extract topic topic_len = struct.unpack("!H", payload[0:2])[0] - topic = payload[2:2+topic_len] + topic = payload[2 : 2 + topic_len] # Extract packet ID for QoS > 0 if qos > 0: - pid = struct.unpack("!H", payload[2+topic_len:2+topic_len+2])[0] - message = payload[2+topic_len+2:] + pid = struct.unpack("!H", payload[2 + topic_len : 2 + topic_len + 2])[0] + message = payload[2 + topic_len + 2 :] # Send PUBACK for QoS 1 if qos == 1: self._send_packet(PUBACK, struct.pack("!H", pid)) else: - message = payload[2+topic_len:] + message = payload[2 + topic_len :] # Call the callback if set if self.callback: self.callback(topic, message) - return \ No newline at end of file + return diff --git a/src/esp_sensors/oled_display.py b/src/esp_sensors/oled_display.py index 1fe8c74..36982fa 100644 --- a/src/esp_sensors/oled_display.py +++ b/src/esp_sensors/oled_display.py @@ -1,6 +1,7 @@ """ OLED display module for ESP32 using SSD1306 controller. """ + LINE_HEIGHT = 8 # Height of each line in pixels HEADER_LINE = 0 @@ -23,16 +24,16 @@ class OLEDDisplay(Sensor): """SSD1306 OLED display implementation.""" def __init__( - self, - name: str = None, - scl_pin: int = None, - sda_pin: int = None, - width: int = None, - height: int = None, - address: int | str = None, - interval: int = None, - on_time: int = None, - display_config=None, + self, + name: str = None, + scl_pin: int = None, + sda_pin: int = None, + width: int = None, + height: int = None, + address: int | str = None, + interval: int = None, + on_time: int = None, + display_config=None, ): """ Initialize a new OLED display. @@ -99,7 +100,9 @@ class OLEDDisplay(Sensor): print(f" I2C bus: {i2c}") # print('i2c scan:', i2c.scan()) print(f" I2C address: {self.address}") - self._display = ssd1306.SSD1306_I2C(self.width, self.height, i2c, addr=self.address) + self._display = ssd1306.SSD1306_I2C( + self.width, self.height, i2c, addr=self.address + ) print(f" Display initialized: {self._display}") self._display.fill(0) # Clear the display self._display.text("Initialized", 0, 0, 1) @@ -149,7 +152,9 @@ class OLEDDisplay(Sensor): y = i * LINE_HEIGHT if y < self.height: # Make sure we don't go off the screen x = 0 - self._display.fill_rect(x, y, self.width, LINE_HEIGHT, 0) # Clear the line + self._display.fill_rect( + x, y, self.width, LINE_HEIGHT, 0 + ) # Clear the line self._display.text(str(value), x, y, 1) else: print(f"Line {i} exceeds display height, skipping") @@ -176,7 +181,9 @@ class OLEDDisplay(Sensor): # self._display.fill(0) # Clear the display x = 0 y = VALUE_LINES_START * LINE_HEIGHT - self._display.fill_rect(x, y, self.width, self.height-y, 0) # Clear the line + self._display.fill_rect( + x, y, self.width, self.height - y, 0 + ) # Clear the line # Display each value on a new line (8 pixels per line) for i, value in enumerate(values): self.set_line_text(VALUE_LINES_START + i, value) @@ -233,4 +240,5 @@ class OLEDDisplay(Sensor): metadata["type"] = "SSD1306" metadata["values_count"] = len(self._values) return metadata + # endregion diff --git a/src/esp_sensors/sensor.py b/src/esp_sensors/sensor.py index 6bafbec..b65e315 100644 --- a/src/esp_sensors/sensor.py +++ b/src/esp_sensors/sensor.py @@ -3,7 +3,6 @@ Base sensor module for ESP-based sensors. """ - class Sensor: """Base class for all sensors.""" @@ -12,7 +11,7 @@ class Sensor: name: str = None, pin: int = None, interval: int = None, - sensor_config = None, + sensor_config=None, ): """ Initialize a new sensor. @@ -30,13 +29,15 @@ class Sensor: self.name = ( name if name is not None else sensor_config.get("name", "Unnamed Sensor") ) - self.id = sensor_config.get("id", "sensor_" + self.name.lower().replace(" ", "_")) + self.id = sensor_config.get( + "id", "sensor_" + self.name.lower().replace(" ", "_") + ) self.pin = pin if pin is not None else sensor_config.get("pin", 0) self.interval = ( interval if interval is not None else sensor_config.get("interval", 60) ) - self._last_reading= None + self._last_reading = None def read(self) -> float: """ @@ -50,7 +51,7 @@ class Sensor: self._last_reading = 0.0 return self._last_reading - def get_metadata(self) : + def get_metadata(self): """ Get sensor metadata. diff --git a/src/main.py b/src/main.py index fefb302..23f7890 100644 --- a/src/main.py +++ b/src/main.py @@ -12,7 +12,12 @@ This program: import time from esp_sensors.dht22 import DHT22Sensor -from esp_sensors.mqtt import setup_mqtt, publish_sensor_data, check_config_update, get_data_topic +from esp_sensors.mqtt import ( + setup_mqtt, + publish_sensor_data, + check_config_update, + get_data_topic, +) from esp_sensors.oled_display import OLEDDisplay from esp_sensors.config import Config @@ -110,16 +115,13 @@ def main(): time_str = f"Time: {time.time():.0f}" # Print to console - print('='*20) + print("=" * 20) print(f"{temp_str}, {hum_str}") - print('='*20) - + print("=" * 20) # Display values ## TODO: only display values, if the button has been clicked - display.display_values( - [temp_str, hum_str, time_str] - ) + display.display_values([temp_str, hum_str, time_str]) display.set_status("") @@ -131,7 +133,9 @@ def main(): # Set up MQTT client if enabled display.set_status("Setting up MQTT...") - print(f"MQTT enabled: {mqtt_enabled}, broker: {config.mqtt_config.get('broker')}") + print( + f"MQTT enabled: {mqtt_enabled}, broker: {config.mqtt_config.get('broker')}" + ) mqtt_client = setup_mqtt(config.mqtt_config) if mqtt_client: @@ -149,24 +153,39 @@ def main(): if load_config_from_mqtt: display.set_status("Checking MQTT config...") print("Checking for configuration updates before publishing...") - updated_config = check_config_update(mqtt_client, config.mqtt_config, config.config) + updated_config = check_config_update( + mqtt_client, config.mqtt_config, config.config + ) # If we got an updated configuration with a newer version, save it - if updated_config != config.config and updated_config.get("version", 0) > config.current_version: + if ( + updated_config != config.config + and updated_config.get("version", 0) > config.current_version + ): display.set_status("Updating config...") - print(f"Found newer configuration (version {updated_config.get('version')}), updating...") + print( + f"Found newer configuration (version {updated_config.get('version')}), updating..." + ) config.save_config(updated_config) - mqtt_client.publish(get_data_topic(config.mqtt_config) + "/config_status", "Configuration updated") + mqtt_client.publish( + get_data_topic(config.mqtt_config) + "/config_status", + "Configuration updated", + ) # Note: We continue with the current config for this cycle # The updated config will be used after the next reboot else: - print(f"No configuration updates found or no newer version available (local version: {config.current_version})") - + print( + f"No configuration updates found or no newer version available (local version: {config.current_version})" + ) # Now publish sensor data using the same MQTT client display.set_status("Publishing to MQTT...") - print(f"Publishing sensor data to MQTT at {config.mqtt_config.get('broker')}:{config.mqtt_config.get('port')}") - publish_sensor_data(mqtt_client, config.mqtt_config, dht_sensor, temperature, humidity) + print( + f"Publishing sensor data to MQTT at {config.mqtt_config.get('broker')}:{config.mqtt_config.get('port')}" + ) + publish_sensor_data( + mqtt_client, config.mqtt_config, dht_sensor, temperature, humidity + ) print("Sensor data published to MQTT") # Disconnect MQTT client after both operations @@ -187,7 +206,7 @@ def main(): time_until_next_read = config.update_interval display.set_status(f"Sleeping {time_until_next_read}s") - print('sleeping for', time_until_next_read, 'seconds') + print("sleeping for", time_until_next_read, "seconds") deepsleep(time_until_next_read * 1000) except KeyboardInterrupt: @@ -210,6 +229,7 @@ def main(): def connect_wifi(network_config: dict, fallback_config: dict = None): import network + ssid = network_config.get("ssid") password = network_config.get("password") timeout = network_config.get("timeout", 10) @@ -236,7 +256,7 @@ def connect_wifi(network_config: dict, fallback_config: dict = None): time.sleep(1) - print('Connection successful') + print("Connection successful") print(station.ifconfig()) return True @@ -246,5 +266,5 @@ if __name__ == "__main__": main() except Exception as e: print(f"An error occurred: {e}") - time.sleep(5) # give time to read the error message and respond - deepsleep(1) # dummy deepsleep to basically reset the system + time.sleep(5) # give time to read the error message and respond + deepsleep(1) # dummy deepsleep to basically reset the system diff --git a/tests/test_mqtt_client.py b/tests/test_mqtt_client.py index b14491a..b0b51e4 100644 --- a/tests/test_mqtt_client.py +++ b/tests/test_mqtt_client.py @@ -9,10 +9,18 @@ import socket import struct from unittest.mock import patch, MagicMock, call from src.esp_sensors.mqtt_client import ( - MQTTClient, MQTTException, - CONNECT, CONNACK, PUBLISH, PUBACK, SUBSCRIBE, SUBACK, - PINGREQ, PINGRESP, DISCONNECT, - CONN_ACCEPTED + MQTTClient, + MQTTException, + CONNECT, + CONNACK, + PUBLISH, + PUBACK, + SUBSCRIBE, + SUBACK, + PINGREQ, + PINGRESP, + DISCONNECT, + CONN_ACCEPTED, ) @@ -29,7 +37,7 @@ class TestMQTTClient: user="test_user", password="test_pass", keepalive=60, - ssl=False + ssl=False, ) def test_init(self, mqtt_client): @@ -82,16 +90,16 @@ class TestMQTTClient: # Test with string input result = mqtt_client._encode_string("test") assert len(result) == 6 # 2 bytes length + 4 bytes string - assert result[0:2] == b'\x00\x04' # Length (4) in network byte order - assert result[2:] == b'test' # String content + assert result[0:2] == b"\x00\x04" # Length (4) in network byte order + assert result[2:] == b"test" # String content # Test with bytes input result = mqtt_client._encode_string(b"test") assert len(result) == 6 - assert result[0:2] == b'\x00\x04' - assert result[2:] == b'test' + assert result[0:2] == b"\x00\x04" + assert result[2:] == b"test" - @patch('socket.socket') + @patch("socket.socket") def test_connect_success(self, mock_socket, mqtt_client): """Test successful connection to MQTT broker.""" # Configure the mock socket @@ -99,7 +107,9 @@ class TestMQTTClient: mock_socket.return_value = mock_sock # Mock the _recv_packet method to return a successful CONNACK - with patch.object(mqtt_client, '_recv_packet', return_value=(CONNACK, b'\x00\x00')): + with patch.object( + mqtt_client, "_recv_packet", return_value=(CONNACK, b"\x00\x00") + ): # Call connect result = mqtt_client.connect() @@ -115,7 +125,7 @@ class TestMQTTClient: assert mqtt_client.connected is True assert mqtt_client.sock is mock_sock - @patch('socket.socket') + @patch("socket.socket") def test_connect_failure(self, mock_socket, mqtt_client): """Test connection failure to MQTT broker.""" # Configure the mock socket @@ -123,12 +133,14 @@ class TestMQTTClient: mock_socket.return_value = mock_sock # Mock the _recv_packet method to return a failed CONNACK - with patch.object(mqtt_client, '_recv_packet', return_value=(CONNACK, b'\x00\x01')): + with patch.object( + mqtt_client, "_recv_packet", return_value=(CONNACK, b"\x00\x01") + ): # Call connect and verify it raises an exception with pytest.raises(MQTTException, match="Connection refused: 1"): mqtt_client.connect() - @patch('socket.socket') + @patch("socket.socket") def test_disconnect(self, mock_socket, mqtt_client): """Test disconnection from MQTT broker.""" # Configure the mock socket @@ -152,7 +164,7 @@ class TestMQTTClient: assert mqtt_client.connected is False assert mqtt_client.sock is None - @patch('socket.socket') + @patch("socket.socket") def test_publish(self, mock_socket, mqtt_client): """Test publishing a message to a topic.""" # Configure the mock socket @@ -174,13 +186,15 @@ class TestMQTTClient: mock_sock.reset_mock() # Mock the _recv_packet method instead of directly mocking socket.recv - with patch.object(mqtt_client, '_recv_packet', return_value=(PUBACK, b'\x00\x01')): + with patch.object( + mqtt_client, "_recv_packet", return_value=(PUBACK, b"\x00\x01") + ): mqtt_client.publish("test/topic", "test message", qos=1) # Verify PUBLISH packet was sent assert mock_sock.send.call_count == 1 - @patch('socket.socket') + @patch("socket.socket") def test_subscribe(self, mock_socket, mqtt_client): """Test subscribing to a topic.""" # Configure the mock socket @@ -193,7 +207,9 @@ class TestMQTTClient: mqtt_client.last_ping = 0 # Mock the _recv_packet method to return a successful SUBACK - with patch.object(mqtt_client, '_recv_packet', return_value=(SUBACK, b'\x00\x01\x00')): + with patch.object( + mqtt_client, "_recv_packet", return_value=(SUBACK, b"\x00\x01\x00") + ): # Call subscribe mqtt_client.subscribe("test/topic") @@ -204,7 +220,7 @@ class TestMQTTClient: assert "test/topic" in mqtt_client.subscriptions assert mqtt_client.subscriptions["test/topic"] == 0 - @patch('socket.socket') + @patch("socket.socket") def test_check_msg(self, mock_socket, mqtt_client): """Test checking for messages.""" # Configure the mock socket @@ -227,7 +243,7 @@ class TestMQTTClient: payload = topic_encoded + message.encode() # Mock the _recv_packet method to return a PUBLISH packet - with patch.object(mqtt_client, '_recv_packet', return_value=(PUBLISH, payload)): + with patch.object(mqtt_client, "_recv_packet", return_value=(PUBLISH, payload)): # Call check_msg mqtt_client.check_msg()