1- import asyncio
21import socket
2+ from collections .abc import AsyncGenerator
33from dataclasses import dataclass , field
44from enum import Enum , IntEnum
5- from typing import Any , Dict , Tuple
5+ from typing import Any , Dict
66
7+ from anyio import Event , fail_after
8+ from jumpstarter_driver_power .driver import PowerInterface , PowerReading
79from pysnmp .carrier .asyncio .dgram import udp
810from pysnmp .entity import config , engine
911from pysnmp .entity .rfc3413 import cmdgen
@@ -36,7 +38,7 @@ class SNMPError(Exception):
3638
3739
3840@dataclass (kw_only = True )
39- class SNMPServer (Driver ):
41+ class SNMPServer (PowerInterface , Driver ):
4042 """SNMP Power Control Driver"""
4143
4244 host : str = field ()
@@ -117,7 +119,7 @@ def _setup_snmp(self):
117119 def client (cls ) -> str :
118120 return "jumpstarter_driver_snmp.client.SNMPServerClient"
119121
120- def _create_snmp_callback (self , result : Dict [str , Any ], response_received : asyncio . Event ):
122+ def _create_snmp_callback (self , result : Dict [str , Any ], response_received : Event ):
121123 def callback (snmpEngine , sendRequestHandle , errorIndication , errorStatus , errorIndex , varBinds , cbCtx ):
122124 self .logger .debug (f"Callback { errorIndication } { errorStatus } { errorIndex } { varBinds } " )
123125 if errorIndication :
@@ -138,29 +140,17 @@ def callback(snmpEngine, sendRequestHandle, errorIndication, errorStatus, errorI
138140
139141 return callback
140142
141- def _setup_event_loop (self ) -> Tuple [asyncio .AbstractEventLoop , bool ]:
142- try :
143- loop = asyncio .get_running_loop ()
144- return loop , False
145- except RuntimeError :
146- loop = asyncio .new_event_loop ()
147- asyncio .set_event_loop (loop )
148- return loop , True
149-
150- async def _run_snmp_dispatcher (self , snmp_engine : engine .SnmpEngine , response_received : asyncio .Event ):
143+ async def _run_snmp_dispatcher (self , snmp_engine : engine .SnmpEngine , response_received : Event ):
151144 snmp_engine .open_dispatcher ()
152145 await response_received .wait ()
153146 snmp_engine .close_dispatcher ()
154147
155- def _snmp_set (self , state : PowerState ):
148+ async def _snmp_set (self , state : PowerState ):
156149 result = {"success" : False , "error" : None }
157- response_received = asyncio .Event ()
158- loop = None
159- created_loop = False
150+ response_received = Event ()
160151
161152 try :
162153 self .logger .info (f"Sending power { state .name } command to { self .host } " )
163- loop , created_loop = self ._setup_event_loop ()
164154 snmp_engine = self ._setup_snmp ()
165155 callback = self ._create_snmp_callback (result , response_received )
166156 cmdgen .SetCommandGenerator ().send_varbinds (
@@ -172,10 +162,10 @@ def _snmp_set(self, state: PowerState):
172162 callback ,
173163 )
174164
175- dispatcher_task = loop .create_task (self ._run_snmp_dispatcher (snmp_engine , response_received ))
176165 try :
177- loop .run_until_complete (asyncio .wait_for (dispatcher_task , self .timeout ))
178- except asyncio .TimeoutError :
166+ with fail_after (self .timeout ):
167+ await self ._run_snmp_dispatcher (snmp_engine , response_received )
168+ except TimeoutError :
179169 self .logger .warning (f"SNMP operation timed out after { self .timeout } seconds" )
180170 result ["error" ] = "SNMP operation timed out"
181171
@@ -188,19 +178,20 @@ def _snmp_set(self, state: PowerState):
188178 error_msg = f"SNMP set failed: { str (e )} "
189179 self .logger .error (error_msg )
190180 raise SNMPError (error_msg ) from e
191- finally :
192- if created_loop and loop :
193- loop .close ()
194181
195182 @export
196- def on (self ):
183+ async def on (self ):
197184 """Turn power on"""
198- return self ._snmp_set (PowerState .ON )
185+ return await self ._snmp_set (PowerState .ON )
199186
200187 @export
201- def off (self ):
188+ async def off (self ):
202189 """Turn power off"""
203- return self ._snmp_set (PowerState .OFF )
190+ return await self ._snmp_set (PowerState .OFF )
191+
192+ @export
193+ async def read (self ) -> AsyncGenerator [PowerReading , None ]:
194+ raise NotImplementedError
204195
205196 def close (self ):
206197 """No cleanup needed since engines are created per operation"""
0 commit comments