


#!/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