Fixed state tracking across service & esp for cleaner communication and experience

This commit is contained in:
Space-Banane
2026-01-31 19:17:56 +01:00
parent 16c28d1ea4
commit d5d106c0ab

142
listen.py
View File

@@ -16,8 +16,7 @@ from dotenv import load_dotenv
# Configure logging # Configure logging
logging.basicConfig( logging.basicConfig(
level=logging.INFO, level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
) )
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -25,18 +24,18 @@ logger = logging.getLogger(__name__)
load_dotenv() load_dotenv()
# Configuration # Configuration
ESP_IP = os.getenv('ESP_IP') ESP_IP = os.getenv("ESP_IP")
ESP_API_KEY = os.getenv('ESP_API_KEY', '') ESP_API_KEY = os.getenv("ESP_API_KEY", "")
KUMA_URL = os.getenv('KUMA_URL') KUMA_URL = os.getenv("KUMA_URL")
KUMA_KEY = os.getenv('KUMA_KEY') KUMA_KEY = os.getenv("KUMA_KEY")
POLL_INTERVAL = int(os.getenv('POLL_INTERVAL', '30')) POLL_INTERVAL = int(os.getenv("POLL_INTERVAL", "30"))
class UptimeKumaMonitor: class UptimeKumaMonitor:
"""Handles communication with Uptime Kuma API""" """Handles communication with Uptime Kuma API"""
def __init__(self, session: aiohttp.ClientSession, base_url: str, api_key: str): def __init__(self, session: aiohttp.ClientSession, base_url: str, api_key: str):
self.base_url = base_url.rstrip('/') self.base_url = base_url.rstrip("/")
self.api_key = api_key self.api_key = api_key
self.kuma = UptimeKuma(session, base_url, api_key) self.kuma = UptimeKuma(session, base_url, api_key)
@@ -63,7 +62,9 @@ class UptimeKumaMonitor:
# Check monitor_status attribute (it's an enum) # Check monitor_status attribute (it's an enum)
if monitor.monitor_status.value != 1: if monitor.monitor_status.value != 1:
all_up = False all_up = False
down_monitors.append(monitor.monitor_name or f"Monitor {monitor_id}") down_monitors.append(
monitor.monitor_name or f"Monitor {monitor_id}"
)
if down_monitors: if down_monitors:
logger.warning(f"Monitors DOWN: {', '.join(down_monitors)}") logger.warning(f"Monitors DOWN: {', '.join(down_monitors)}")
@@ -79,13 +80,15 @@ class UptimeKumaMonitor:
class ESP32Controller: class ESP32Controller:
"""Handles communication with ESP32 via ESPHome API""" """Handles communication with ESP32 via ESPHome API"""
def __init__(self, host: str, encryption_key: str = ''): def __init__(self, host: str, encryption_key: str = ""):
self.host = host self.host = host
self.encryption_key = encryption_key self.encryption_key = encryption_key
self.client: Optional[APIClient] = None self.client: Optional[APIClient] = None
self.reconnect_logic: Optional[ReconnectLogic] = None self.reconnect_logic: Optional[ReconnectLogic] = None
self.current_state = None # Track current LED state to minimize traffic self.current_state = None # Track current LED state to minimize traffic
self.light_keys = {} # Map light IDs to entity keys self.light_keys = {} # Map light IDs to entity keys
self.switch_keys = {} # Map switch IDs to entity keys
self.switch_states = {} # Track switch states
async def connect(self): async def connect(self):
"""Connect to ESP32""" """Connect to ESP32"""
@@ -93,20 +96,71 @@ class ESP32Controller:
self.client = APIClient( self.client = APIClient(
address=self.host, address=self.host,
port=6053, port=6053,
password='', password="",
noise_psk=self.encryption_key if self.encryption_key else None noise_psk=self.encryption_key if self.encryption_key else None,
) )
await self.client.connect(login=True) await self.client.connect(login=True)
logger.info(f"Connected to ESP32 at {self.host}") logger.info(f"Connected to ESP32 at {self.host}")
# Get entity list to map light IDs to keys # Get entity list to map entity IDs to keys
entities, services = await self.client.list_entities_services() entities, services = await self.client.list_entities_services()
switch_names = ['monitoring_enabled', 'force_red_led', 'force_green_led', 'quiet_hours_active']
for entity in entities: for entity in entities:
if hasattr(entity, 'object_id') and hasattr(entity, 'key'): if hasattr(entity, "object_id") and hasattr(entity, "key"):
# Separate switches from lights
if entity.object_id in switch_names:
self.switch_keys[entity.object_id] = entity.key
else:
self.light_keys[entity.object_id] = entity.key self.light_keys[entity.object_id] = entity.key
logger.info(f"Found lights: {list(self.light_keys.keys())}") logger.info(f"Found lights: {list(self.light_keys.keys())}")
logger.info(f"Found switches: {list(self.switch_keys.keys())}")
# Fetch initial states for switches
for entity in entities:
if hasattr(entity, "object_id") and entity.object_id in self.switch_keys:
if hasattr(entity, "state"):
self.switch_states[entity.object_id] = entity.state
logger.info(f"Initial state for '{entity.object_id}': {entity.state}")
# Subscribe to state changes for switches
def on_state_changed(state):
# Log all state changes for debugging
entity_name = None
entity_type = None
# Check if it's a switch
for switch_id, key in self.switch_keys.items():
if state.key == key:
self.switch_states[switch_id] = state.state
entity_name = switch_id
entity_type = "switch"
logger.info(
f"Switch '{switch_id}' changed to: {state.state}"
)
break
# Check if it's a light (for debugging)
if entity_name is None:
for light_id, key in self.light_keys.items():
if state.key == key:
entity_name = light_id
entity_type = "light"
logger.debug(
f"Light '{light_id}' state changed (key={state.key})"
)
break
# Log unknown entities
if entity_name is None:
logger.debug(f"Unknown entity state change (key={state.key})")
try:
self.client.subscribe_states(on_state_changed)
except Exception as e:
logger.error(f"Failed to subscribe to states: {e}")
# Set up reconnection logic # Set up reconnection logic
async def on_disconnect(): async def on_disconnect():
@@ -119,7 +173,7 @@ class ESP32Controller:
client=self.client, client=self.client,
on_disconnect=on_disconnect, on_disconnect=on_disconnect,
on_connect=on_connect, on_connect=on_connect,
name=self.host name=self.host,
) )
return True return True
@@ -128,9 +182,29 @@ class ESP32Controller:
logger.error(f"Failed to connect to ESP32: {e}") logger.error(f"Failed to connect to ESP32: {e}")
return False return False
def should_update_leds(self) -> bool:
"""Check if LEDs should be updated based on switch states"""
# Check if monitoring is enabled
monitoring_enabled = self.switch_states.get("monitoring_enabled", True)
if not monitoring_enabled:
logger.debug("Monitoring disabled - skipping LED update")
return False
# Check if quiet hours are active
quiet_hours = self.switch_states.get("quiet_hours", False)
if quiet_hours:
logger.debug("Quiet hours active - skipping LED update")
return False
return True
async def set_all_monitors_up(self): async def set_all_monitors_up(self):
"""Set green LED with breathing effect (all monitors UP)""" """Set green LED with breathing effect (all monitors UP)"""
if self.current_state == 'all_up': if not self.should_update_leds():
logger.info("Skipping LED update due to switch states")
return
if self.current_state == "all_up":
return # Already in correct state return # Already in correct state
if not self.client: if not self.client:
@@ -139,19 +213,19 @@ class ESP32Controller:
try: try:
# Turn off red LED (sanitized name: rote_led) # Turn off red LED (sanitized name: rote_led)
if 'rote_led' in self.light_keys: if "rote_led" in self.light_keys:
self.client.light_command(key=self.light_keys['rote_led'], state=False) self.client.light_command(key=self.light_keys["rote_led"], state=False)
# Turn on green LED with breathing effect (sanitized name: gr_ne_led) # Turn on green LED with breathing effect (sanitized name: gr_ne_led)
if 'gr_ne_led' in self.light_keys: if "gr_ne_led" in self.light_keys:
self.client.light_command( self.client.light_command(
key=self.light_keys['gr_ne_led'], key=self.light_keys["gr_ne_led"],
state=True, state=True,
brightness=1.0, brightness=1.0,
effect='Breathing' effect="Breathing",
) )
self.current_state = 'all_up' self.current_state = "all_up"
logger.info("✓ All monitors UP - Green LED breathing") logger.info("✓ All monitors UP - Green LED breathing")
except Exception as e: except Exception as e:
@@ -159,7 +233,11 @@ class ESP32Controller:
async def set_monitors_down(self): async def set_monitors_down(self):
"""Set red LED with fast heartbeat effect (monitors DOWN)""" """Set red LED with fast heartbeat effect (monitors DOWN)"""
if self.current_state == 'monitors_down': if not self.should_update_leds():
logger.info("Skipping LED update due to switch states")
return
if self.current_state == "monitors_down":
return # Already in correct state return # Already in correct state
if not self.client: if not self.client:
@@ -168,19 +246,19 @@ class ESP32Controller:
try: try:
# Turn off green LED (sanitized name: gr_ne_led) # Turn off green LED (sanitized name: gr_ne_led)
if 'gr_ne_led' in self.light_keys: if "gr_ne_led" in self.light_keys:
self.client.light_command(key=self.light_keys['gr_ne_led'], state=False) self.client.light_command(key=self.light_keys["gr_ne_led"], state=False)
# Turn on red LED with fast heartbeat effect (sanitized name: rote_led) # Turn on red LED with fast heartbeat effect (sanitized name: rote_led)
if 'rote_led' in self.light_keys: if "rote_led" in self.light_keys:
self.client.light_command( self.client.light_command(
key=self.light_keys['rote_led'], key=self.light_keys["rote_led"],
state=True, state=True,
brightness=1.0, brightness=1.0,
effect='Fast Heartbeat' effect="Fast Heartbeat",
) )
self.current_state = 'monitors_down' self.current_state = "monitors_down"
logger.warning("✗ Monitors DOWN - Red LED fast heartbeat") logger.warning("✗ Monitors DOWN - Red LED fast heartbeat")
except Exception as e: except Exception as e:
@@ -224,6 +302,10 @@ async def main():
try: try:
async with aiohttp.ClientSession() as session: async with aiohttp.ClientSession() as session:
# Wait for esp to be fully connected
logger.info("Waiting for ESP32 to stabilize...")
await asyncio.sleep(5)
logger.info("(actually) Starting Uptime Kuma monitoring...")
# Initialize Uptime Kuma monitor # Initialize Uptime Kuma monitor
kuma = UptimeKumaMonitor(session, KUMA_URL, KUMA_KEY) kuma = UptimeKumaMonitor(session, KUMA_URL, KUMA_KEY)
@@ -248,7 +330,7 @@ async def main():
await esp.disconnect() await esp.disconnect()
if __name__ == '__main__': if __name__ == "__main__":
try: try:
asyncio.run(main()) asyncio.run(main())
except KeyboardInterrupt: except KeyboardInterrupt: