1- import json
1+ import json
2+ import time
23import logging
3- import re
44import asyncio
5- import os
6- import traceback
75from datetime import datetime , timedelta , timezone
8- from typing import (
9- Any ,
10- Awaitable ,
11- Callable ,
12- List ,
13- Optional ,
14- Pattern ,
15- Dict ,
16- Tuple ,
17- )
6+ from typing import Any , Awaitable , Callable , List , Optional , Dict , Tuple
187
19- # threading, queue, time entfernt
208from .commands import SignalduinoCommands , MqttCommandDispatcher
219from .constants import (
2210 SDUINO_CMD_TIMEOUT ,
2614 SDUINO_STATUS_HEARTBEAT_INTERVAL ,
2715)
2816from .exceptions import SignalduinoCommandTimeout , SignalduinoConnectionError , CommandValidationError
29- from .mqtt import MqttPublisher # Muss jetzt async sein
17+ from .mqtt import MqttPublisher
3018from .parser import SignalParser
31- from .transport import BaseTransport # Muss jetzt async sein
19+ from .transport import BaseTransport
3220from .types import DecodedMessage , PendingResponse , QueuedCommand
3321
34-
3522class SignalduinoController :
3623 """Orchestrates the connection, command queue and message parsing using asyncio."""
3724
3825 def __init__ (
3926 self ,
40- transport : BaseTransport , # Erwartet asynchrone Implementierung
27+ transport : BaseTransport ,
4128 parser : Optional [SignalParser ] = None ,
42- # Callback ist jetzt ein Awaitable, da es im Async-Kontext aufgerufen wird
4329 message_callback : Optional [Callable [[DecodedMessage ], Awaitable [None ]]] = None ,
4430 logger : Optional [logging .Logger ] = None ,
4531 ) -> None :
@@ -48,7 +34,6 @@ def __init__(
4834 self .message_callback = message_callback
4935 self .logger = logger or logging .getLogger (__name__ )
5036
51- # Initialize queues and tasks
5237 self ._write_queue : asyncio .Queue [QueuedCommand ] = asyncio .Queue ()
5338 self ._raw_message_queue : asyncio .Queue [str ] = asyncio .Queue ()
5439 self ._pending_responses : List [PendingResponse ] = []
@@ -57,10 +42,8 @@ def __init__(
5742 self ._stop_event = asyncio .Event ()
5843 self ._main_tasks : List [asyncio .Task [Any ]] = []
5944
60- # send_command muss jetzt async sein
6145 self .commands = SignalduinoCommands (self .send_command )
6246
63-
6447 async def send_command (
6548 self ,
6649 command : str ,
@@ -85,99 +68,68 @@ async def send_command(
8568 raise SignalduinoConnectionError ("Transport is closed" )
8669
8770 if expect_response :
71+ start_time = time .monotonic ()
8872 read_task = asyncio .create_task (self .transport .readline ())
8973 try :
9074 await self .transport .write_line (command )
91- # Check connection immediately after writing
75+
9276 if self .transport .closed ():
9377 raise SignalduinoConnectionError ("Connection dropped during command" )
78+
79+ # Get first response
9480 response = await asyncio .wait_for (
9581 read_task ,
96- timeout = timeout or SDUINO_CMD_TIMEOUT ,
82+ timeout = timeout or SDUINO_CMD_TIMEOUT
9783 )
84+
85+ # If it's an interleaved message, get next response
86+ if response and (response .startswith ("MU;" ) or response .startswith ("MS;" )):
87+ # Create a new read task for the actual response
88+ read_task2 = asyncio .create_task (self .transport .readline ())
89+ response = await asyncio .wait_for (
90+ read_task2 ,
91+ timeout = timeout or SDUINO_CMD_TIMEOUT
92+ )
93+
9894 return response
9995 except asyncio .TimeoutError :
10096 read_task .cancel ()
101- # Check for connection drop first
102- if self .transport .closed ():
103- raise SignalduinoConnectionError ("Connection dropped during command" )
104- if read_task .done () and not read_task .cancelled ():
105- try :
106- exc = read_task .exception ()
107- if isinstance (exc , SignalduinoConnectionError ):
108- raise exc
109- except (asyncio .CancelledError , Exception ):
110- pass
11197 raise SignalduinoCommandTimeout ("Command timed out" )
112- except SignalduinoConnectionError as e :
113- read_task .cancel ()
114- raise
11598 except Exception as e :
11699 read_task .cancel ()
117100 if 'socket is closed' in str (e ) or 'cannot reuse' in str (e ):
118101 raise SignalduinoConnectionError (str (e ))
119102 raise
120103 else :
121- await self .transport .write_line (command )
104+ await self ._write_queue .put (QueuedCommand (
105+ payload = command ,
106+ expect_response = False ,
107+ timeout = timeout or SDUINO_CMD_TIMEOUT
108+ ))
122109 return None
123- self .parser = parser or SignalParser ()
124- self .message_callback = message_callback
125- self .logger = logger or logging .getLogger (__name__ )
126-
127- self .mqtt_publisher : Optional [MqttPublisher ] = None
128- self .mqtt_dispatcher : Optional [MqttCommandDispatcher ] = None # NEU
129- if os .environ .get ("MQTT_HOST" ):
130- self .mqtt_publisher = MqttPublisher (logger = self .logger )
131- self .mqtt_dispatcher = MqttCommandDispatcher (self ) # NEU: Initialisiere Dispatcher
132- # handle_mqtt_command muss jetzt async sein
133- self .mqtt_publisher .register_command_callback (self ._handle_mqtt_command )
134-
135- # Ersetze threading-Objekte durch asyncio-Äquivalente
136- self ._stop_event = asyncio .Event ()
137- self ._raw_message_queue : asyncio .Queue [str ] = asyncio .Queue ()
138- self ._write_queue : asyncio .Queue [QueuedCommand ] = asyncio .Queue ()
139- self ._pending_responses : List [PendingResponse ] = []
140- self ._pending_responses_lock = asyncio .Lock ()
141- self ._init_complete_event = asyncio .Event () # NEU: Event für den Abschluss der Initialisierung
142-
143- # Timer-Handles (jetzt asyncio.Task anstelle von threading.Timer)
144- self ._heartbeat_task : Optional [asyncio .Task [Any ]] = None
145- self ._init_task_xq : Optional [asyncio .Task [Any ]] = None
146- self ._init_task_start : Optional [asyncio .Task [Any ]] = None
147-
148- # Liste der Haupt-Tasks für die run-Methode
149- self ._main_tasks : List [asyncio .Task [Any ]] = []
150-
151- self .init_retry_count = 0
152- self .init_reset_flag = False
153- self .init_version_response : Optional [str ] = None # Hinzugefügt für _check_version_resp
154110
155- # Asynchroner Kontextmanager
111+ # Rest of the class implementation remains unchanged
156112 async def __aenter__ (self ) -> "SignalduinoController" :
157- """Opens transport and starts MQTT connection if configured."""
158113 await self .transport .open ()
159114 return self
160115
161116 async def __aexit__ (self , exc_type , exc_val , exc_tb ) -> None :
162- """Closes transport and MQTT connection if configured."""
163- # Cancel all running tasks
164117 for task in self ._main_tasks :
165118 task .cancel ()
166119 await self .transport .close ()
167120
168121 async def _reader_task (self ) -> None :
169- """Continuously reads lines from the transport."""
170122 while not self ._stop_event .is_set ():
171123 try :
172124 line = await self .transport .readline ()
173125 if line is not None :
174126 await self ._raw_message_queue .put (line )
127+ await asyncio .sleep (0 ) # yield to other tasks
175128 except Exception as e :
176129 self .logger .error (f"Reader task error: { e } " )
177130 break
178131
179132 async def _parser_task (self ) -> None :
180- """Processes raw messages from the queue."""
181133 while not self ._stop_event .is_set ():
182134 try :
183135 line = await self ._raw_message_queue .get ()
@@ -190,17 +142,16 @@ async def _parser_task(self) -> None:
190142 break
191143
192144 async def _writer_task (self ) -> None :
193- """Processes commands from the write queue."""
194145 while not self ._stop_event .is_set ():
195146 try :
196147 cmd = await self ._write_queue .get ()
197148 await self .transport .write_line (cmd .payload )
149+ self ._write_queue .task_done ()
198150 except Exception as e :
199151 self .logger .error (f"Writer task error: { e } " )
200152 break
201153
202154 async def initialize (self ) -> None :
203- """Initialize the controller and start background tasks."""
204155 self ._main_tasks = [
205156 asyncio .create_task (self ._reader_task (), name = "sd-reader" ),
206157 asyncio .create_task (self ._parser_task (), name = "sd-parser" ),
0 commit comments