IDE: refactor discovery panel / zeroconf
authorEdouard Tisserant <edouard.tisserant@gmail.com>
Mon, 18 Nov 2024 13:52:40 +0100
changeset 4039 9ff455817691
parent 4038 4b47b4ce0f12
child 4040 ea2db66b185f
IDE: refactor discovery panel / zeroconf

Bundle ZeroConf listener and interfaces changes monitor into separate module.
Prepare to extend with USB device browsing.
connectors/ZeroConfListener.py
controls/DiscoveryPanel.py
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/connectors/ZeroConfListener.py	Mon Nov 18 13:52:40 2024 +0100
@@ -0,0 +1,108 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# See COPYING file for copyrights details.
+
+import weakref
+from zeroconf import ServiceBrowser, Zeroconf, get_all_addresses
+import threading
+
+service_type = '_Beremiz._tcp.local.'
+
+class ZeroConfListenerClass:
+    def __init__(self, dialog):
+        self.dialog = weakref.ref(dialog)
+
+        self.IfacesMonitorState = None
+        self.IfacesMonitorTimer = None
+        self.Browser = None
+        self.ZeroConfInstance = None
+        self.PublishedServices = set()
+
+        self.start()
+
+    def __del__(self):
+        self.stop()
+
+    def start(self):
+        self.ZeroConfInstance = Zeroconf()
+        self.Browser = ServiceBrowser(self.ZeroConfInstance, service_type, self)
+        # Start the ifaces_monitor timer thread
+        self.IfacesMonitorTimer = threading.Timer(1.0, self.ifaces_monitor)
+        self.IfacesMonitorTimer.start()
+
+    def stop(self):
+        if self.IfacesMonitorTimer is not None:
+            self.IfacesMonitorTimer.cancel()
+            self.IfacesMonitorTimer = None
+
+        if self.Browser is not None:
+            self.Browser.cancel()
+            self.Browser = None
+
+        if  self.ZeroConfInstance is not None:
+            self.ZeroConfInstance.close()
+            self.ZeroConfInstance = None
+
+    def update_service(self, zeroconf, _type, name):
+        self.remove_service(zeroconf, _type, name)
+        self.add_service(zeroconf, _type, name)
+
+    def add_service(self, zeroconf, _type, name):
+        dialog = self.dialog()
+        if not dialog:
+            return
+
+        info = self.ZeroConfInstance.get_service_info(_type, name)
+        if info is None:
+            return
+
+        typename = info.properties.get(b"protocol", None).decode()
+        ip = str(info.parsed_addresses()[0])
+        port = info.port
+        dialog.addService(typename, ip, port, name)
+        self.PublishedServices.add(name)
+
+    def remove_service(self, zeroconf, _type, name):
+        dialog = self.dialog()
+        if not dialog:
+            return
+        
+        if name in self.PublishedServices:
+            dialog.removeService(name)
+            self.PublishedServices.discard(name)
+
+    def ifaces_monitor(self):
+        dialog = self.dialog()
+        if not dialog:
+            return
+
+        NewState = get_all_addresses()
+        OldState = self.IfacesMonitorState
+        self.IfacesMonitorState = NewState
+        do_restart = False
+        
+        if OldState is not None:
+            # detect if a new address appeared
+            for addr in NewState:
+                if addr not in OldState:
+                    do_restart = True
+                    break
+                else:
+                    OldState.remove(addr)
+            # detect if an address disappeared
+            if len(OldState) > 0:
+                do_restart = True
+
+            
+        if do_restart:
+            self.stop()
+            
+            while self.PublishedServices:
+                dialog.removeService(self.PublishedServices.pop())
+        
+            self.start()
+        else:
+            # Restart the ifaces_monitor timer thread
+            self.IfacesMonitorTimer = threading.Timer(1.0, self.ifaces_monitor)
+            self.IfacesMonitorTimer.start()
--- a/controls/DiscoveryPanel.py	Mon Nov 18 13:37:08 2024 +0100
+++ b/controls/DiscoveryPanel.py	Mon Nov 18 13:52:40 2024 +0100
@@ -24,57 +24,9 @@
 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 
 
-import socket
-import weakref
 import wx
 import wx.lib.mixins.listctrl as listmix
