diff --git a/snmp2mqtt.py b/snmp2mqtt.py index c1d5e9d..ed36c9d 100755 --- a/snmp2mqtt.py +++ b/snmp2mqtt.py @@ -51,40 +51,56 @@ class DeviceMonitorThread(threading.Thread): logging.info(f"Starting monitoring thread for device: {self.device_name} ({self.device_config['ip']})") try: - # Setup MQTT connection and Home Assistant config - ha_config = ha_create_config(self.req) + # Setup MQTT connection client = connect_mqtt(self.mqtt_config) client.loop_start() - config_topic = f"homeassistant/device/{ha_config['dev']['ids']}/config" - state_topic = ha_config['state_topic'] + state_topic = f"SNMP/{self.device_name}/state" + availability_topic = f"SNMP/{self.device_name}/availability" - logging.info(f"[{self.device_name}] MQTT client connected, starting monitoring loop") + logging.info(f"[{self.device_name}] MQTT client connected") + # Publish Home Assistant autodiscovery configuration (only once on startup) + publish_ha_autodiscovery_config(client, self.req) + + # Mark device as available + publish(availability_topic, client, "online", True, 1) + + logging.info(f"[{self.device_name}] Starting monitoring loop") + + # Main monitoring loop while not shutdown_event.is_set(): try: - # Publish Home Assistant configuration - publish(config_topic, client, ha_config, True, 0) - logging.debug(f"[{self.device_name}] Published config to {config_topic}") - # Get SNMP data and publish state state = asyncio.run(get_snmp(self.req)) publish(state_topic, client, state, False, 0) logging.debug(f"[{self.device_name}] Published state to {state_topic}: {json.dumps(state)}") + # Update availability (heartbeat) + publish(availability_topic, client, "online", False, 1) + except Exception as e: logging.error(f"[{self.device_name}] Error in monitoring loop: {e}") + # Mark as offline on error + publish(availability_topic, client, "offline", False, 1) # Wait for next iteration or shutdown signal shutdown_event.wait(timeout=self.sleep_interval) - # Cleanup + # Cleanup - mark device as offline + publish(availability_topic, client, "offline", True, 1) client.loop_stop() client.disconnect() logging.info(f"[{self.device_name}] Monitoring thread stopped gracefully") except Exception as e: logging.error(f"[{self.device_name}] Fatal error in monitoring thread: {e}") + # Try to mark as offline on fatal error + try: + publish(availability_topic, client, "offline", True, 1) + client.disconnect() + except: + pass logging.info(f"[{self.device_name}] Thread {self.name} finished") @@ -174,7 +190,11 @@ def connect_mqtt(mqtt_config): def publish(topic, client, data, retain, qos): - msg = json.dumps(data) + if isinstance(data, str): + msg = data + else: + msg = json.dumps(data) + result = client.publish(topic=topic, payload=msg, qos=qos, retain=bool(retain)) status = result[0] if status == 0: @@ -217,38 +237,93 @@ async def get_snmp(req): return data -def ha_create_config(req): - ha_config = {} - device = { - "ids": f"{req['device_name']}_{req['ip']}".replace(".", "_"), - "name": req['device_name'], - } - origin = { - "name": "snmp2mqtt" - } - ha_config.update({"dev": device, "o": origin}) - ha_config.update({"state_topic": f"SNMP/{req['device_name']}/state"}) - ha_config.update({"qos": 2}) - cmps = {} +def create_ha_device_info(req): + """Create device information for Home Assistant MQTT Discovery""" + return { + "identifiers": [f"snmp2mqtt_{req['device_name']}_{req['ip']}".replace(".", "_")], + "name": req['device_name'], + "model": "SNMP Device", + "manufacturer": "Network Equipment", + "via_device": "snmp2mqtt" + } + + +def create_ha_sensor_config(req, oid): + """Create Home Assistant MQTT Discovery configuration for a single sensor""" + device_info = create_ha_device_info(req) + sensor_id = f"{req['device_name']}_{req['ip']}_{oid['name']}".replace(".", "_") + + config = { + "name": f"{req['device_name']} {oid['name']}", + "unique_id": sensor_id, + "state_topic": f"SNMP/{req['device_name']}/state", + "value_template": f"{{{{ value_json.{oid['name']} }}}}", + "device": device_info, + "origin": { + "name": "snmp2mqtt", + "sw_version": "1.0.0", + "support_url": "https://git.antoineve.me/AntoineVe/snmp2mqtt" + } + } + + # Add device class if specified + if 'HA_device_class' in oid: + config['device_class'] = oid['HA_device_class'] + + # Add unit of measurement if specified + if 'HA_unit' in oid: + config['unit_of_measurement'] = oid['HA_unit'] + + # Add icon based on device class + icon_mapping = { + 'data_size': 'mdi:network', + 'connectivity': 'mdi:network-outline', + 'power_factor': 'mdi:gauge', + 'temperature': 'mdi:thermometer', + 'signal_strength': 'mdi:signal' + } + if 'HA_device_class' in oid and oid['HA_device_class'] in icon_mapping: + config['icon'] = icon_mapping[oid['HA_device_class']] + + # Add availability topic + config['availability'] = { + "topic": f"SNMP/{req['device_name']}/availability", + "payload_available": "online", + "payload_not_available": "offline" + } + + return config + + +def get_ha_discovery_topic(req, oid): + """Get the correct Home Assistant MQTT Discovery topic for a sensor""" + platform = oid['HA_platform'] # 'sensor' or 'binary_sensor' + node_id = req['device_name'] + object_id = f"{req['device_name']}_{oid['name']}".replace(".", "_") + + # Format: homeassistant////config + return f"homeassistant/{platform}/{node_id}/{object_id}/config" + + +def publish_ha_autodiscovery_config(client, req): + """Publish Home Assistant MQTT Discovery configuration for all sensors of a device""" + logging.info(f"[{req['device_name']}] Publishing Home Assistant autodiscovery configuration") + + # Publish availability as online + availability_topic = f"SNMP/{req['device_name']}/availability" + publish(availability_topic, client, "online", True, 1) + + # Publish discovery configuration for each OID/sensor for oid in req['oids']: - cmps.update( - { - f"{req['device_name']}_{req['ip']}_{oid['name']}".replace(".", "_"): - { - "p": oid['HA_platform'], - "device_class": oid['HA_device_class'], - "value_template": f"{{{{ value_json.{oid['name']}}}}}", - "unique_id": f"{req['device_name']}_{req['ip']}_{oid['name']}".replace(".", "_"), - "name": oid['name'] - } - }) - if "HA_unit" in oid.keys(): - cmps.update( - {f"{req['device_name']}_{req['ip']}_{oid['name']}".replace(".", "_"): - {"unit_of_measurement": oid['HA_unit']}}) - ha_config.update({"cmps": cmps}) - logging.debug(f"config : {json.dumps(ha_config)}") - return ha_config + config = create_ha_sensor_config(req, oid) + topic = get_ha_discovery_topic(req, oid) + + # Publish with retain=True so HA discovers it after restarts + publish(topic, client, config, True, 1) + logging.info(f"[{req['device_name']}] Published discovery config for {oid['name']} to {topic}") + + # Small delay to avoid overwhelming the broker + time.sleep(0.1) def signal_handler(signum, frame):