@@ -84,6 +84,8 @@ def __init__(
8484 self ._update_in_progress = False
8585 self ._has_data = False
8686
87+ # Websocket processing is now handled by trigger system
88+
8789 # Track entities we added or removed
8890 # By unique ID rather than the objects themselves
8991 self .known_unique_ids : Set [str ] = set ()
@@ -780,193 +782,17 @@ async def _update_rule_type_in_dict(self, target_data: Dict[str, List[Any]], rul
780782
781783 @callback
782784 def _handle_websocket_message (self , message : dict [str , Any ]) -> None :
783- """Handle a message from the WebSocket connection."""
784- try :
785- if not message :
786- return
785+ """Handle a message from the WebSocket connection.
786+
787+ NOTE: This method is now primarily handled by the trigger system.
788+ Triggers detect config changes and dispatch refreshes directly via _dispatch_coordinator_refresh().
789+ This keeps the detection logic DRY and centralized in one place.
790+ """
791+ # The trigger system now handles all websocket message detection and dispatches
792+ # coordinator refreshes when config changes are detected. This method is kept
793+ # for backward compatibility but should only be called as a fallback.
794+ pass
787795
788- # Get message meta data if available
789- meta = message .get ("meta" , {})
790- msg_type = meta .get ("message" , "" )
791- msg_data = message .get ("data" , {})
792-
793- # Use string representation of message to quickly determine the message type
794- message_str = str (message ).lower ()
795-
796- # Process delete events first
797- if "delete" in message_str and ("event" in message or "events" in message_str ):
798- # This looks like a deletion event, check for IDs being removed
799- LOGGER .debug ("WebSocket deletion event: %s" , message )
800-
801- # Attempt to match deletion events to entities
802- pass
803-
804- # Check if any key event data exists (log even if we don't process it)
805- if any (key in message_str for key in ["rule" , "policy" , "route" , "forward" , "nat" , "traffic" , "port" ]):
806- # These keywords might indicate a rule-related event
807- LOGGER .debug ("WebSocket rule event: %s" , message )
808-
809- # Map keywords to rule types
810- rule_type_keywords = {
811- "firewall_policies" : ["policy" , "security" , "firewall" ],
812- "traffic_rules" : ["traffic" , "traffic_rules" ],
813- "port_forwards" : ["port" , "forward" , "nat" ],
814- "traffic_routes" : ["route" , "traffic" ],
815- "legacy_firewall_rules" : ["firewall" , "rule" , "allow" , "deny" ],
816- "qos_rules" : ["qos" , "quality" , "service" ],
817- "vpn_clients" : ["vpn" , "client" ],
818- "vpn_servers" : ["vpn" , "server" ],
819- }
820-
821- # Check if this message might relate to rule changes
822- should_refresh = False
823- refresh_reason = None
824- rule_type_affected = None
825-
826- # Configuration changes and provisioning often relate to rule updates
827- if "cfgversion" in str (message ).lower () or "provisioned" in str (message ).lower ():
828- should_refresh = True
829- refresh_reason = "Configuration version change detected"
830-
831- # Direct rule-related events
832- elif any (word in msg_type .lower () for word in ["firewall" , "rule" , "policy" , "route" , "forward" , "qos" ]):
833- should_refresh = True
834- refresh_reason = f"Rule-related event type: { msg_type } "
835-
836- # Try to determine the specific rule type affected
837- for rule_type , keywords in rule_type_keywords .items ():
838- if any (keyword in msg_type .lower () for keyword in keywords ):
839- rule_type_affected = rule_type
840- break
841-
842- # General CRUD operations that might indicate rule changes
843- elif any (op in msg_type .lower () for op in ["add" , "delete" , "update" , "remove" ]):
844- # Check if the operation relates to any rule types
845- message_str = str (message ).lower ()
846-
847- # Special handling for port forwards vs device port tables
848- if "port" in message_str :
849- # Check if this is a device port_table update (which is distinct from port forwards)
850- if "port_table" in message_str and not any (kw in message_str for kw in ["port_forward" , "portforward" , "nat" ]):
851- # Skip false positive port_table updates that aren't related to port forwarding
852- log_websocket ("Skipping CRUD operation for port_table (not related to port forwards)" )
853- return
854-
855- for rule_type , keywords in rule_type_keywords .items ():
856- if any (keyword in message_str for keyword in keywords ):
857- # For port_forwards, require more specific keywords to avoid false positives
858- if rule_type == "port_forwards" and not any (kw in message_str for kw in ["port_forward" , "portforward" , "nat" ]):
859- continue
860-
861- should_refresh = True
862- rule_type_affected = rule_type
863- refresh_reason = f"CRUD operation detected for { rule_type } "
864- break
865-
866- # Device updates - only process if they contain config changes
867- elif "device" in msg_type .lower () and "update" in msg_type .lower ():
868- # Only refresh for specific configuration changes
869- message_str = str (message ).lower ()
870- config_keywords = ["config" , "firewall" , "rule" , "policy" , "route" , "qos" ]
871-
872- # Skip updating for commonly noisy device state update patterns
873- if isinstance (msg_data , list ) and len (msg_data ) == 1 :
874- # Skip purely device state updates - these don't affect configurations
875- if set (msg_data [0 ].keys ()).issubset ({"state" , "upgrade_state" , "provisioned_at" }):
876- log_websocket ("Skipping refresh for routine device state update: %s" ,
877- set (msg_data [0 ].keys ()))
878- return
879-
880- # Check if this is specifically a port_table update (which is not related to port forwards)
881- if "port_table" in message_str and not any (kw in message_str for kw in ["port_forward" , "portforward" , "nat" ]):
882- # Skip device updates that only contain port_table information without port forwarding references
883- log_websocket ("Skipping refresh for device update with port_table (not related to port forwards)" )
884- return
885-
886- # Check if this contains configuration version changes (accept these)
887- if "cfgversion" in message_str :
888- should_refresh = True
889- refresh_reason = "Device update with configuration version change"
890- # Otherwise, be more selective about what triggers refreshes
891- elif any (keyword in message_str for keyword in config_keywords ):
892- should_refresh = True
893- refresh_reason = "Device update with potential rule changes"
894- else :
895- # Not all device updates need a refresh - skip ones without config changes
896- log_websocket ("Skipping refresh for device update without rule-related changes" )
897- return
898-
899- # Check for QoS-specific event patterns that might not be caught by other checks
900- if not should_refresh and "qos" in str (message ).lower ():
901- should_refresh = True
902- rule_type_affected = "qos_rules"
903- refresh_reason = "QoS-related event detected"
904- log_websocket ("QoS-specific event detected: %s" , msg_type )
905-
906- if should_refresh :
907- # Use a semaphore to prevent multiple concurrent refreshes
908- if not hasattr (self , '_refresh_semaphore' ):
909- self ._refresh_semaphore = asyncio .Semaphore (1 )
910-
911- # Only proceed if we can acquire the semaphore
912- if self ._refresh_semaphore .locked ():
913- log_websocket ("Skipping refresh as one is already in progress" )
914- return
915-
916- # Should prevent rapid-fire refreshes during switch operations
917- if not hasattr (self , '_min_ws_refresh_interval' ):
918- self ._min_ws_refresh_interval = 1.5
919-
920- if not hasattr (self , '_last_ws_refresh' ):
921- self ._last_ws_refresh = 0
922-
923- if not hasattr (self , '_pending_ws_refresh' ):
924- self ._pending_ws_refresh = False
925-
926- if not hasattr (self , '_ws_refresh_task' ):
927- self ._ws_refresh_task = None
928-
929- current_time = time .time ()
930- if current_time - self ._last_ws_refresh < self ._min_ws_refresh_interval :
931- log_websocket (
932- "Debouncing refresh request (last refresh was %0.1f seconds ago)" ,
933- current_time - self ._last_ws_refresh
934- )
935-
936- # Cancel any pending refresh task
937- if self ._ws_refresh_task and not self ._ws_refresh_task .done ():
938- self ._ws_refresh_task .cancel ()
939-
940- # Schedule a delayed refresh if one isn't already pending
941- if not self ._pending_ws_refresh :
942- self ._pending_ws_refresh = True
943- delay = self ._min_ws_refresh_interval - (current_time - self ._last_ws_refresh )
944-
945- async def delayed_refresh ():
946- await asyncio .sleep (delay )
947- self ._pending_ws_refresh = False
948- log_websocket ("Executing delayed refresh after debounce period" )
949- # Use the standard refresh workflow for all rule types
950- await self ._controlled_refresh_wrapper ()
951-
952- self ._ws_refresh_task = self .hass .async_create_task (delayed_refresh ())
953- return
954-
955- # Update last refresh timestamp
956- self ._last_ws_refresh = current_time
957-
958- # Log refresh events at INFO level for visibility
959- LOGGER .info ("Refreshing data due to: %s (rule type: %s)" ,
960- refresh_reason , rule_type_affected or "unknown" )
961-
962- # Use the standard refresh workflow for all rule types
963- self .hass .async_create_task (self ._controlled_refresh_wrapper ())
964- elif DEBUG_WEBSOCKET :
965- # Only log non-refreshing messages when debug is enabled
966- log_websocket ("No refresh triggered for message type: %s" , msg_type )
967- except Exception as err :
968- LOGGER .error ("Error handling websocket message: %s" , err )
969-
970796 async def _controlled_refresh_wrapper (self ):
971797 """Wrapper for the controlled refresh process to ensure proper semaphore handling."""
972798 # Acquire semaphore before starting refresh
0 commit comments