modify filemode for linux

This commit is contained in:
2026-02-14 20:08:34 +01:00
parent 0e78302640
commit ae455dba20
393 changed files with 114401 additions and 114401 deletions
@@ -1,93 +1,93 @@
#!/usr/bin/env python3
"""
Base Module Interface
Definiert die einheitliche Schnittstelle für alle Gerätemodule
"""
from abc import ABC, abstractmethod
from typing import List, Dict, Tuple
import logging
logger = logging.getLogger(__name__)
class BaseModule(ABC):
"""
Abstrakte Basisklasse für alle Gerätemodule
Jedes Modul muss nur discover() implementieren und gibt
eine Liste von (actors, sensors) zurück.
Die Datenbank-Logik bleibt im Hauptscript.
"""
def __init__(self, config):
"""
Initialisiert das Modul
Args:
config: Config-Objekt mit allen Einstellungen
"""
self.config = config
self.module_name = self.__class__.__name__.replace('Module', '')
@abstractmethod
def discover(self) -> Tuple[List[Dict], List[Dict]]:
"""
Führt Device Discovery durch
Returns:
Tuple (actors, sensors) mit Listen von Dicts:
Actor Dict Format:
{
'type': str, # z.B. 'RollerShutter'
'name': str, # z.B. 'Wohnzimmer Rollo'
'url': str, # Eindeutige ID/URL
'commands': [ # Liste von Commands
{
'command': str,
'parameters': [
{
'name': str,
'type': str,
'min': float (optional),
'max': float (optional),
'values': list (optional)
}
]
}
],
'states': [ # Liste von States
{
'name': str,
'type': int/str,
'current_value': any,
'unit': str (optional)
}
]
}
Sensor Dict Format:
{
'type': str, # z.B. 'TemperatureSensor'
'name': str, # z.B. 'Außentemperatur'
'url': str, # Eindeutige ID/URL
'states': [ # Liste von States
{
'name': str,
'type': int/str,
'current_value': any,
'unit': str (optional)
}
]
}
"""
pass
def is_enabled(self) -> bool:
"""Prüft ob Modul aktiviert ist"""
return True # Override in Subklassen falls nötig
def get_name(self) -> str:
"""Gibt Modulname zurück"""
return self.module_name
#!/usr/bin/env python3
"""
Base Module Interface
Definiert die einheitliche Schnittstelle für alle Gerätemodule
"""
from abc import ABC, abstractmethod
from typing import List, Dict, Tuple
import logging
logger = logging.getLogger(__name__)
class BaseModule(ABC):
"""
Abstrakte Basisklasse für alle Gerätemodule
Jedes Modul muss nur discover() implementieren und gibt
eine Liste von (actors, sensors) zurück.
Die Datenbank-Logik bleibt im Hauptscript.
"""
def __init__(self, config):
"""
Initialisiert das Modul
Args:
config: Config-Objekt mit allen Einstellungen
"""
self.config = config
self.module_name = self.__class__.__name__.replace('Module', '')
@abstractmethod
def discover(self) -> Tuple[List[Dict], List[Dict]]:
"""
Führt Device Discovery durch
Returns:
Tuple (actors, sensors) mit Listen von Dicts:
Actor Dict Format:
{
'type': str, # z.B. 'RollerShutter'
'name': str, # z.B. 'Wohnzimmer Rollo'
'url': str, # Eindeutige ID/URL
'commands': [ # Liste von Commands
{
'command': str,
'parameters': [
{
'name': str,
'type': str,
'min': float (optional),
'max': float (optional),
'values': list (optional)
}
]
}
],
'states': [ # Liste von States
{
'name': str,
'type': int/str,
'current_value': any,
'unit': str (optional)
}
]
}
Sensor Dict Format:
{
'type': str, # z.B. 'TemperatureSensor'
'name': str, # z.B. 'Außentemperatur'
'url': str, # Eindeutige ID/URL
'states': [ # Liste von States
{
'name': str,
'type': int/str,
'current_value': any,
'unit': str (optional)
}
]
}
"""
pass
def is_enabled(self) -> bool:
"""Prüft ob Modul aktiviert ist"""
return True # Override in Subklassen falls nötig
def get_name(self) -> str:
"""Gibt Modulname zurück"""
return self.module_name
File diff suppressed because it is too large Load Diff
@@ -1,456 +1,456 @@
#!/usr/bin/env python3
"""
Shelly Module
Enthält NUR Shelly-spezifische Geräte-Discovery Logik
KEINE Datenbank-Operationen!
"""
import json
import requests
import socket
import logging
from typing import List, Dict, Optional, Tuple
from modules.base_module import BaseModule
# 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 = 2, end_ip: int = 254, timeout: float = 0.3) -> 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
"""
logger.info(f"Suche Shelly Getät unter: {ip}")
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') or data['type'].startswith('SHSW')):
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
# ============================================================================
# MODULE WRAPPER
# ============================================================================
class ShellyModule(BaseModule):
"""
Shelly Modul - Implementiert BaseModule Interface
Gibt Actors/Sensors zurück, KEINE DB-Operationen
"""
def is_enabled(self) -> bool:
"""Prüft ob Shelly aktiviert ist"""
return self.config.shelly_enable
def discover(self) -> Tuple[List[Dict], List[Dict]]:
"""
Führt Shelly Discovery durch
Returns:
Tuple (actors, sensors)
"""
logger.info("\n" + "=" * 60)
logger.info("SHELLY-GERÄTE WERDEN GESUCHT")
logger.info("=" * 60)
actors = []
sensors = []
try:
discovery = ShellyDiscovery(network_range=self.config.shelly_network_range)
devices = discovery.discover_devices(
start_ip=self.config.shelly_start_ip,
end_ip=self.config.shelly_end_ip
)
if not devices:
logger.info("Keine Shelly-Geräte gefunden")
return actors, sensors
logger.info(f"{len(devices)} Shelly-Geräte gefunden")
# Verarbeite jedes Gerät
for device in devices:
if device['generation'] == 2:
device_actors, device_sensors = self._parse_gen2_device(device)
else:
device_actors, device_sensors = self._parse_gen1_device(device)
actors.extend(device_actors)
sensors.extend(device_sensors)
except Exception as e:
logger.error(f"✗ Shelly Discovery Fehler: {e}")
logger.info(f"Shelly: {len(actors)} Aktoren, {len(sensors)} Sensoren gefunden")
return actors, sensors
def _parse_gen2_device(self, device: Dict) -> Tuple[List[Dict], List[Dict]]:
"""Parst Gen2 Shelly-Gerät"""
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
switch_count = sum(1 for key in status.keys() if key.startswith('switch:'))
for i in range(switch_count):
switch_data = status.get(f'switch:{i}', {})
actors.append({
'type': f'ShellySwitch_{device_model}'.replace(' ', '_'),
'name': f"{device_name}_Switch_{i}",
'url': f"http://{ip}/rpc/Switch.Set?id={i}",
'commands': [
{'command': 'turn_on', 'parameters': []},
{'command': 'turn_off', 'parameters': []},
{'command': 'toggle', 'parameters': []}
],
'states': [
{
'name': 'output',
'type': 'boolean',
'current_value': switch_data.get('output', False)
}
]
})
# Temperatursensoren
temp_count = sum(1 for key in status.keys() if key.startswith('temperature:'))
for i in range(temp_count):
temp_data = status.get(f'temperature:{i}', {})
sensors.append({
'type': 'ShellyTemperatureSensor',
'name': f"{device_name}_Temp_{i}",
'url': f"http://{ip}/rpc/Temperature.GetStatus?id={i}",
'states': [
{
'name': 'temperature',
'type': 'number',
'current_value': temp_data.get('tC'),
'unit': '°C'
}
]
})
# Energy-Meter
em_count = sum(1 for key in status.keys() if key.startswith('em:'))
for i in range(em_count):
em_data = status.get(f'em:{i}', {})
sensors.append({
'type': 'ShellyEnergyMeter',
'name': f"{device_name}_EM_{i}",
'url': f"http://{ip}/rpc/em.GetStatus?id={i}",
'states': []
})
if(em_data.get('a_voltage') is not None):
sensors[len(sensors)-1]['states'].append({
'name': 'Spannung Phase A',
'type': 'number',
'url': 'a_voltage',
'current_value': em_data.get('a_voltage'),
'unit': 'V'})
if(em_data.get('b_voltage') is not None):
sensors[len(sensors)-1]['states'].append({
'name': 'Spannung Phase B',
'type': 'number',
'url': 'b_voltage',
'current_value': em_data.get('b_voltage'),
'unit': 'V'})
if(em_data.get('c_voltage') is not None):
sensors[len(sensors)-1]['states'].append({
'name': 'Spannung Phase C',
'type': 'number',
'url': 'c_voltage',
'current_value': em_data.get('c_voltage'),
'unit': 'V'})
if(em_data.get('a_current') is not None):
sensors[len(sensors)-1]['states'].append({
'name': 'Strom Phase A',
'type': 'number',
'url': 'a_current',
'current_value': em_data.get('a_current'),
'unit': 'A'})
if(em_data.get('b_current') is not None):
sensors[len(sensors)-1]['states'].append({
'name': 'Strom Phase B',
'type': 'number',
'url': 'b_current',
'current_value': em_data.get('b_current'),
'unit': 'A'})
if(em_data.get('c_current') is not None):
sensors[len(sensors)-1]['states'].append({
'name': 'Strom Phase C',
'type': 'number',
'url': 'c_current',
'current_value': em_data.get('c_current'),
'unit': 'A'})
if(em_data.get('a_act_power') is not None):
sensors[len(sensors)-1]['states'].append({
'name': 'Wirkleistung Phase A',
'type': 'number',
'url': 'a_act_power',
'current_value': em_data.get('a_act_power'),
'unit': 'W'})
if(em_data.get('b_act_power') is not None):
sensors[len(sensors)-1]['states'].append({
'name': 'Wirkleistung Phase B',
'type': 'number',
'url': 'b_act_power',
'current_value': em_data.get('b_act_power'),
'unit': 'W'})
if(em_data.get('c_act_power') is not None):
sensors[len(sensors)-1]['states'].append({
'name': 'Wirkleistung Phase C',
'type': 'number',
'url': 'c_act_power',
'current_value': em_data.get('c_act_power'),
'unit': 'W'})
if(em_data.get('a_aprt_power') is not None):
sensors[len(sensors)-1]['states'].append({
'name': 'Scheinleistung Phase A',
'type': 'number',
'url': 'a_aprt_power',
'current_value': em_data.get('a_aprt_power'),
'unit': 'VA'})
if(em_data.get('b_aprt_power') is not None):
sensors[len(sensors)-1]['states'].append({
'name': 'Scheinleistung Phase B',
'type': 'number',
'url': 'b_aprt_power',
'current_value': em_data.get('b_aprt_power'),
'unit': 'VA'})
if(em_data.get('c_aprt_power') is not None):
sensors[len(sensors)-1]['states'].append({
'name': 'Scheinleistung Phase C',
'type': 'number',
'url': 'c_aprt_power',
'current_value': em_data.get('c_aprt_power'),
'unit': 'VA'})
if(em_data.get('a_freq') is not None):
sensors[len(sensors)-1]['states'].append({
'name': 'Frequenz Phase A',
'type': 'number',
'url': 'a_freq',
'current_value': em_data.get('a_freq'),
'unit': 'Hz'})
if(em_data.get('b_freq') is not None):
sensors[len(sensors)-1]['states'].append({
'name': 'Frequenz Phase B',
'type': 'number',
'url': 'b_freq',
'current_value': em_data.get('b_freq'),
'unit': 'Hz'})
if(em_data.get('c_freq') is not None):
sensors[len(sensors)-1]['states'].append({
'name': 'Frequenz Phase C',
'type': 'number',
'url': 'c_freq',
'current_value': em_data.get('c_freq'),
'unit': 'Hz'})
if(em_data.get('total_act_power') is not None):
sensors[len(sensors)-1]['states'].append({
'name': 'Wirkleistung gesamt',
'type': 'number',
'url': 'total_act_power',
'current_value': em_data.get('total_act_power'),
'unit': 'W'})
return actors, sensors
def _parse_gen1_device(self, device: Dict) -> Tuple[List[Dict], List[Dict]]:
"""Parst Gen1 Shelly-Gerät"""
actors = []
sensors = []
info = device.get('info', {})
status = device.get('status', {})
ip = device['ip']
device_name = info.get('name', f"Shelly_{info.get('type', ip)}")
device_type = info.get('type', 'Unknown')
# Relays als Aktoren
relays = status.get('relays', [])
for i, relay in enumerate(relays):
actors.append({
'type': f'ShellyRelay_{device_type}'.replace(' ', '_'),
'name': f"{device_name}_Relay_{i}",
'url': f"http://{ip}/relay/{i}",
'commands': [
{'command': 'turn_on', 'parameters': []},
{'command': 'turn_off', 'parameters': []},
{'command': 'toggle', 'parameters': []}
],
'states': [
{
'name': 'ison',
'type': 'boolean',
'current_value': relay.get('ison', False)
}
]
})
# Temperatursensoren
temp_data = status.get('tmp', {})
if temp_data and 'tC' in temp_data:
sensors.append({
'type': 'ShellyTemperatureSensor',
'name': f"{device_name}_Temp",
'url': f"http://{ip}/status",
'states': [
{
'name': 'temperature',
'type': 'number',
'current_value': temp_data.get('tC'),
'unit': '°C'
}
]
})
#!/usr/bin/env python3
"""
Shelly Module
Enthält NUR Shelly-spezifische Geräte-Discovery Logik
KEINE Datenbank-Operationen!
"""
import json
import requests
import socket
import logging
from typing import List, Dict, Optional, Tuple
from modules.base_module import BaseModule
# 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 = 2, end_ip: int = 254, timeout: float = 0.3) -> 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
"""
logger.info(f"Suche Shelly Getät unter: {ip}")
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') or data['type'].startswith('SHSW')):
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
# ============================================================================
# MODULE WRAPPER
# ============================================================================
class ShellyModule(BaseModule):
"""
Shelly Modul - Implementiert BaseModule Interface
Gibt Actors/Sensors zurück, KEINE DB-Operationen
"""
def is_enabled(self) -> bool:
"""Prüft ob Shelly aktiviert ist"""
return self.config.shelly_enable
def discover(self) -> Tuple[List[Dict], List[Dict]]:
"""
Führt Shelly Discovery durch
Returns:
Tuple (actors, sensors)
"""
logger.info("\n" + "=" * 60)
logger.info("SHELLY-GERÄTE WERDEN GESUCHT")
logger.info("=" * 60)
actors = []
sensors = []
try:
discovery = ShellyDiscovery(network_range=self.config.shelly_network_range)
devices = discovery.discover_devices(
start_ip=self.config.shelly_start_ip,
end_ip=self.config.shelly_end_ip
)
if not devices:
logger.info("Keine Shelly-Geräte gefunden")
return actors, sensors
logger.info(f"{len(devices)} Shelly-Geräte gefunden")
# Verarbeite jedes Gerät
for device in devices:
if device['generation'] == 2:
device_actors, device_sensors = self._parse_gen2_device(device)
else:
device_actors, device_sensors = self._parse_gen1_device(device)
actors.extend(device_actors)
sensors.extend(device_sensors)
except Exception as e:
logger.error(f"✗ Shelly Discovery Fehler: {e}")
logger.info(f"Shelly: {len(actors)} Aktoren, {len(sensors)} Sensoren gefunden")
return actors, sensors
def _parse_gen2_device(self, device: Dict) -> Tuple[List[Dict], List[Dict]]:
"""Parst Gen2 Shelly-Gerät"""
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
switch_count = sum(1 for key in status.keys() if key.startswith('switch:'))
for i in range(switch_count):
switch_data = status.get(f'switch:{i}', {})
actors.append({
'type': f'ShellySwitch_{device_model}'.replace(' ', '_'),
'name': f"{device_name}_Switch_{i}",
'url': f"http://{ip}/rpc/Switch.Set?id={i}",
'commands': [
{'command': 'turn_on', 'parameters': []},
{'command': 'turn_off', 'parameters': []},
{'command': 'toggle', 'parameters': []}
],
'states': [
{
'name': 'output',
'type': 'boolean',
'current_value': switch_data.get('output', False)
}
]
})
# Temperatursensoren
temp_count = sum(1 for key in status.keys() if key.startswith('temperature:'))
for i in range(temp_count):
temp_data = status.get(f'temperature:{i}', {})
sensors.append({
'type': 'ShellyTemperatureSensor',
'name': f"{device_name}_Temp_{i}",
'url': f"http://{ip}/rpc/Temperature.GetStatus?id={i}",
'states': [
{
'name': 'temperature',
'type': 'number',
'current_value': temp_data.get('tC'),
'unit': '°C'
}
]
})
# Energy-Meter
em_count = sum(1 for key in status.keys() if key.startswith('em:'))
for i in range(em_count):
em_data = status.get(f'em:{i}', {})
sensors.append({
'type': 'ShellyEnergyMeter',
'name': f"{device_name}_EM_{i}",
'url': f"http://{ip}/rpc/em.GetStatus?id={i}",
'states': []
})
if(em_data.get('a_voltage') is not None):
sensors[len(sensors)-1]['states'].append({
'name': 'Spannung Phase A',
'type': 'number',
'url': 'a_voltage',
'current_value': em_data.get('a_voltage'),
'unit': 'V'})
if(em_data.get('b_voltage') is not None):
sensors[len(sensors)-1]['states'].append({
'name': 'Spannung Phase B',
'type': 'number',
'url': 'b_voltage',
'current_value': em_data.get('b_voltage'),
'unit': 'V'})
if(em_data.get('c_voltage') is not None):
sensors[len(sensors)-1]['states'].append({
'name': 'Spannung Phase C',
'type': 'number',
'url': 'c_voltage',
'current_value': em_data.get('c_voltage'),
'unit': 'V'})
if(em_data.get('a_current') is not None):
sensors[len(sensors)-1]['states'].append({
'name': 'Strom Phase A',
'type': 'number',
'url': 'a_current',
'current_value': em_data.get('a_current'),
'unit': 'A'})
if(em_data.get('b_current') is not None):
sensors[len(sensors)-1]['states'].append({
'name': 'Strom Phase B',
'type': 'number',
'url': 'b_current',
'current_value': em_data.get('b_current'),
'unit': 'A'})
if(em_data.get('c_current') is not None):
sensors[len(sensors)-1]['states'].append({
'name': 'Strom Phase C',
'type': 'number',
'url': 'c_current',
'current_value': em_data.get('c_current'),
'unit': 'A'})
if(em_data.get('a_act_power') is not None):
sensors[len(sensors)-1]['states'].append({
'name': 'Wirkleistung Phase A',
'type': 'number',
'url': 'a_act_power',
'current_value': em_data.get('a_act_power'),
'unit': 'W'})
if(em_data.get('b_act_power') is not None):
sensors[len(sensors)-1]['states'].append({
'name': 'Wirkleistung Phase B',
'type': 'number',
'url': 'b_act_power',
'current_value': em_data.get('b_act_power'),
'unit': 'W'})
if(em_data.get('c_act_power') is not None):
sensors[len(sensors)-1]['states'].append({
'name': 'Wirkleistung Phase C',
'type': 'number',
'url': 'c_act_power',
'current_value': em_data.get('c_act_power'),
'unit': 'W'})
if(em_data.get('a_aprt_power') is not None):
sensors[len(sensors)-1]['states'].append({
'name': 'Scheinleistung Phase A',
'type': 'number',
'url': 'a_aprt_power',
'current_value': em_data.get('a_aprt_power'),
'unit': 'VA'})
if(em_data.get('b_aprt_power') is not None):
sensors[len(sensors)-1]['states'].append({
'name': 'Scheinleistung Phase B',
'type': 'number',
'url': 'b_aprt_power',
'current_value': em_data.get('b_aprt_power'),
'unit': 'VA'})
if(em_data.get('c_aprt_power') is not None):
sensors[len(sensors)-1]['states'].append({
'name': 'Scheinleistung Phase C',
'type': 'number',
'url': 'c_aprt_power',
'current_value': em_data.get('c_aprt_power'),
'unit': 'VA'})
if(em_data.get('a_freq') is not None):
sensors[len(sensors)-1]['states'].append({
'name': 'Frequenz Phase A',
'type': 'number',
'url': 'a_freq',
'current_value': em_data.get('a_freq'),
'unit': 'Hz'})
if(em_data.get('b_freq') is not None):
sensors[len(sensors)-1]['states'].append({
'name': 'Frequenz Phase B',
'type': 'number',
'url': 'b_freq',
'current_value': em_data.get('b_freq'),
'unit': 'Hz'})
if(em_data.get('c_freq') is not None):
sensors[len(sensors)-1]['states'].append({
'name': 'Frequenz Phase C',
'type': 'number',
'url': 'c_freq',
'current_value': em_data.get('c_freq'),
'unit': 'Hz'})
if(em_data.get('total_act_power') is not None):
sensors[len(sensors)-1]['states'].append({
'name': 'Wirkleistung gesamt',
'type': 'number',
'url': 'total_act_power',
'current_value': em_data.get('total_act_power'),
'unit': 'W'})
return actors, sensors
def _parse_gen1_device(self, device: Dict) -> Tuple[List[Dict], List[Dict]]:
"""Parst Gen1 Shelly-Gerät"""
actors = []
sensors = []
info = device.get('info', {})
status = device.get('status', {})
ip = device['ip']
device_name = info.get('name', f"Shelly_{info.get('type', ip)}")
device_type = info.get('type', 'Unknown')
# Relays als Aktoren
relays = status.get('relays', [])
for i, relay in enumerate(relays):
actors.append({
'type': f'ShellyRelay_{device_type}'.replace(' ', '_'),
'name': f"{device_name}_Relay_{i}",
'url': f"http://{ip}/relay/{i}",
'commands': [
{'command': 'turn_on', 'parameters': []},
{'command': 'turn_off', 'parameters': []},
{'command': 'toggle', 'parameters': []}
],
'states': [
{
'name': 'ison',
'type': 'boolean',
'current_value': relay.get('ison', False)
}
]
})
# Temperatursensoren
temp_data = status.get('tmp', {})
if temp_data and 'tC' in temp_data:
sensors.append({
'type': 'ShellyTemperatureSensor',
'name': f"{device_name}_Temp",
'url': f"http://{ip}/status",
'states': [
{
'name': 'temperature',
'type': 'number',
'current_value': temp_data.get('tC'),
'unit': '°C'
}
]
})
return actors, sensors
@@ -1,310 +1,310 @@
#!/usr/bin/env python3
"""
Tahoma Module
Enthält NUR Tahoma-spezifische Geräte-Discovery Logik
KEINE Datenbank-Operationen!
"""
import requests
import urllib3
import re
import logging
from typing import List, Dict, Optional, Tuple
from modules.base_module import BaseModule
# SSL-Warnungen deaktivieren
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
logger = logging.getLogger(__name__)
class TahomaAPI:
"""Original TahomaAPI Klasse - unverändert"""
def __init__(self, gateway_ip: str, api_token: str):
self.base_url = f"https://{gateway_ip}:8443/enduser-mobile-web/1/enduserAPI"
self.headers = {
"Authorization": f"Bearer {api_token}",
"Content-Type": "application/json"
}
def get_setup(self) -> Optional[Dict]:
try:
url = f"{self.base_url}/setup"
response = requests.get(url, headers=self.headers, verify=False, timeout=10)
response.raise_for_status()
return response.json()
except requests.exceptions.RequestException as e:
logger.error(f"Fehler beim Abrufen der Setup-Daten: {e}")
return None
def get_devices(self) -> List[Dict]:
setup = self.get_setup()
if not setup:
return []
devices = setup.get('devices', [])
logger.info(f"{len(devices)} Tahoma-Geräte gefunden")
return devices
class DeviceClassifier:
"""Original DeviceClassifier - unverändert"""
ACTOR_TYPES = {
'RollerShutter', 'ExteriorScreen', 'Awning', 'Blind',
'GarageDoor', 'Window', 'Light', 'OnOff', 'DimmableLight',
'HeatingSystem', 'Valve', 'Switch', 'Door', 'Curtain',
'VenetianBlind', 'PergolaScreen'
}
SENSOR_TYPES = {
'TemperatureSensor', 'LightSensor', 'HumiditySensor',
'ContactSensor', 'OccupancySensor', 'SmokeSensor',
'WaterDetectionSensor', 'WindowHandle', 'MotionSensor',
'SunSensor', 'WindSensor', 'RainSensor', 'ConsumptionSensor'
}
# Tahoma Commands mit Parametern
TAHOMA_COMMANDS = {
"setClosure": [{"name": "position", "type": "integer", "min": 0, "max": 100}],
"setClosureAndOrientation": [
{"name": "position", "type": "integer", "min": 0, "max": 100},
{"name": "neigung", "type": "integer", "min": 0, "max": 100}
],
"setOrientation": [{"name": "neigung", "type": "integer", "min": 0, "max": 100}],
"up": [], "down": [], "my": [], "stop": [], "refresh": [], "wink":[],
"setMyPosition": [{"name": "position", "type": "integer", "min": 0, "max": 100}],
"on": [], "off": [], "toggle": [],
"setIntensity": [{"name": "helligkeit", "type": "integer", "min": 0, "max": 100}],
"setColor": [
{"name": "farbton", "type": "integer", "min": 0, "max": 360},
{"name": "sättigung", "type": "integer", "min": 0, "max": 100}
],
"setColorTemperature": [{"name": "farbtemperatur", "type": "integer", "min": 2000, "max": 6500}],
"setTargetTemperature": [{"name": "temperatur", "type": "float", "min": 5.0, "max": 30.0}],
"setMode": [{"name": "betriebsart", "type": "string"}],
"pulse": [{"name": "impuls_dauer", "type": "integer", "min": 1, "max": 3600}],
"setLevel": [{"name": "ausgangs_level", "type": "integer", "min": 0, "max": 100}],
"trigger": [],
}
@classmethod
def is_actor(cls, device: Dict) -> bool:
"""Prüft ob Gerät ein Aktor ist"""
device_type = device.get('controllableName', device.get('uiClass', ''))
if device_type in cls.ACTOR_TYPES:
return True
commands = device.get('definition', {}).get('commands', [])
if commands:
command_names = [cmd.get('commandName', '') for cmd in commands]
actor_commands = {'open', 'close', 'on', 'off', 'up', 'down', 'setPosition', 'dim'}
if any(cmd in actor_commands for cmd in command_names):
return True
return False
@classmethod
def is_sensor(cls, device: Dict) -> bool:
"""Prüft ob Gerät ein Sensor ist"""
device_type = device.get('controllableName', device.get('uiClass', ''))
if device_type in cls.SENSOR_TYPES:
return True
states = device.get('states', [])
commands = device.get('definition', {}).get('commands', [])
if states and len(states) > 0 and len(commands) <= 1:
return True
return False
@classmethod
def extract_actor_data(cls, device: Dict) -> tuple:
"""Extrahiert Commands und States aus Aktor"""
commands = []
states = []
cmd_definitions = device.get('definition', {}).get('commands', [])
for cmd in cmd_definitions:
command_name = cmd.get('commandName', '')
cmd_params = cls.TAHOMA_COMMANDS.get(command_name, "Not in List")
if(cmd_params != "Not in List"): #only append command, if it is on of the listed commands, to prevent flooding the DB with bullshit.
command_entry = {
'command': command_name,
'parameters': []
}
for cmd_param in cmd_params:
param_detail = {'name': cmd_param.get('name', '')}
if 'type' in cmd_param:
param_detail['type'] = cmd_param['type']
if 'min' in cmd_param:
param_detail['min'] = cmd_param['min']
if 'max' in cmd_param:
param_detail['max'] = cmd_param['max']
if 'values' in cmd_param:
param_detail['values'] = cmd_param['values']
if param_detail['name']:
command_entry['parameters'].append(param_detail)
commands.append(command_entry)
# States extrahieren
state_definitions = device.get('states', [])
for state in state_definitions:
state_name = state.get('name', '')
if state_name:
state_entry = {
'name': state_name,
'type': state.get('type', 0)
}
if 'value' in state:
state_entry['current_value'] = state['value']
states.append(state_entry)
return commands, states
@classmethod
def extract_sensor_data(cls, device: Dict) -> list:
"""Extrahiert States aus Sensor"""
states = []
state_definitions = device.get('states', [])
for state in state_definitions:
state_name = state.get('name', '')
if state_name:
state_entry = {
'name': state_name,
'type': state.get('type', 0)
}
if 'value' in state:
state_entry['current_value'] = state['value']
states.append(state_entry)
return states
class TahomaModule(BaseModule):
"""
Tahoma Modul - Implementiert BaseModule Interface
Gibt nur Actors/Sensors zurück, KEINE DB-Operationen
"""
def is_enabled(self) -> bool:
"""Prüft ob Tahoma aktiviert ist"""
return (self.config.tahoma_enable and
self.config.tahoma_ip and
self.config.tahoma_token)
def discover(self) -> Tuple[List[Dict], List[Dict]]:
"""
Führt Tahoma Discovery durch
Returns:
Tuple (actors, sensors) - Listen von Dicts im vereinheitlichten Format
"""
logger.info("\n" + "=" * 60)
logger.info("TAHOMA-GERÄTE WERDEN ABGERUFEN")
logger.info("=" * 60)
actors = []
sensors = []
# TahomaAPI initialisieren
tahoma = TahomaAPI(self.config.tahoma_ip, self.config.tahoma_token)
devices = tahoma.get_devices()
if not devices:
logger.warning("Keine Tahoma-Geräte gefunden")
return actors, sensors
# Geräte gruppieren (Original-Logik)
device_groups = {}
standalone_devices = []
for device in devices:
device_url = device.get('deviceURL', '')
match = re.match(r'(.+)#(\d+)$', device_url)
if match:
base_url = match.group(1)
if base_url not in device_groups:
device_groups[base_url] = []
device_groups[base_url].append(device)
else:
standalone_devices.append(device)
# Gruppierte Geräte verarbeiten
for base_url, group_devices in device_groups.items():
main_device = None
for dev in group_devices:
if dev.get('deviceURL', '').endswith('#1'):
main_device = dev
break
if not main_device and group_devices:
main_device = group_devices[0]
main_name = main_device.get('label', 'Unbekannt') if main_device else 'Unbekannt'
for device in group_devices:
actor, sensor = self._process_device(device, main_name)
if actor:
actors.append(actor)
if sensor:
sensors.append(sensor)
# Standalone Geräte verarbeiten
for device in standalone_devices:
device_name = device.get('label', 'Unbekannt')
actor, sensor = self._process_device(device, device_name)
if actor:
actors.append(actor)
if sensor:
sensors.append(sensor)
logger.info(f"Tahoma: {len(actors)} Aktoren, {len(sensors)} Sensoren gefunden")
return actors, sensors
def _process_device(self, device: Dict, device_name: str) -> Tuple[Optional[Dict], Optional[Dict]]:
"""
Verarbeitet ein einzelnes Gerät
Returns:
Tuple (actor_dict or None, sensor_dict or None)
"""
device_url = device.get('deviceURL', '')
device_type = device.get('controllableName', device.get('uiClass', 'Unknown'))
is_actor = DeviceClassifier.is_actor(device)
is_sensor = DeviceClassifier.is_sensor(device)
actor = None
sensor = None
if is_actor:
commands, states = DeviceClassifier.extract_actor_data(device)
actor = {
'type': device_type,
'name': device_name,
'url': device_url,
'commands': commands,
'states': states
}
elif is_sensor:
states = DeviceClassifier.extract_sensor_data(device)
sensor = {
'type': device_type,
'name': device_name,
'url': device_url,
'states': states
}
return actor, sensor
#!/usr/bin/env python3
"""
Tahoma Module
Enthält NUR Tahoma-spezifische Geräte-Discovery Logik
KEINE Datenbank-Operationen!
"""
import requests
import urllib3
import re
import logging
from typing import List, Dict, Optional, Tuple
from modules.base_module import BaseModule
# SSL-Warnungen deaktivieren
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
logger = logging.getLogger(__name__)
class TahomaAPI:
"""Original TahomaAPI Klasse - unverändert"""
def __init__(self, gateway_ip: str, api_token: str):
self.base_url = f"https://{gateway_ip}:8443/enduser-mobile-web/1/enduserAPI"
self.headers = {
"Authorization": f"Bearer {api_token}",
"Content-Type": "application/json"
}
def get_setup(self) -> Optional[Dict]:
try:
url = f"{self.base_url}/setup"
response = requests.get(url, headers=self.headers, verify=False, timeout=10)
response.raise_for_status()
return response.json()
except requests.exceptions.RequestException as e:
logger.error(f"Fehler beim Abrufen der Setup-Daten: {e}")
return None
def get_devices(self) -> List[Dict]:
setup = self.get_setup()
if not setup:
return []
devices = setup.get('devices', [])
logger.info(f"{len(devices)} Tahoma-Geräte gefunden")
return devices
class DeviceClassifier:
"""Original DeviceClassifier - unverändert"""
ACTOR_TYPES = {
'RollerShutter', 'ExteriorScreen', 'Awning', 'Blind',
'GarageDoor', 'Window', 'Light', 'OnOff', 'DimmableLight',
'HeatingSystem', 'Valve', 'Switch', 'Door', 'Curtain',
'VenetianBlind', 'PergolaScreen'
}
SENSOR_TYPES = {
'TemperatureSensor', 'LightSensor', 'HumiditySensor',
'ContactSensor', 'OccupancySensor', 'SmokeSensor',
'WaterDetectionSensor', 'WindowHandle', 'MotionSensor',
'SunSensor', 'WindSensor', 'RainSensor', 'ConsumptionSensor'
}
# Tahoma Commands mit Parametern
TAHOMA_COMMANDS = {
"setClosure": [{"name": "position", "type": "integer", "min": 0, "max": 100}],
"setClosureAndOrientation": [
{"name": "position", "type": "integer", "min": 0, "max": 100},
{"name": "neigung", "type": "integer", "min": 0, "max": 100}
],
"setOrientation": [{"name": "neigung", "type": "integer", "min": 0, "max": 100}],
"up": [], "down": [], "my": [], "stop": [], "refresh": [], "wink":[],
"setMyPosition": [{"name": "position", "type": "integer", "min": 0, "max": 100}],
"on": [], "off": [], "toggle": [],
"setIntensity": [{"name": "helligkeit", "type": "integer", "min": 0, "max": 100}],
"setColor": [
{"name": "farbton", "type": "integer", "min": 0, "max": 360},
{"name": "sättigung", "type": "integer", "min": 0, "max": 100}
],
"setColorTemperature": [{"name": "farbtemperatur", "type": "integer", "min": 2000, "max": 6500}],
"setTargetTemperature": [{"name": "temperatur", "type": "float", "min": 5.0, "max": 30.0}],
"setMode": [{"name": "betriebsart", "type": "string"}],
"pulse": [{"name": "impuls_dauer", "type": "integer", "min": 1, "max": 3600}],
"setLevel": [{"name": "ausgangs_level", "type": "integer", "min": 0, "max": 100}],
"trigger": [],
}
@classmethod
def is_actor(cls, device: Dict) -> bool:
"""Prüft ob Gerät ein Aktor ist"""
device_type = device.get('controllableName', device.get('uiClass', ''))
if device_type in cls.ACTOR_TYPES:
return True
commands = device.get('definition', {}).get('commands', [])
if commands:
command_names = [cmd.get('commandName', '') for cmd in commands]
actor_commands = {'open', 'close', 'on', 'off', 'up', 'down', 'setPosition', 'dim'}
if any(cmd in actor_commands for cmd in command_names):
return True
return False
@classmethod
def is_sensor(cls, device: Dict) -> bool:
"""Prüft ob Gerät ein Sensor ist"""
device_type = device.get('controllableName', device.get('uiClass', ''))
if device_type in cls.SENSOR_TYPES:
return True
states = device.get('states', [])
commands = device.get('definition', {}).get('commands', [])
if states and len(states) > 0 and len(commands) <= 1:
return True
return False
@classmethod
def extract_actor_data(cls, device: Dict) -> tuple:
"""Extrahiert Commands und States aus Aktor"""
commands = []
states = []
cmd_definitions = device.get('definition', {}).get('commands', [])
for cmd in cmd_definitions:
command_name = cmd.get('commandName', '')
cmd_params = cls.TAHOMA_COMMANDS.get(command_name, "Not in List")
if(cmd_params != "Not in List"): #only append command, if it is on of the listed commands, to prevent flooding the DB with bullshit.
command_entry = {
'command': command_name,
'parameters': []
}
for cmd_param in cmd_params:
param_detail = {'name': cmd_param.get('name', '')}
if 'type' in cmd_param:
param_detail['type'] = cmd_param['type']
if 'min' in cmd_param:
param_detail['min'] = cmd_param['min']
if 'max' in cmd_param:
param_detail['max'] = cmd_param['max']
if 'values' in cmd_param:
param_detail['values'] = cmd_param['values']
if param_detail['name']:
command_entry['parameters'].append(param_detail)
commands.append(command_entry)
# States extrahieren
state_definitions = device.get('states', [])
for state in state_definitions:
state_name = state.get('name', '')
if state_name:
state_entry = {
'name': state_name,
'type': state.get('type', 0)
}
if 'value' in state:
state_entry['current_value'] = state['value']
states.append(state_entry)
return commands, states
@classmethod
def extract_sensor_data(cls, device: Dict) -> list:
"""Extrahiert States aus Sensor"""
states = []
state_definitions = device.get('states', [])
for state in state_definitions:
state_name = state.get('name', '')
if state_name:
state_entry = {
'name': state_name,
'type': state.get('type', 0)
}
if 'value' in state:
state_entry['current_value'] = state['value']
states.append(state_entry)
return states
class TahomaModule(BaseModule):
"""
Tahoma Modul - Implementiert BaseModule Interface
Gibt nur Actors/Sensors zurück, KEINE DB-Operationen
"""
def is_enabled(self) -> bool:
"""Prüft ob Tahoma aktiviert ist"""
return (self.config.tahoma_enable and
self.config.tahoma_ip and
self.config.tahoma_token)
def discover(self) -> Tuple[List[Dict], List[Dict]]:
"""
Führt Tahoma Discovery durch
Returns:
Tuple (actors, sensors) - Listen von Dicts im vereinheitlichten Format
"""
logger.info("\n" + "=" * 60)
logger.info("TAHOMA-GERÄTE WERDEN ABGERUFEN")
logger.info("=" * 60)
actors = []
sensors = []
# TahomaAPI initialisieren
tahoma = TahomaAPI(self.config.tahoma_ip, self.config.tahoma_token)
devices = tahoma.get_devices()
if not devices:
logger.warning("Keine Tahoma-Geräte gefunden")
return actors, sensors
# Geräte gruppieren (Original-Logik)
device_groups = {}
standalone_devices = []
for device in devices:
device_url = device.get('deviceURL', '')
match = re.match(r'(.+)#(\d+)$', device_url)
if match:
base_url = match.group(1)
if base_url not in device_groups:
device_groups[base_url] = []
device_groups[base_url].append(device)
else:
standalone_devices.append(device)
# Gruppierte Geräte verarbeiten
for base_url, group_devices in device_groups.items():
main_device = None
for dev in group_devices:
if dev.get('deviceURL', '').endswith('#1'):
main_device = dev
break
if not main_device and group_devices:
main_device = group_devices[0]
main_name = main_device.get('label', 'Unbekannt') if main_device else 'Unbekannt'
for device in group_devices:
actor, sensor = self._process_device(device, main_name)
if actor:
actors.append(actor)
if sensor:
sensors.append(sensor)
# Standalone Geräte verarbeiten
for device in standalone_devices:
device_name = device.get('label', 'Unbekannt')
actor, sensor = self._process_device(device, device_name)
if actor:
actors.append(actor)
if sensor:
sensors.append(sensor)
logger.info(f"Tahoma: {len(actors)} Aktoren, {len(sensors)} Sensoren gefunden")
return actors, sensors
def _process_device(self, device: Dict, device_name: str) -> Tuple[Optional[Dict], Optional[Dict]]:
"""
Verarbeitet ein einzelnes Gerät
Returns:
Tuple (actor_dict or None, sensor_dict or None)
"""
device_url = device.get('deviceURL', '')
device_type = device.get('controllableName', device.get('uiClass', 'Unknown'))
is_actor = DeviceClassifier.is_actor(device)
is_sensor = DeviceClassifier.is_sensor(device)
actor = None
sensor = None
if is_actor:
commands, states = DeviceClassifier.extract_actor_data(device)
actor = {
'type': device_type,
'name': device_name,
'url': device_url,
'commands': commands,
'states': states
}
elif is_sensor:
states = DeviceClassifier.extract_sensor_data(device)
sensor = {
'type': device_type,
'name': device_name,
'url': device_url,
'states': states
}
return actor, sensor
+313 -313
View File
@@ -1,313 +1,313 @@
#!/usr/bin/env python3
"""
WLED Module
Enthält NUR WLED-spezifische Geräte-Discovery Logik
KEINE Datenbank-Operationen!
"""
import requests
import socket
import concurrent.futures
import logging
from typing import List, Dict, Optional, Tuple
from modules.base_module import BaseModule
logger = logging.getLogger(__name__)
class WLEDDiscovery:
"""Original WLEDDiscovery Klasse - unverändert"""
@staticmethod
def discover_devices(timeout: int = 5) -> List[str]:
"""Sucht nach WLED-Geräten via mDNS"""
try:
from zeroconf import ServiceBrowser, ServiceListener, Zeroconf
import time
class WLEDListener(ServiceListener):
def __init__(self):
self.devices = []
def add_service(self, zc, type_, name):
info = zc.get_service_info(type_, name)
if info:
addresses = [socket.inet_ntoa(addr) for addr in info.addresses]
for addr in addresses:
if addr not in self.devices:
self.devices.append(addr)
logger.info(f"WLED-Gerät gefunden: {name} ({addr})")
def remove_service(self, zc, type_, name):
pass
def update_service(self, zc, type_, name):
pass
zeroconf = Zeroconf()
listener = WLEDListener()
browser = ServiceBrowser(zeroconf, "_http._tcp.local.", listener)
logger.info(f"Suche nach WLED-Geräten (Timeout: {timeout}s)...")
time.sleep(timeout)
zeroconf.close()
# Nur WLED-Geräte filtern
wled_devices = []
for ip in listener.devices:
if WLEDDiscovery.is_wled_device(ip):
wled_devices.append(ip)
logger.info(f"{len(wled_devices)} WLED-Geräte gefunden")
return wled_devices
except ImportError:
logger.warning("zeroconf nicht installiert. Verwende Netzwerk-Scan...")
return WLEDDiscovery.scan_network()
except Exception as e:
logger.error(f"Fehler bei WLED-Discovery: {e}")
return []
@staticmethod
def scan_network(network: str = None, max_threads: int = 50) -> List[str]:
"""Scannt das Netzwerk nach WLED-Geräten"""
if network is None:
try:
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.connect(("8.8.8.8", 80))
local_ip = s.getsockname()[0]
s.close()
network_prefix = '.'.join(local_ip.split('.')[:-1])
except:
logger.warning("Konnte lokale IP nicht ermitteln, verwende 192.168.1.x")
network_prefix = "192.168.1"
else:
network_prefix = '.'.join(network.split('.')[:3])
logger.info(f"Scanne Netzwerk {network_prefix}.0/24 nach WLED-Geräten...")
def check_ip(ip):
if WLEDDiscovery.is_wled_device(ip):
return ip
return None
wled_devices = []
with concurrent.futures.ThreadPoolExecutor(max_workers=max_threads) as executor:
futures = [executor.submit(check_ip, f"{network_prefix}.{i}")
for i in range(1, 255)]
for future in concurrent.futures.as_completed(futures):
result = future.result()
if result:
wled_devices.append(result)
logger.info(f"WLED-Gerät gefunden: {result}")
return wled_devices
@staticmethod
def is_wled_device(ip: str, timeout: float = 1.0) -> bool:
"""Prüft ob IP ein WLED-Gerät ist"""
try:
response = requests.get(
f"http://{ip}/json/info",
timeout=timeout,
headers={'User-Agent': 'DeviceDiscovery/1.0'}
)
if response.status_code == 200:
data = response.json()
return 'ver' in data or 'name' in data
except:
pass
return False
class WLEDAPI:
"""Original WLEDAPI Klasse - unverändert"""
def __init__(self, ip: str):
self.ip = ip
self.base_url = f"http://{ip}"
def get_info(self) -> Optional[Dict]:
try:
response = requests.get(f"{self.base_url}/json/info", timeout=2)
response.raise_for_status()
return response.json()
except Exception as e:
logger.error(f"Fehler beim Abrufen der WLED-Info von {self.ip}: {e}")
return None
def get_presets(self) -> Optional[List]:
try:
response = requests.get(f"{self.base_url}/presets.json", timeout=2)
response.raise_for_status()
presets_data = response.json()
preset_list = []
if isinstance(presets_data, dict):
for preset_id, preset_data in presets_data.items():
preset_name = preset_data.get('n', f'Preset {preset_id}')
preset_list.append({int(preset_id): preset_name})
return preset_list
except Exception as e:
logger.error(f"Fehler beim Abrufen der WLED-Presets von {self.ip}: {e}")
return None
def get_effects(self) -> Optional[List]:
try:
response = requests.get(f"{self.base_url}/json/eff", timeout=2)
response.raise_for_status()
eff_data = response.json()
eff_list = []
for eff_id, eff_name in enumerate(eff_data):
if not eff_name:
eff_name = f"Effect {eff_id}"
eff_list.append({int(eff_id): eff_name})
return eff_list
except Exception as e:
logger.error(f"Fehler beim Abrufen der WLED-Effects von {self.ip}: {e}")
return None
def get_state(self) -> Optional[Dict]:
try:
response = requests.get(f"{self.base_url}/json/state", timeout=2)
response.raise_for_status()
return response.json()
except Exception as e:
logger.error(f"Fehler beim Abrufen des WLED-State von {self.ip}: {e}")
return None
def get_device_data(self) -> Optional[Dict]:
"""Erstellt Geräte-Dict im vereinheitlichten Format"""
info = self.get_info()
state = self.get_state()
if not info:
return None
name = info.get('name', f"WLED {self.ip}")
preset_values = self.get_presets()
eff_values = self.get_effects()
commands = [
{'command': 'on', 'parameters': []},
{'command': 'off', 'parameters': []},
{
'command': 'setBrightness',
'parameters': [
{'name': 'brightness', 'type': 'integer', 'min': 0, 'max': 255}
]
},
{
'command': 'setColor',
'parameters': [
{'name': 'red', 'type': 'integer', 'min': 0, 'max': 255},
{'name': 'green', 'type': 'integer', 'min': 0, 'max': 255},
{'name': 'blue', 'type': 'integer', 'min': 0, 'max': 255}
]
},
{
'command': 'setEffect',
'parameters': [
{'name': 'effect', 'type': 'integer', 'min': 0, 'max': 255, 'values': eff_values}
]
},
{
'command': 'setPreset',
'parameters': [
{'name': 'preset', 'type': 'integer', 'min': 1, 'max': 250, 'values': preset_values}
]
}
]
states = []
if state:
states.append({
'name': 'power',
'type': 'boolean',
'current_value': state.get('on', False)
})
states.append({
'name': 'brightness',
'type': 'integer',
'current_value': state.get('bri', 0)
})
segments = state.get('seg', [])
if segments and len(segments) > 0:
colors = segments[0].get('col', [[0,0,0]])
if colors and len(colors) > 0:
states.append({
'name': 'color_rgb',
'type': 'array',
'current_value': colors[0]
})
return {
'type': 'WLED',
'name': name,
'url': f"wled://{self.ip}",
'commands': commands,
'states': states
}
class WLEDModule(BaseModule):
"""
WLED Modul - Implementiert BaseModule Interface
Gibt nur Actors zurück, KEINE DB-Operationen
"""
def is_enabled(self) -> bool:
"""Prüft ob WLED aktiviert ist"""
return self.config.wled_enable
def discover(self) -> Tuple[List[Dict], List[Dict]]:
"""
Führt WLED Discovery durch
Returns:
Tuple (actors, sensors) - WLED sind immer Actors
"""
logger.info("\n" + "=" * 60)
logger.info("WLED-GERÄTE WERDEN GESUCHT")
logger.info("=" * 60)
actors = []
sensors = []
# Discovery
wled_ips = WLEDDiscovery.discover_devices(timeout=self.config.wled_discovery_timeout)
# Manuelle IPs hinzufügen
if self.config.wled_manual_ips:
logger.info(f"Füge {len(self.config.wled_manual_ips)} manuelle WLED-IPs hinzu...")
for manual_ip in self.config.wled_manual_ips:
if manual_ip not in wled_ips:
if WLEDDiscovery.is_wled_device(manual_ip):
wled_ips.append(manual_ip)
logger.info(f"✓ Manuelles WLED-Gerät: {manual_ip}")
else:
logger.warning(f"{manual_ip} ist kein WLED-Gerät")
if not wled_ips:
logger.info("Keine WLED-Geräte gefunden")
return actors, sensors
logger.info(f"{len(wled_ips)} WLED-Geräte gefunden")
# Gerätedaten abrufen
for ip in wled_ips:
try:
wled = WLEDAPI(ip)
device_data = wled.get_device_data()
if device_data:
actors.append(device_data)
else:
logger.warning(f"⚠ Konnte keine Daten von WLED {ip} abrufen")
except Exception as e:
logger.error(f"✗ Fehler beim Verarbeiten von WLED {ip}: {e}")
logger.info(f"WLED: {len(actors)} Aktoren gefunden")
return actors, sensors
#!/usr/bin/env python3
"""
WLED Module
Enthält NUR WLED-spezifische Geräte-Discovery Logik
KEINE Datenbank-Operationen!
"""
import requests
import socket
import concurrent.futures
import logging
from typing import List, Dict, Optional, Tuple
from modules.base_module import BaseModule
logger = logging.getLogger(__name__)
class WLEDDiscovery:
"""Original WLEDDiscovery Klasse - unverändert"""
@staticmethod
def discover_devices(timeout: int = 5) -> List[str]:
"""Sucht nach WLED-Geräten via mDNS"""
try:
from zeroconf import ServiceBrowser, ServiceListener, Zeroconf
import time
class WLEDListener(ServiceListener):
def __init__(self):
self.devices = []
def add_service(self, zc, type_, name):
info = zc.get_service_info(type_, name)
if info:
addresses = [socket.inet_ntoa(addr) for addr in info.addresses]
for addr in addresses:
if addr not in self.devices:
self.devices.append(addr)
logger.info(f"WLED-Gerät gefunden: {name} ({addr})")
def remove_service(self, zc, type_, name):
pass
def update_service(self, zc, type_, name):
pass
zeroconf = Zeroconf()
listener = WLEDListener()
browser = ServiceBrowser(zeroconf, "_http._tcp.local.", listener)
logger.info(f"Suche nach WLED-Geräten (Timeout: {timeout}s)...")
time.sleep(timeout)
zeroconf.close()
# Nur WLED-Geräte filtern
wled_devices = []
for ip in listener.devices:
if WLEDDiscovery.is_wled_device(ip):
wled_devices.append(ip)
logger.info(f"{len(wled_devices)} WLED-Geräte gefunden")
return wled_devices
except ImportError:
logger.warning("zeroconf nicht installiert. Verwende Netzwerk-Scan...")
return WLEDDiscovery.scan_network()
except Exception as e:
logger.error(f"Fehler bei WLED-Discovery: {e}")
return []
@staticmethod
def scan_network(network: str = None, max_threads: int = 50) -> List[str]:
"""Scannt das Netzwerk nach WLED-Geräten"""
if network is None:
try:
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.connect(("8.8.8.8", 80))
local_ip = s.getsockname()[0]
s.close()
network_prefix = '.'.join(local_ip.split('.')[:-1])
except:
logger.warning("Konnte lokale IP nicht ermitteln, verwende 192.168.1.x")
network_prefix = "192.168.1"
else:
network_prefix = '.'.join(network.split('.')[:3])
logger.info(f"Scanne Netzwerk {network_prefix}.0/24 nach WLED-Geräten...")
def check_ip(ip):
if WLEDDiscovery.is_wled_device(ip):
return ip
return None
wled_devices = []
with concurrent.futures.ThreadPoolExecutor(max_workers=max_threads) as executor:
futures = [executor.submit(check_ip, f"{network_prefix}.{i}")
for i in range(1, 255)]
for future in concurrent.futures.as_completed(futures):
result = future.result()
if result:
wled_devices.append(result)
logger.info(f"WLED-Gerät gefunden: {result}")
return wled_devices
@staticmethod
def is_wled_device(ip: str, timeout: float = 1.0) -> bool:
"""Prüft ob IP ein WLED-Gerät ist"""
try:
response = requests.get(
f"http://{ip}/json/info",
timeout=timeout,
headers={'User-Agent': 'DeviceDiscovery/1.0'}
)
if response.status_code == 200:
data = response.json()
return 'ver' in data or 'name' in data
except:
pass
return False
class WLEDAPI:
"""Original WLEDAPI Klasse - unverändert"""
def __init__(self, ip: str):
self.ip = ip
self.base_url = f"http://{ip}"
def get_info(self) -> Optional[Dict]:
try:
response = requests.get(f"{self.base_url}/json/info", timeout=2)
response.raise_for_status()
return response.json()
except Exception as e:
logger.error(f"Fehler beim Abrufen der WLED-Info von {self.ip}: {e}")
return None
def get_presets(self) -> Optional[List]:
try:
response = requests.get(f"{self.base_url}/presets.json", timeout=2)
response.raise_for_status()
presets_data = response.json()
preset_list = []
if isinstance(presets_data, dict):
for preset_id, preset_data in presets_data.items():
preset_name = preset_data.get('n', f'Preset {preset_id}')
preset_list.append({int(preset_id): preset_name})
return preset_list
except Exception as e:
logger.error(f"Fehler beim Abrufen der WLED-Presets von {self.ip}: {e}")
return None
def get_effects(self) -> Optional[List]:
try:
response = requests.get(f"{self.base_url}/json/eff", timeout=2)
response.raise_for_status()
eff_data = response.json()
eff_list = []
for eff_id, eff_name in enumerate(eff_data):
if not eff_name:
eff_name = f"Effect {eff_id}"
eff_list.append({int(eff_id): eff_name})
return eff_list
except Exception as e:
logger.error(f"Fehler beim Abrufen der WLED-Effects von {self.ip}: {e}")
return None
def get_state(self) -> Optional[Dict]:
try:
response = requests.get(f"{self.base_url}/json/state", timeout=2)
response.raise_for_status()
return response.json()
except Exception as e:
logger.error(f"Fehler beim Abrufen des WLED-State von {self.ip}: {e}")
return None
def get_device_data(self) -> Optional[Dict]:
"""Erstellt Geräte-Dict im vereinheitlichten Format"""
info = self.get_info()
state = self.get_state()
if not info:
return None
name = info.get('name', f"WLED {self.ip}")
preset_values = self.get_presets()
eff_values = self.get_effects()
commands = [
{'command': 'on', 'parameters': []},
{'command': 'off', 'parameters': []},
{
'command': 'setBrightness',
'parameters': [
{'name': 'brightness', 'type': 'integer', 'min': 0, 'max': 255}
]
},
{
'command': 'setColor',
'parameters': [
{'name': 'red', 'type': 'integer', 'min': 0, 'max': 255},
{'name': 'green', 'type': 'integer', 'min': 0, 'max': 255},
{'name': 'blue', 'type': 'integer', 'min': 0, 'max': 255}
]
},
{
'command': 'setEffect',
'parameters': [
{'name': 'effect', 'type': 'integer', 'min': 0, 'max': 255, 'values': eff_values}
]
},
{
'command': 'setPreset',
'parameters': [
{'name': 'preset', 'type': 'integer', 'min': 1, 'max': 250, 'values': preset_values}
]
}
]
states = []
if state:
states.append({
'name': 'power',
'type': 'boolean',
'current_value': state.get('on', False)
})
states.append({
'name': 'brightness',
'type': 'integer',
'current_value': state.get('bri', 0)
})
segments = state.get('seg', [])
if segments and len(segments) > 0:
colors = segments[0].get('col', [[0,0,0]])
if colors and len(colors) > 0:
states.append({
'name': 'color_rgb',
'type': 'array',
'current_value': colors[0]
})
return {
'type': 'WLED',
'name': name,
'url': f"wled://{self.ip}",
'commands': commands,
'states': states
}
class WLEDModule(BaseModule):
"""
WLED Modul - Implementiert BaseModule Interface
Gibt nur Actors zurück, KEINE DB-Operationen
"""
def is_enabled(self) -> bool:
"""Prüft ob WLED aktiviert ist"""
return self.config.wled_enable
def discover(self) -> Tuple[List[Dict], List[Dict]]:
"""
Führt WLED Discovery durch
Returns:
Tuple (actors, sensors) - WLED sind immer Actors
"""
logger.info("\n" + "=" * 60)
logger.info("WLED-GERÄTE WERDEN GESUCHT")
logger.info("=" * 60)
actors = []
sensors = []
# Discovery
wled_ips = WLEDDiscovery.discover_devices(timeout=self.config.wled_discovery_timeout)
# Manuelle IPs hinzufügen
if self.config.wled_manual_ips:
logger.info(f"Füge {len(self.config.wled_manual_ips)} manuelle WLED-IPs hinzu...")
for manual_ip in self.config.wled_manual_ips:
if manual_ip not in wled_ips:
if WLEDDiscovery.is_wled_device(manual_ip):
wled_ips.append(manual_ip)
logger.info(f"✓ Manuelles WLED-Gerät: {manual_ip}")
else:
logger.warning(f"{manual_ip} ist kein WLED-Gerät")
if not wled_ips:
logger.info("Keine WLED-Geräte gefunden")
return actors, sensors
logger.info(f"{len(wled_ips)} WLED-Geräte gefunden")
# Gerätedaten abrufen
for ip in wled_ips:
try:
wled = WLEDAPI(ip)
device_data = wled.get_device_data()
if device_data:
actors.append(device_data)
else:
logger.warning(f"⚠ Konnte keine Daten von WLED {ip} abrufen")
except Exception as e:
logger.error(f"✗ Fehler beim Verarbeiten von WLED {ip}: {e}")
logger.info(f"WLED: {len(actors)} Aktoren gefunden")
return actors, sensors