Initial commit
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,596 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
MQTT/Home Assistant Discovery Integration
|
||||
Erweitert das Tahoma Script um MQTT-Geräte via Home Assistant Discovery
|
||||
"""
|
||||
|
||||
import paho.mqtt.client as mqtt
|
||||
import json
|
||||
import time
|
||||
from typing import Dict, List, Optional
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class HomeAssistantDiscovery:
|
||||
"""Klasse für Home Assistant MQTT Discovery"""
|
||||
|
||||
# Bekannte Discovery-Komponenten
|
||||
COMPONENTS = [
|
||||
'binary_sensor', 'sensor', 'switch', 'light', 'cover',
|
||||
'climate', 'fan', 'lock', 'camera', 'vacuum', 'alarm_control_panel',
|
||||
'device_tracker', 'number', 'select', 'button', 'text'
|
||||
]
|
||||
|
||||
def __init__(self, broker: str, port: int = 1883, username: str = None,
|
||||
password: str = None, discovery_prefix: str = 'homeassistant'):
|
||||
"""
|
||||
Initialisiert Home Assistant Discovery
|
||||
|
||||
Args:
|
||||
broker: MQTT Broker IP/Hostname
|
||||
port: MQTT Port (Standard: 1883)
|
||||
username: MQTT Benutzername (optional)
|
||||
password: MQTT Passwort (optional)
|
||||
discovery_prefix: Discovery Prefix (Standard: 'homeassistant')
|
||||
"""
|
||||
self.broker = broker
|
||||
self.port = port
|
||||
self.username = username
|
||||
self.password = password
|
||||
self.discovery_prefix = discovery_prefix
|
||||
self.client = None
|
||||
self.discovered_devices = {}
|
||||
|
||||
def connect(self) -> bool:
|
||||
"""
|
||||
Verbindet mit dem MQTT Broker
|
||||
|
||||
Returns:
|
||||
True bei Erfolg, False bei Fehler
|
||||
"""
|
||||
try:
|
||||
self.client = mqtt.Client()
|
||||
|
||||
if self.username and self.password:
|
||||
self.client.username_pw_set(self.username, self.password)
|
||||
|
||||
self.client.on_connect = self._on_connect
|
||||
self.client.on_message = self._on_message
|
||||
|
||||
self.client.connect(self.broker, self.port, 60)
|
||||
logger.info(f"Verbunden mit MQTT Broker {self.broker}:{self.port}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"MQTT Verbindungsfehler: {e}")
|
||||
return False
|
||||
|
||||
def _on_connect(self, client, userdata, flags, rc):
|
||||
"""Callback wenn Verbindung hergestellt wurde"""
|
||||
if rc == 0:
|
||||
logger.info("MQTT Verbindung erfolgreich")
|
||||
# Alle Discovery Topics abonnieren mit Wildcard für object_id
|
||||
for component in self.COMPONENTS:
|
||||
# Unterstützt beide Topic-Formate:
|
||||
# homeassistant/component/node_id/config (4 Teile)
|
||||
# homeassistant/component/node_id/object_id/config (5 Teile)
|
||||
topic = f"{self.discovery_prefix}/{component}/+/+/config"
|
||||
client.subscribe(topic)
|
||||
logger.debug(f"Abonniert: {topic}")
|
||||
# Zusätzlich auch das kürzere Format abonnieren
|
||||
topic_short = f"{self.discovery_prefix}/{component}/+/config"
|
||||
client.subscribe(topic_short)
|
||||
logger.debug(f"Abonniert: {topic_short}")
|
||||
else:
|
||||
logger.error(f"MQTT Verbindung fehlgeschlagen, Code: {rc}")
|
||||
|
||||
def _on_message(self, client, userdata, msg):
|
||||
"""Callback wenn Nachricht empfangen wurde"""
|
||||
try:
|
||||
# Topic analysieren - unterstützt beide Formate:
|
||||
# homeassistant/component/node_id/config (4 Teile)
|
||||
# homeassistant/component/node_id/object_id/config (5 Teile)
|
||||
topic_parts = msg.topic.split('/')
|
||||
|
||||
if topic_parts[-1] != 'config':
|
||||
return # Kein Config-Topic
|
||||
|
||||
if len(topic_parts) == 4:
|
||||
# Format: homeassistant/component/node_id/config
|
||||
component = topic_parts[1]
|
||||
node_id = topic_parts[2]
|
||||
object_id = None
|
||||
elif len(topic_parts) == 5:
|
||||
# Format: homeassistant/component/node_id/object_id/config
|
||||
component = topic_parts[1]
|
||||
node_id = topic_parts[2]
|
||||
object_id = topic_parts[3]
|
||||
else:
|
||||
logger.debug(f"Unbekanntes Topic-Format: {msg.topic}")
|
||||
return
|
||||
|
||||
# Payload parsen
|
||||
if msg.payload:
|
||||
config = json.loads(msg.payload.decode('utf-8'))
|
||||
|
||||
# Eindeutigen Key erstellen
|
||||
if object_id:
|
||||
device_key = f"{component}_{node_id}_{object_id}"
|
||||
else:
|
||||
device_key = f"{component}_{node_id}"
|
||||
|
||||
# Gerät speichern
|
||||
self.discovered_devices[device_key] = {
|
||||
'component': component,
|
||||
'node_id': node_id,
|
||||
'object_id': object_id,
|
||||
'config': config,
|
||||
'topic': msg.topic
|
||||
}
|
||||
|
||||
device_name = config.get('name', config.get('unique_id', object_id or node_id))
|
||||
logger.debug(f"Gerät gefunden: {device_name} ({component}) - {msg.topic}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Verarbeiten der MQTT-Nachricht von {msg.topic}: {e}")
|
||||
|
||||
def discover_devices(self, timeout: int = 10) -> Dict:
|
||||
"""
|
||||
Sucht nach Home Assistant Discovery Geräten
|
||||
|
||||
Args:
|
||||
timeout: Timeout in Sekunden
|
||||
|
||||
Returns:
|
||||
Dictionary mit gefundenen Geräten
|
||||
"""
|
||||
logger.info(f"Starte Home Assistant Discovery (Timeout: {timeout}s)...")
|
||||
logger.info(f"Lausche auf {self.discovery_prefix}/+/+/+/config und {self.discovery_prefix}/+/+/config")
|
||||
|
||||
self.discovered_devices = {}
|
||||
|
||||
# MQTT Loop starten
|
||||
self.client.loop_start()
|
||||
|
||||
# Warten auf Nachrichten - mit Fortschrittsanzeige
|
||||
for i in range(timeout):
|
||||
time.sleep(1)
|
||||
if (i + 1) % 5 == 0 or i == timeout - 1:
|
||||
logger.info(f"Discovery läuft... {len(self.discovered_devices)} Geräte gefunden ({i+1}/{timeout}s)")
|
||||
|
||||
# Loop stoppen
|
||||
self.client.loop_stop()
|
||||
|
||||
logger.info(f"✓ {len(self.discovered_devices)} MQTT-Geräte gefunden")
|
||||
|
||||
# Debug: Zeige einige gefundene Topics
|
||||
if self.discovered_devices:
|
||||
logger.debug("Gefundene Geräte (Auswahl):")
|
||||
for i, (key, device) in enumerate(list(self.discovered_devices.items())[:5]):
|
||||
logger.debug(f" - {device['config'].get('name', key)} ({device['component']}) via {device['topic']}")
|
||||
if len(self.discovered_devices) > 5:
|
||||
logger.debug(f" ... und {len(self.discovered_devices) - 5} weitere")
|
||||
|
||||
return self.discovered_devices
|
||||
|
||||
def disconnect(self):
|
||||
"""Trennt die MQTT-Verbindung"""
|
||||
if self.client:
|
||||
self.client.disconnect()
|
||||
logger.info("MQTT-Verbindung getrennt")
|
||||
|
||||
|
||||
class MQTTDeviceConverter:
|
||||
"""Konvertiert MQTT Discovery Entities in Datenbank-Format, gruppiert nach Gerät"""
|
||||
|
||||
# Mapping von HA Komponenten zu Actor/Sensor
|
||||
ACTOR_COMPONENTS = ['switch', 'light', 'cover', 'fan', 'lock', 'climate',
|
||||
'vacuum', 'alarm_control_panel', ' ', 'number', 'select']
|
||||
SENSOR_COMPONENTS = ['binary_sensor', 'sensor', 'device_tracker']
|
||||
|
||||
@staticmethod
|
||||
def group_entities_by_device(discovered_devices: Dict) -> Dict[str, List]:
|
||||
"""
|
||||
Gruppiert Discovery-Entities nach Gerät (node_id)
|
||||
|
||||
Args:
|
||||
discovered_devices: Dictionary mit allen gefundenen Entities
|
||||
|
||||
Returns:
|
||||
Dictionary: {device_id: [entity1, entity2, ...]}
|
||||
"""
|
||||
devices = {}
|
||||
|
||||
for entity_key, entity in discovered_devices.items():
|
||||
# Device Identifier aus Config extrahieren
|
||||
config = entity.get('config', {})
|
||||
device_info = config.get('device') or config.get('dev') or {}
|
||||
# Node ID als Geräte-Identifier verwenden
|
||||
node_id = entity.get('node_id', 'unknown')
|
||||
|
||||
# Zusätzlich Device Identifiers prüfen falls vorhanden
|
||||
if device_info and 'identifiers' in device_info:
|
||||
identifiers = device_info['identifiers']
|
||||
if isinstance(identifiers, list) and identifiers:
|
||||
node_id = identifiers[0]
|
||||
|
||||
if node_id not in devices:
|
||||
devices[node_id] = {
|
||||
'entities': [],
|
||||
'device_info': device_info,
|
||||
'node_id': node_id
|
||||
}
|
||||
|
||||
devices[node_id]['entities'].append(entity)
|
||||
|
||||
return devices
|
||||
|
||||
@staticmethod
|
||||
def is_actor_entity(component: str) -> bool:
|
||||
"""Prüft ob Entity-Komponente ein Aktor ist"""
|
||||
if(component == "climate"):
|
||||
logger.info("Climate device gefunden");
|
||||
return component in MQTTDeviceConverter.ACTOR_COMPONENTS
|
||||
|
||||
@staticmethod
|
||||
def is_sensor_entity(component: str) -> bool:
|
||||
"""Prüft ob Entity-Komponente ein Sensor ist"""
|
||||
return component in MQTTDeviceConverter.SENSOR_COMPONENTS
|
||||
|
||||
@staticmethod
|
||||
def convert_device_to_actors_and_sensors(device_id: str, device_data: Dict) -> tuple:
|
||||
"""
|
||||
Konvertiert ein Gerät mit allen seinen Entities in Actor/Sensor-Format
|
||||
|
||||
Args:
|
||||
device_id: Geräte-ID (node_id)
|
||||
device_data: Device-Daten mit Entities-Liste
|
||||
|
||||
Returns:
|
||||
Tuple (actor_dict or None, sensor_dict or None)
|
||||
"""
|
||||
entities = device_data['entities']
|
||||
device_info = device_data.get('device_info')
|
||||
# Gerätename vom ersten Entity oder aus device_info
|
||||
device_name = device_info.get('name', device_id)
|
||||
if not device_name or device_name == device_id:
|
||||
# Fallback: Name vom ersten Entity
|
||||
if entities:
|
||||
device_name = entities[0]['config'].get('name', device_id)
|
||||
|
||||
# Device URL
|
||||
device_url = f"mqtt://{device_id}"
|
||||
|
||||
# Entities nach Actor/Sensor trennen
|
||||
actor_entities = [e for e in entities if MQTTDeviceConverter.is_actor_entity(e['component'])]
|
||||
sensor_entities = [e for e in entities if MQTTDeviceConverter.is_sensor_entity(e['component'])]
|
||||
|
||||
actor_result = None
|
||||
sensor_result = None
|
||||
|
||||
# Actor erstellen falls Actor-Entities vorhanden
|
||||
if actor_entities:
|
||||
commands = []
|
||||
states = []
|
||||
|
||||
for entity in actor_entities:
|
||||
component = entity['component']
|
||||
object_id = entity.get('object_id', 'unknown')
|
||||
config = entity['config']
|
||||
|
||||
# Command aus der Entity erstellen
|
||||
command_entry = MQTTDeviceConverter._entity_to_command(component, object_id, config)
|
||||
if command_entry:
|
||||
commands.append(command_entry)
|
||||
|
||||
# States aus der Entity extrahieren
|
||||
entity_states = MQTTDeviceConverter._entity_to_states(component, object_id, config)
|
||||
states.extend(entity_states)
|
||||
|
||||
if commands: # Nur Actor erstellen wenn Commands vorhanden
|
||||
actor_result = {
|
||||
'name': device_name,
|
||||
'type': f"mqtt_device", # Allgemeiner Typ für Multi-Entity-Geräte
|
||||
'url': device_url,
|
||||
'commands': commands,
|
||||
'states': states
|
||||
}
|
||||
|
||||
# Sensor erstellen falls Sensor-Entities vorhanden
|
||||
if sensor_entities:
|
||||
states = []
|
||||
|
||||
for entity in sensor_entities:
|
||||
component = entity['component']
|
||||
object_id = entity.get('object_id', 'unknown')
|
||||
config = entity['config']
|
||||
|
||||
# States aus der Entity extrahieren
|
||||
entity_states = MQTTDeviceConverter._entity_to_states(component, object_id, config)
|
||||
states.extend(entity_states)
|
||||
|
||||
if states: # Nur Sensor erstellen wenn States vorhanden
|
||||
sensor_result = {
|
||||
'name': device_name,
|
||||
'type': f"mqtt_device",
|
||||
'url': device_url,
|
||||
'states': states
|
||||
}
|
||||
|
||||
return actor_result, sensor_result
|
||||
|
||||
@staticmethod
|
||||
def _entity_to_command(component: str, object_id: str, config: Dict) -> Optional[Dict]:
|
||||
"""
|
||||
Konvertiert eine MQTT Entity in ein Command
|
||||
|
||||
Args:
|
||||
component: Entity-Typ (number, button, switch, etc.)
|
||||
object_id: Object ID (z.B. set_max_ampere_limit)
|
||||
config: Entity-Konfiguration
|
||||
|
||||
Returns:
|
||||
Command-Dictionary oder None
|
||||
"""
|
||||
# Command Topic - verschiedene mögliche Feldnamen
|
||||
command_topic = config.get('command_topic') or config.get('cmd_t') or config.get('temperature_command_topic')
|
||||
|
||||
if not command_topic:
|
||||
return None
|
||||
|
||||
# Command-Name aus object_id ableiten
|
||||
command_name = object_id.replace('_', ' ').title().replace(' ', '')
|
||||
# Oder aus dem Namen
|
||||
entity_name = config.get('name', object_id)
|
||||
|
||||
command_entry = {
|
||||
'command': command_name,
|
||||
'parameters': []
|
||||
}
|
||||
|
||||
# Parameter basierend auf Component-Typ
|
||||
if component == 'number':
|
||||
# Number hat einen Wert-Parameter
|
||||
param = {
|
||||
'name': 'value',
|
||||
'type': 'number',
|
||||
'url': command_topic
|
||||
}
|
||||
|
||||
# Min/Max aus Config
|
||||
if 'min' in config:
|
||||
param['min'] = config['min']
|
||||
if 'max' in config:
|
||||
param['max'] = config['max']
|
||||
|
||||
# Unit hinzufügen - verschiedene mögliche Feldnamen
|
||||
unit = (
|
||||
config.get('unit_of_measurement') or
|
||||
config.get('unit_of_meas') or
|
||||
config.get('unit') or
|
||||
config.get('u')
|
||||
)
|
||||
if unit:
|
||||
param['unit'] = unit
|
||||
|
||||
command_entry['parameters'].append(param)
|
||||
|
||||
elif component == 'select':
|
||||
# Select hat Optionen
|
||||
param = {
|
||||
'name': 'option',
|
||||
'type': 'string',
|
||||
'url': command_topic
|
||||
}
|
||||
|
||||
options = config.get('options') or config.get('ops')
|
||||
if options:
|
||||
param['values'] = options
|
||||
|
||||
command_entry['parameters'].append(param)
|
||||
|
||||
elif component in ['switch', 'light']:
|
||||
# Switch/Light haben on/off
|
||||
param = {
|
||||
'name': 'state',
|
||||
'type': 'string',
|
||||
'url': command_topic,
|
||||
'values': [
|
||||
config.get('payload_on', config.get('pl_on', 'ON')),
|
||||
config.get('payload_off', config.get('pl_off', 'OFF'))
|
||||
]
|
||||
}
|
||||
command_entry['parameters'].append(param)
|
||||
|
||||
# Brightness für Light
|
||||
brightness_cmd_topic = (
|
||||
config.get('brightness_command_topic') or
|
||||
config.get('bri_cmd_t')
|
||||
)
|
||||
if component == 'light' and brightness_cmd_topic:
|
||||
command_entry['parameters'].append({
|
||||
'name': 'brightness',
|
||||
'type': 'integer',
|
||||
'min': 0,
|
||||
'max': 255,
|
||||
'url': brightness_cmd_topic
|
||||
})
|
||||
|
||||
elif component == 'cover':
|
||||
# Cover hat position
|
||||
set_pos_topic = (
|
||||
config.get('set_position_topic') or
|
||||
config.get('pos_cmd_t')
|
||||
)
|
||||
if set_pos_topic:
|
||||
param = {
|
||||
'name': 'position',
|
||||
'type': 'integer',
|
||||
'min': 0,
|
||||
'max': 100,
|
||||
'url': set_pos_topic
|
||||
}
|
||||
command_entry['parameters'].append(param)
|
||||
else:
|
||||
# Nur open/close/stop
|
||||
param = {
|
||||
'name': 'action',
|
||||
'type': 'string',
|
||||
'url': command_topic,
|
||||
'values': ['OPEN', 'CLOSE', 'STOP']
|
||||
}
|
||||
command_entry['parameters'].append(param)
|
||||
|
||||
elif component == 'button':
|
||||
# Button hat normalerweise keinen Parameter, nur das Topic
|
||||
param = {
|
||||
'name': 'press',
|
||||
'type': 'trigger',
|
||||
'url': command_topic
|
||||
}
|
||||
command_entry['parameters'].append(param)
|
||||
|
||||
elif component == 'climate':
|
||||
# Climate hat Temperatur-Setpoint
|
||||
temp_cmd_topic = (
|
||||
config.get('temperature_command_topic') or
|
||||
config.get('temp_cmd_t')
|
||||
)
|
||||
if temp_cmd_topic:
|
||||
param = {
|
||||
'name': 'temperature',
|
||||
'type': 'number',
|
||||
'url': temp_cmd_topic
|
||||
}
|
||||
|
||||
if 'min_temp' in config:
|
||||
param['min'] = config['min_temp']
|
||||
if 'max_temp' in config:
|
||||
param['max'] = config['max_temp']
|
||||
|
||||
command_entry['parameters'].append(param)
|
||||
mode_cmd_topic = (
|
||||
config.get('mode_command_topic') or
|
||||
config.get('mode_cmd_t')
|
||||
)
|
||||
if mode_cmd_topic:
|
||||
param = {
|
||||
'name': 'mode',
|
||||
'type': 'string',
|
||||
'url': temp_cmd_topic,
|
||||
'values': config.get('modes', [])
|
||||
}
|
||||
else:
|
||||
# Generischer Command mit dem Topic
|
||||
param = {
|
||||
'name': 'value',
|
||||
'type': 'string',
|
||||
'url': command_topic
|
||||
}
|
||||
command_entry['parameters'].append(param)
|
||||
|
||||
return command_entry
|
||||
|
||||
@staticmethod
|
||||
def _entity_to_states(component: str, object_id: str, config: Dict) -> List[Dict]:
|
||||
"""
|
||||
Extrahiert States aus einer MQTT Entity
|
||||
|
||||
Args:
|
||||
component: Entity-Typ
|
||||
object_id: Object ID
|
||||
config: Entity-Konfiguration
|
||||
|
||||
Returns:
|
||||
Liste von State-Dictionaries
|
||||
"""
|
||||
states = []
|
||||
|
||||
# State Topic - verschiedene mögliche Feldnamen prüfen
|
||||
state_topic = (
|
||||
config.get('state_topic') or
|
||||
config.get('stat_t') or # Abkürzung
|
||||
config.get('~') and config.get('stat_t') # Mit Base Topic
|
||||
)
|
||||
|
||||
# Bei number/select: oft kein separates state_topic, dann command_topic verwenden
|
||||
if not state_topic and component in ['number', 'select', 'button']:
|
||||
# Bei diesen Komponenten kann der State über command_topic abgefragt werden
|
||||
# oder es gibt ein explizites state_topic
|
||||
state_topic = config.get('command_topic') or config.get('cmd_t')
|
||||
|
||||
if state_topic:
|
||||
state_entry = {
|
||||
'name': object_id,
|
||||
'type': 'string',
|
||||
'url': state_topic
|
||||
}
|
||||
|
||||
# Unit hinzufügen - verschiedene mögliche Feldnamen
|
||||
unit = (
|
||||
config.get('unit_of_measurement') or
|
||||
config.get('unit_of_meas') or
|
||||
config.get('unit') or
|
||||
config.get('u') # Weitere Abkürzung
|
||||
)
|
||||
if unit:
|
||||
state_entry['unit'] = unit
|
||||
|
||||
# Device Class als zusätzliche Info
|
||||
if 'device_class' in config:
|
||||
state_entry['device_class'] = config['device_class']
|
||||
elif 'dev_cla' in config:
|
||||
state_entry['device_class'] = config['dev_cla']
|
||||
|
||||
# Typ anpassen basierend auf Component
|
||||
if component == 'number':
|
||||
state_entry['type'] = 'number'
|
||||
elif component == 'binary_sensor':
|
||||
state_entry['type'] = 'boolean'
|
||||
elif component == 'sensor':
|
||||
# Bei Sensor den Typ aus value_template ableiten oder number annehmen
|
||||
state_entry['type'] = 'number' # Default für Sensoren
|
||||
|
||||
states.append(state_entry)
|
||||
|
||||
# Zusätzliche State Topics (z.B. brightness bei Light)
|
||||
if component == 'light':
|
||||
brightness_topic = (
|
||||
config.get('brightness_state_topic') or
|
||||
config.get('bri_stat_t')
|
||||
)
|
||||
if brightness_topic:
|
||||
states.append({
|
||||
'name': f"{object_id}_brightness",
|
||||
'type': 'integer',
|
||||
'url': brightness_topic
|
||||
})
|
||||
|
||||
if component == 'cover':
|
||||
position_topic = (
|
||||
config.get('position_topic') or
|
||||
config.get('pos_t')
|
||||
)
|
||||
if position_topic:
|
||||
states.append({
|
||||
'name': f"{object_id}_position",
|
||||
'type': 'integer',
|
||||
'url': position_topic
|
||||
})
|
||||
|
||||
if component == 'climate':
|
||||
current_temp_topic = (
|
||||
config.get('current_temperature_topic') or
|
||||
config.get('curr_temp_t')
|
||||
)
|
||||
if current_temp_topic:
|
||||
states.append({
|
||||
'name': f"{object_id}_current_temp",
|
||||
'type': 'number',
|
||||
'unit': '°C',
|
||||
'url': current_temp_topic
|
||||
})
|
||||
|
||||
return states
|
||||
@@ -0,0 +1,756 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Shelly Device Discovery Script
|
||||
Findet alle Shelly-Geräte im lokalen Netzwerk und speichert Sensoren und Aktoren
|
||||
in der Datenbank gemäß dem EnergyFlow Schema.
|
||||
"""
|
||||
|
||||
import json
|
||||
import requests
|
||||
import socket
|
||||
import mysql.connector
|
||||
from mysql.connector import Error
|
||||
from typing import List, Dict, Optional
|
||||
import argparse
|
||||
import logging
|
||||
from datetime import datetime
|
||||
|
||||
# Logging konfigurieren
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(levelname)s - %(message)s'
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ShellyDiscovery:
|
||||
"""Klasse zum Entdecken von Shelly-Geräten im Netzwerk"""
|
||||
|
||||
SHELLY_MDNS_SERVICE = "_http._tcp.local."
|
||||
COMMON_PORTS = [80]
|
||||
|
||||
def __init__(self, network_range: str = "192.168.1"):
|
||||
self.network_range = network_range
|
||||
self.devices = []
|
||||
|
||||
def scan_network(self, start_ip: int = 1, end_ip: int = 254, timeout: float = 0.5) -> List[str]:
|
||||
"""
|
||||
Scannt das Netzwerk nach aktiven Hosts
|
||||
|
||||
Args:
|
||||
start_ip: Start IP (letztes Oktett)
|
||||
end_ip: End IP (letztes Oktett)
|
||||
timeout: Timeout für Socket-Verbindung
|
||||
|
||||
Returns:
|
||||
Liste von erreichbaren IP-Adressen
|
||||
"""
|
||||
active_hosts = []
|
||||
logger.info(f"Scanne Netzwerk {self.network_range}.{start_ip}-{end_ip}...")
|
||||
|
||||
for i in range(start_ip, end_ip + 1):
|
||||
ip = f"{self.network_range}.{i}"
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
sock.settimeout(timeout)
|
||||
|
||||
try:
|
||||
result = sock.connect_ex((ip, 80))
|
||||
if result == 0:
|
||||
active_hosts.append(ip)
|
||||
logger.debug(f"Host gefunden: {ip}")
|
||||
except:
|
||||
pass
|
||||
finally:
|
||||
sock.close()
|
||||
|
||||
logger.info(f"{len(active_hosts)} aktive Hosts gefunden")
|
||||
return active_hosts
|
||||
|
||||
def is_shelly_device(self, ip: str) -> Optional[Dict]:
|
||||
"""
|
||||
Prüft ob ein Host ein Shelly-Gerät ist
|
||||
|
||||
Args:
|
||||
ip: IP-Adresse des Hosts
|
||||
|
||||
Returns:
|
||||
Device Info Dict wenn Shelly, sonst None
|
||||
"""
|
||||
try:
|
||||
# Versuche Gen2 API (neuere Shelly-Geräte)
|
||||
response = requests.get(
|
||||
f"http://{ip}/rpc/Shelly.GetDeviceInfo",
|
||||
timeout=2
|
||||
)
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
logger.info(f"Shelly Gen2 Gerät gefunden: {ip} - {data.get('name', 'Unknown')}")
|
||||
return {
|
||||
'ip': ip,
|
||||
'generation': 2,
|
||||
'info': data
|
||||
}
|
||||
except:
|
||||
pass
|
||||
|
||||
try:
|
||||
# Versuche Gen1 API (ältere Shelly-Geräte)
|
||||
response = requests.get(
|
||||
f"http://{ip}/shelly",
|
||||
timeout=2
|
||||
)
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
if 'type' in data and data['type'].startswith('SHELLY'):
|
||||
logger.info(f"Shelly Gen1 Gerät gefunden: {ip} - {data.get('type', 'Unknown')}")
|
||||
return {
|
||||
'ip': ip,
|
||||
'generation': 1,
|
||||
'info': data
|
||||
}
|
||||
except:
|
||||
pass
|
||||
|
||||
return None
|
||||
|
||||
def get_device_status(self, device: Dict) -> Optional[Dict]:
|
||||
"""
|
||||
Holt den Status eines Shelly-Geräts
|
||||
|
||||
Args:
|
||||
device: Device Info Dictionary
|
||||
|
||||
Returns:
|
||||
Status Dictionary oder None
|
||||
"""
|
||||
ip = device['ip']
|
||||
|
||||
try:
|
||||
if device['generation'] == 2:
|
||||
# Gen2 Status
|
||||
response = requests.get(
|
||||
f"http://{ip}/rpc/Shelly.GetStatus",
|
||||
timeout=2
|
||||
)
|
||||
if response.status_code == 200:
|
||||
return response.json()
|
||||
else:
|
||||
# Gen1 Status
|
||||
response = requests.get(
|
||||
f"http://{ip}/status",
|
||||
timeout=2
|
||||
)
|
||||
if response.status_code == 200:
|
||||
return response.json()
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Abrufen des Status von {ip}: {e}")
|
||||
|
||||
return None
|
||||
|
||||
def discover_devices(self, start_ip: int = 1, end_ip: int = 254) -> List[Dict]:
|
||||
"""
|
||||
Entdeckt alle Shelly-Geräte im Netzwerk
|
||||
|
||||
Returns:
|
||||
Liste von Shelly-Geräten mit Status
|
||||
"""
|
||||
active_hosts = self.scan_network(start_ip, end_ip)
|
||||
|
||||
for ip in active_hosts:
|
||||
device = self.is_shelly_device(ip)
|
||||
if device:
|
||||
status = self.get_device_status(device)
|
||||
device['status'] = status
|
||||
self.devices.append(device)
|
||||
|
||||
logger.info(f"Insgesamt {len(self.devices)} Shelly-Geräte entdeckt")
|
||||
return self.devices
|
||||
|
||||
|
||||
class ShellyDatabaseWriter:
|
||||
"""Klasse zum Schreiben der Shelly-Daten in die Datenbank"""
|
||||
|
||||
def __init__(self, host: str, user: str, password: str, database: str):
|
||||
self.host = host
|
||||
self.user = user
|
||||
self.password = password
|
||||
self.database = database
|
||||
self.connection = None
|
||||
|
||||
def connect(self):
|
||||
"""Stellt Verbindung zur Datenbank her"""
|
||||
try:
|
||||
self.connection = mysql.connector.connect(
|
||||
host=self.host,
|
||||
user=self.user,
|
||||
password=self.password,
|
||||
database=self.database,
|
||||
charset='utf8mb4',
|
||||
collation='utf8mb4_bin'
|
||||
)
|
||||
logger.info("Datenbankverbindung hergestellt")
|
||||
except Error as e:
|
||||
logger.error(f"Fehler bei Datenbankverbindung: {e}")
|
||||
raise
|
||||
|
||||
def disconnect(self):
|
||||
"""Schließt Datenbankverbindung"""
|
||||
if self.connection and self.connection.is_connected():
|
||||
self.connection.close()
|
||||
logger.info("Datenbankverbindung geschlossen")
|
||||
|
||||
def parse_gen2_device(self, device: Dict) -> tuple:
|
||||
"""
|
||||
Parst Gen2 Shelly-Gerät und extrahiert Aktoren/Sensoren
|
||||
|
||||
Returns:
|
||||
(actors, sensors) Tuple mit Listen
|
||||
"""
|
||||
actors = []
|
||||
sensors = []
|
||||
|
||||
info = device.get('info', {})
|
||||
status = device.get('status', {})
|
||||
ip = device['ip']
|
||||
|
||||
device_name = info.get('name', f"Shelly_{info.get('id', ip)}")
|
||||
device_model = info.get('model', 'Unknown')
|
||||
|
||||
# Switches als Aktoren
|
||||
if 'switch:0' in status or 'switch' in status:
|
||||
switch_count = 0
|
||||
for key in status.keys():
|
||||
if key.startswith('switch:'):
|
||||
switch_count += 1
|
||||
|
||||
for i in range(switch_count):
|
||||
switch_data = status.get(f'switch:{i}', {})
|
||||
actors.append({
|
||||
'type': f'ShellySwitch_{device_model}',
|
||||
'name': f"{device_name}_Switch_{i}",
|
||||
'url': f"http://{ip}/rpc/Switch.Set?id={i}",
|
||||
'parameters': json.dumps({
|
||||
'device_id': info.get('id'),
|
||||
'switch_id': i,
|
||||
'model': device_model,
|
||||
'generation': 2
|
||||
}),
|
||||
'commands': [
|
||||
{'command_name': 'turn_on', 'params': []},
|
||||
{'command_name': 'turn_off', 'params': []},
|
||||
{'command_name': 'toggle', 'params': []}
|
||||
],
|
||||
'states': [
|
||||
{
|
||||
'state_name': 'output',
|
||||
'current_value': str(switch_data.get('output', False)),
|
||||
'unit': None
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
# Cover/Roller als Aktoren
|
||||
if 'cover:0' in status:
|
||||
cover_count = 0
|
||||
for key in status.keys():
|
||||
if key.startswith('cover:'):
|
||||
cover_count += 1
|
||||
|
||||
for i in range(cover_count):
|
||||
cover_data = status.get(f'cover:{i}', {})
|
||||
actors.append({
|
||||
'type': f'ShellyCover_{device_model}',
|
||||
'name': f"{device_name}_Cover_{i}",
|
||||
'url': f"http://{ip}/rpc/Cover.GoToPosition?id={i}",
|
||||
'parameters': json.dumps({
|
||||
'device_id': info.get('id'),
|
||||
'cover_id': i,
|
||||
'model': device_model,
|
||||
'generation': 2
|
||||
}),
|
||||
'commands': [
|
||||
{'command_name': 'open', 'params': []},
|
||||
{'command_name': 'close', 'params': []},
|
||||
{'command_name': 'stop', 'params': []},
|
||||
{'command_name': 'set_position', 'params': [
|
||||
{
|
||||
'parameter_name': 'position',
|
||||
'parameter_type': 'integer',
|
||||
'min_value': 0,
|
||||
'max_value': 100
|
||||
}
|
||||
]}
|
||||
],
|
||||
'states': [
|
||||
{
|
||||
'state_name': 'current_pos',
|
||||
'current_value': str(cover_data.get('current_pos', 0)),
|
||||
'unit': '%'
|
||||
},
|
||||
{
|
||||
'state_name': 'state',
|
||||
'current_value': cover_data.get('state', 'unknown'),
|
||||
'unit': None
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
# Temperatur-Sensoren
|
||||
for key in status.keys():
|
||||
if key.startswith('temperature:'):
|
||||
temp_id = key.split(':')[1]
|
||||
temp_data = status[key]
|
||||
sensors.append({
|
||||
'type': 'ShellyTemperatureSensor',
|
||||
'name': f"{device_name}_Temperature_{temp_id}",
|
||||
'url': f"http://{ip}/rpc/Temperature.GetStatus?id={temp_id}",
|
||||
'parameters': json.dumps({
|
||||
'device_id': info.get('id'),
|
||||
'sensor_id': temp_id,
|
||||
'model': device_model
|
||||
}),
|
||||
'states': [
|
||||
{
|
||||
'state_name': 'temperature',
|
||||
'current_value': str(temp_data.get('tC', 0)),
|
||||
'unit': '°C'
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
# Humidity-Sensoren
|
||||
for key in status.keys():
|
||||
if key.startswith('humidity:'):
|
||||
hum_id = key.split(':')[1]
|
||||
hum_data = status[key]
|
||||
sensors.append({
|
||||
'type': 'ShellyHumiditySensor',
|
||||
'name': f"{device_name}_Humidity_{hum_id}",
|
||||
'url': f"http://{ip}/rpc/Humidity.GetStatus?id={hum_id}",
|
||||
'parameters': json.dumps({
|
||||
'device_id': info.get('id'),
|
||||
'sensor_id': hum_id,
|
||||
'model': device_model
|
||||
}),
|
||||
'states': [
|
||||
{
|
||||
'state_name': 'humidity',
|
||||
'current_value': str(hum_data.get('rh', 0)),
|
||||
'unit': '%'
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
# Energie-Sensoren (Power Meter)
|
||||
for i in range(10): # Max 10 switches/covers prüfen
|
||||
switch_key = f'switch:{i}'
|
||||
if switch_key in status:
|
||||
switch_data = status[switch_key]
|
||||
if 'apower' in switch_data: # Aktuelle Leistung
|
||||
sensors.append({
|
||||
'type': 'ShellyPowerMeter',
|
||||
'name': f"{device_name}_Power_{i}",
|
||||
'url': f"http://{ip}/rpc/Switch.GetStatus?id={i}",
|
||||
'parameters': json.dumps({
|
||||
'device_id': info.get('id'),
|
||||
'switch_id': i,
|
||||
'model': device_model
|
||||
}),
|
||||
'states': [
|
||||
{
|
||||
'state_name': 'active_power',
|
||||
'current_value': str(switch_data.get('apower', 0)),
|
||||
'unit': 'W'
|
||||
},
|
||||
{
|
||||
'state_name': 'voltage',
|
||||
'current_value': str(switch_data.get('voltage', 0)),
|
||||
'unit': 'V'
|
||||
},
|
||||
{
|
||||
'state_name': 'current',
|
||||
'current_value': str(switch_data.get('current', 0)),
|
||||
'unit': 'A'
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
return actors, sensors
|
||||
|
||||
def parse_gen1_device(self, device: Dict) -> tuple:
|
||||
"""
|
||||
Parst Gen1 Shelly-Gerät und extrahiert Aktoren/Sensoren
|
||||
|
||||
Returns:
|
||||
(actors, sensors) Tuple mit Listen
|
||||
"""
|
||||
actors = []
|
||||
sensors = []
|
||||
|
||||
info = device.get('info', {})
|
||||
status = device.get('status', {})
|
||||
ip = device['ip']
|
||||
|
||||
device_type = info.get('type', 'Unknown')
|
||||
device_name = info.get('name', f"Shelly_{device_type}_{ip}")
|
||||
|
||||
# Relays als Aktoren
|
||||
if 'relays' in status:
|
||||
for i, relay in enumerate(status['relays']):
|
||||
actors.append({
|
||||
'type': f'ShellyRelay_{device_type}',
|
||||
'name': f"{device_name}_Relay_{i}",
|
||||
'url': f"http://{ip}/relay/{i}",
|
||||
'parameters': json.dumps({
|
||||
'device_type': device_type,
|
||||
'relay_id': i,
|
||||
'generation': 1
|
||||
}),
|
||||
'commands': [
|
||||
{'command_name': 'turn_on', 'params': []},
|
||||
{'command_name': 'turn_off', 'params': []},
|
||||
{'command_name': 'toggle', 'params': []}
|
||||
],
|
||||
'states': [
|
||||
{
|
||||
'state_name': 'ison',
|
||||
'current_value': str(relay.get('ison', False)),
|
||||
'unit': None
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
# Rollers als Aktoren
|
||||
if 'rollers' in status:
|
||||
for i, roller in enumerate(status['rollers']):
|
||||
actors.append({
|
||||
'type': f'ShellyRoller_{device_type}',
|
||||
'name': f"{device_name}_Roller_{i}",
|
||||
'url': f"http://{ip}/roller/{i}",
|
||||
'parameters': json.dumps({
|
||||
'device_type': device_type,
|
||||
'roller_id': i,
|
||||
'generation': 1
|
||||
}),
|
||||
'commands': [
|
||||
{'command_name': 'open', 'params': []},
|
||||
{'command_name': 'close', 'params': []},
|
||||
{'command_name': 'stop', 'params': []},
|
||||
{'command_name': 'go_to_position', 'params': [
|
||||
{
|
||||
'parameter_name': 'position',
|
||||
'parameter_type': 'integer',
|
||||
'min_value': 0,
|
||||
'max_value': 100
|
||||
}
|
||||
]}
|
||||
],
|
||||
'states': [
|
||||
{
|
||||
'state_name': 'current_pos',
|
||||
'current_value': str(roller.get('current_pos', 0)),
|
||||
'unit': '%'
|
||||
},
|
||||
{
|
||||
'state_name': 'state',
|
||||
'current_value': roller.get('state', 'stop'),
|
||||
'unit': None
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
# Temperatur-Sensoren
|
||||
if 'tmp' in status:
|
||||
temp_data = status['tmp']
|
||||
if 'tC' in temp_data:
|
||||
sensors.append({
|
||||
'type': 'ShellyTemperatureSensor',
|
||||
'name': f"{device_name}_Temperature",
|
||||
'url': f"http://{ip}/status",
|
||||
'parameters': json.dumps({
|
||||
'device_type': device_type,
|
||||
'generation': 1
|
||||
}),
|
||||
'states': [
|
||||
{
|
||||
'state_name': 'temperature',
|
||||
'current_value': str(temp_data.get('tC', 0)),
|
||||
'unit': '°C'
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
# Energie-Sensoren (Meters)
|
||||
if 'meters' in status:
|
||||
for i, meter in enumerate(status['meters']):
|
||||
sensors.append({
|
||||
'type': 'ShellyPowerMeter',
|
||||
'name': f"{device_name}_Power_{i}",
|
||||
'url': f"http://{ip}/status",
|
||||
'parameters': json.dumps({
|
||||
'device_type': device_type,
|
||||
'meter_id': i,
|
||||
'generation': 1
|
||||
}),
|
||||
'states': [
|
||||
{
|
||||
'state_name': 'power',
|
||||
'current_value': str(meter.get('power', 0)),
|
||||
'unit': 'W'
|
||||
},
|
||||
{
|
||||
'state_name': 'total',
|
||||
'current_value': str(meter.get('total', 0)),
|
||||
'unit': 'Wh'
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
return actors, sensors
|
||||
|
||||
def insert_actor(self, actor: Dict) -> Optional[int]:
|
||||
"""
|
||||
Fügt einen Aktor in die Datenbank ein
|
||||
|
||||
Returns:
|
||||
Actor ID oder None bei Fehler
|
||||
"""
|
||||
try:
|
||||
cursor = self.connection.cursor()
|
||||
|
||||
# Prüfe ob Aktor bereits existiert
|
||||
cursor.execute(
|
||||
"SELECT id FROM actors WHERE url = %s",
|
||||
(actor['url'],)
|
||||
)
|
||||
result = cursor.fetchone()
|
||||
|
||||
if result:
|
||||
actor_id = result[0]
|
||||
# Update bestehender Aktor
|
||||
cursor.execute(
|
||||
"""UPDATE actors
|
||||
SET type = %s, name = %s, parameters = %s
|
||||
WHERE id = %s""",
|
||||
(actor['type'], actor['name'], actor['parameters'], actor_id)
|
||||
)
|
||||
logger.info(f"Aktor aktualisiert: {actor['name']}")
|
||||
else:
|
||||
# Neuer Aktor
|
||||
cursor.execute(
|
||||
"""INSERT INTO actors (type, name, parameters, url)
|
||||
VALUES (%s, %s, %s, %s)""",
|
||||
(actor['type'], actor['name'], actor['parameters'], actor['url'])
|
||||
)
|
||||
actor_id = cursor.lastrowid
|
||||
logger.info(f"Neuer Aktor eingefügt: {actor['name']}")
|
||||
|
||||
# Commands einfügen
|
||||
for command in actor.get('commands', []):
|
||||
cursor.execute(
|
||||
"""INSERT INTO actor_commands (actor_id, command_name)
|
||||
VALUES (%s, %s)
|
||||
ON DUPLICATE KEY UPDATE command_name = command_name""",
|
||||
(actor_id, command['command_name'])
|
||||
)
|
||||
command_id = cursor.lastrowid
|
||||
|
||||
# Command Parameters einfügen
|
||||
for param in command.get('params', []):
|
||||
cursor.execute(
|
||||
"""INSERT INTO command_parameters
|
||||
(command_id, parameter_name, parameter_type, min_value, max_value)
|
||||
VALUES (%s, %s, %s, %s, %s)""",
|
||||
(command_id, param['parameter_name'], param['parameter_type'],
|
||||
param.get('min_value'), param.get('max_value'))
|
||||
)
|
||||
|
||||
# States einfügen
|
||||
for state in actor.get('states', []):
|
||||
cursor.execute(
|
||||
"""INSERT INTO actor_states
|
||||
(actor_id, state_name, current_value, unit)
|
||||
VALUES (%s, %s, %s, %s)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
current_value = VALUES(current_value),
|
||||
last_updated = CURRENT_TIMESTAMP""",
|
||||
(actor_id, state['state_name'], state['current_value'], state['unit'])
|
||||
)
|
||||
|
||||
self.connection.commit()
|
||||
cursor.close()
|
||||
return actor_id
|
||||
|
||||
except Error as e:
|
||||
logger.error(f"Fehler beim Einfügen des Aktors: {e}")
|
||||
self.connection.rollback()
|
||||
return None
|
||||
|
||||
def insert_sensor(self, sensor: Dict) -> Optional[int]:
|
||||
"""
|
||||
Fügt einen Sensor in die Datenbank ein
|
||||
|
||||
Returns:
|
||||
Sensor ID oder None bei Fehler
|
||||
"""
|
||||
try:
|
||||
cursor = self.connection.cursor()
|
||||
|
||||
# Prüfe ob Sensor bereits existiert
|
||||
cursor.execute(
|
||||
"SELECT id FROM sensors WHERE url = %s",
|
||||
(sensor['url'],)
|
||||
)
|
||||
result = cursor.fetchone()
|
||||
|
||||
if result:
|
||||
sensor_id = result[0]
|
||||
# Update bestehender Sensor
|
||||
cursor.execute(
|
||||
"""UPDATE sensors
|
||||
SET type = %s, name = %s, parameters = %s
|
||||
WHERE id = %s""",
|
||||
(sensor['type'], sensor['name'], sensor['parameters'], sensor_id)
|
||||
)
|
||||
logger.info(f"Sensor aktualisiert: {sensor['name']}")
|
||||
else:
|
||||
# Neuer Sensor
|
||||
cursor.execute(
|
||||
"""INSERT INTO sensors (type, name, parameters, url)
|
||||
VALUES (%s, %s, %s, %s)""",
|
||||
(sensor['type'], sensor['name'], sensor['parameters'], sensor['url'])
|
||||
)
|
||||
sensor_id = cursor.lastrowid
|
||||
logger.info(f"Neuer Sensor eingefügt: {sensor['name']}")
|
||||
|
||||
# States einfügen
|
||||
for state in sensor.get('states', []):
|
||||
cursor.execute(
|
||||
"""INSERT INTO sensor_states
|
||||
(sensor_id, state_name, current_value, unit)
|
||||
VALUES (%s, %s, %s, %s)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
current_value = VALUES(current_value),
|
||||
last_updated = CURRENT_TIMESTAMP""",
|
||||
(sensor_id, state['state_name'], state['current_value'], state['unit'])
|
||||
)
|
||||
|
||||
self.connection.commit()
|
||||
cursor.close()
|
||||
return sensor_id
|
||||
|
||||
except Error as e:
|
||||
logger.error(f"Fehler beim Einfügen des Sensors: {e}")
|
||||
self.connection.rollback()
|
||||
return None
|
||||
|
||||
def process_devices(self, devices: List[Dict]):
|
||||
"""
|
||||
Verarbeitet alle entdeckten Geräte und schreibt sie in die DB
|
||||
"""
|
||||
total_actors = 0
|
||||
total_sensors = 0
|
||||
|
||||
for device in devices:
|
||||
logger.info(f"Verarbeite Gerät: {device['ip']}")
|
||||
|
||||
if device['generation'] == 2:
|
||||
actors, sensors = self.parse_gen2_device(device)
|
||||
else:
|
||||
actors, sensors = self.parse_gen1_device(device)
|
||||
|
||||
# Aktoren einfügen
|
||||
for actor in actors:
|
||||
if self.insert_actor(actor):
|
||||
total_actors += 1
|
||||
|
||||
# Sensoren einfügen
|
||||
for sensor in sensors:
|
||||
if self.insert_sensor(sensor):
|
||||
total_sensors += 1
|
||||
|
||||
logger.info(f"Verarbeitung abgeschlossen: {total_actors} Aktoren, {total_sensors} Sensoren")
|
||||
|
||||
|
||||
def main():
|
||||
"""Hauptfunktion"""
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Findet Shelly-Geräte im Netzwerk und schreibt sie in die Datenbank'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--network',
|
||||
default='192.168.1',
|
||||
help='Netzwerk-Präfix (Standard: 192.168.1)'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--start-ip',
|
||||
type=int,
|
||||
default=1,
|
||||
help='Start IP (letztes Oktett, Standard: 1)'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--end-ip',
|
||||
type=int,
|
||||
default=254,
|
||||
help='End IP (letztes Oktett, Standard: 254)'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--db-host',
|
||||
default='localhost',
|
||||
help='Datenbank Host (Standard: localhost)'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--db-user',
|
||||
required=True,
|
||||
help='Datenbank Benutzer'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--db-password',
|
||||
required=True,
|
||||
help='Datenbank Passwort'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--db-name',
|
||||
default='EnergyFlow',
|
||||
help='Datenbank Name (Standard: EnergyFlow)'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--debug',
|
||||
action='store_true',
|
||||
help='Debug-Modus aktivieren'
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.debug:
|
||||
logger.setLevel(logging.DEBUG)
|
||||
|
||||
# Discovery
|
||||
logger.info("Starte Shelly Device Discovery...")
|
||||
discovery = ShellyDiscovery(network_range=args.network)
|
||||
devices = discovery.discover_devices(start_ip=args.start_ip, end_ip=args.end_ip)
|
||||
|
||||
if not devices:
|
||||
logger.warning("Keine Shelly-Geräte gefunden!")
|
||||
return
|
||||
|
||||
# Datenbank-Schreibvorgang
|
||||
logger.info("Schreibe Geräte in Datenbank...")
|
||||
db_writer = ShellyDatabaseWriter(
|
||||
host=args.db_host,
|
||||
user=args.db_user,
|
||||
password=args.db_password,
|
||||
database=args.db_name
|
||||
)
|
||||
|
||||
try:
|
||||
db_writer.connect()
|
||||
db_writer.process_devices(devices)
|
||||
finally:
|
||||
db_writer.disconnect()
|
||||
|
||||
logger.info("Fertig!")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user