-from zeroconf import ServiceBrowser, Zeroconf, get_all_addresses
-
-service_type = '_Beremiz._tcp.local.'
-
-class ZeroConfListenerClass:
-    def __init__(self, dialog):
-        self.dialog = weakref.ref(dialog)
-        self.ZeroConfInstance = Zeroconf()
-        self.Browser = ServiceBrowser(self.ZeroConfInstance, service_type, self)
-
-    def clean(self):
-        if self.Browser is not None:
-            self.Browser.cancel()
-            self.Browser = None
-        if  self.ZeroConfInstance is not None:
-            self.ZeroConfInstance.close()
-            self.ZeroConfInstance = None
-
-    def __del__(self):
-        self.clean()
-
-    def update_service(self, zeroconf, _type, name):
-        self.remove_service(zeroconf, _type, name)
-        self.add_service(zeroconf, _type, name)
-
-    def add_service(self, zeroconf, _type, name):
-        info = self.ZeroConfInstance.get_service_info(_type, name)
-        if info is None:
-            return
-        typename = info.properties.get(b"protocol", None).decode()
-        ip = str(info.parsed_addresses()[0])
-        port = info.port
-
-        wx.CallAfter(self._add_service, typename, ip, port, name)
-
-    def _add_service(self, typename, ip, port, name):
-        dialog = self.dialog()
-        if not dialog: return
-        dialog._addService(typename, ip, port, name)
-
-    def remove_service(self, zeroconf, _type, name):
-        wx.CallAfter(self._remove_service, name)
-
-    def _remove_service(self, name):
-        dialog = self.dialog()
-        if not dialog: return
-        dialog._removeService(name)
+from connectors.ZeroConfListener import ZeroConfListenerClass
 
 class AutoWidthListCtrl(wx.ListCtrl, listmix.ListCtrlAutoWidthMixin):
     def __init__(self, parent, name, pos=wx.DefaultPosition,
@@ -82,7 +34,6 @@
         wx.ListCtrl.__init__(self, parent, wx.ID_ANY, pos, size, style, name=name)
         listmix.ListCtrlAutoWidthMixin.__init__(self)
 
-
 class DiscoveryPanel(wx.Panel, listmix.ColumnSorterMixin):
 
     def _init_coll_MainSizer_Items(self, parent):
@@ -118,15 +69,11 @@
         self.ServicesList = AutoWidthListCtrl(
             name='ServicesList', parent=self, pos=wx.Point(0, 0), size=wx.Size(0, 0),
             style=wx.LC_REPORT | wx.LC_EDIT_LABELS | wx.LC_SORT_ASCENDING | wx.LC_SINGLE_SEL)
-        self.ServicesList.InsertColumn(0, _('NAME'))
-        self.ServicesList.InsertColumn(1, _('TYPE'))
-        self.ServicesList.InsertColumn(2, _('IP'))
-        self.ServicesList.InsertColumn(3, _('PORT'))
-        self.ServicesList.SetColumnWidth(0, 150)
-        self.ServicesList.SetColumnWidth(1, 150)
-        self.ServicesList.SetColumnWidth(2, 150)
-        self.ServicesList.SetColumnWidth(3, 150)
+        for col, (label, width) in enumerate([
+                (_('NAME'), 150), (_('TYPE'), 150), (_('IP'), 150), (_('PORT'), 150)]):
+            self.ServicesList.InsertColumn(col, label)
         self.ServicesList.SetInitialSize(wx.Size(-1, 300))
+            
         self.Bind(wx.EVT_LIST_ITEM_SELECTED, self.OnItemSelected, self.ServicesList)
         self.Bind(wx.EVT_LIST_ITEM_ACTIVATED, self.OnItemActivated, self.ServicesList)
 
@@ -156,46 +103,27 @@
 
         self._init_ctrls(parent)
 
+        self.Bind(wx.EVT_WINDOW_DESTROY, self.OnDestroy)
+
         self.itemDataMap = {}
         self.nextItemId = 0
 
         self.URI = None
 
+        self.LatestSelection = None
+
+        self.ZeroConfListener = None
+
         self.RefreshList()
-        self.LatestSelection = None
-
-        self.IfacesMonitorState = None
-        self.IfacesMonitorTimer = wx.Timer(self)
-        self.IfacesMonitorTimer.Start(2000)
-        self.ZeroConfListener = None
-        self.Bind(wx.EVT_TIMER, self.IfacesMonitor, self.IfacesMonitorTimer)
+
 
     def _cleanup(self):
-        if self.IfacesMonitorTimer is not None:
-            self.IfacesMonitorTimer.Stop()
-            self.IfacesMonitorTimer = None
         if self.ZeroConfListener is not None:
-            self.ZeroConfListener.clean()
+            self.ZeroConfListener.stop()
             self.ZeroConfListener = None
 
-    def __del__(self):
+    def OnDestroy(self, event):
         self._cleanup()
-
-    def Destroy(self):
-        self._cleanup()
-        wx.Panel.Destroy(self)
-
-    def IfacesMonitor(self, event):
-        NewState = get_all_addresses()
-
-        if self.IfacesMonitorState != NewState:
-            if self.IfacesMonitorState is not None:
-                # refresh only if a new address appeared
-                for addr in NewState:
-                    if addr not in self.IfacesMonitorState:
-                        self.RefreshList()
-                        break
-            self.IfacesMonitorState = NewState
         event.Skip()
 
     def RefreshList(self):
@@ -246,6 +174,9 @@
             #     return str("MDNS://%s" % svcname)
         return None
 
+    def removeService(self, name):
+        wx.CallAfter(self._removeService, name)
+
     def _removeService(self, name):
         '''
         called when a service with the desired type goes offline.
@@ -263,6 +194,8 @@
                 self.ServicesList.DeleteItem(idx)
                 break
 
+    def addService(self, typename, ip, port, name):
+        wx.CallAfter(self._addService, typename, ip, port, name)
 
     def _addService(self, typename, ip, port, name):
         '''