modify filemode for linux
This commit is contained in:
@@ -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
|
||||
Reference in New Issue
Block a user