power: allow indefinite remote device init

Remote devices, such as the tplink smartplug and http
based devices, may not be immediately available when
Moonraker starts.  Previously this would result in an error.

Remote switches that requiring polling for state will now
reattempt initialization indefinitely.  This behavior brings
them in line with devices that are updated asynchronously.

Signed-off-by:  Eric Callahan <arksine.code@gmail.com>
This commit is contained in:
Eric Callahan 2022-06-29 08:07:46 -04:00
parent 835e49c10e
commit 31d4335e3e
No known key found for this signature in database
GPG Key ID: 5A1EB336DFB4C71B
1 changed files with 64 additions and 41 deletions

View File

@ -11,7 +11,6 @@ import struct
import socket import socket
import asyncio import asyncio
import time import time
from tornado.escape import json_decode
# Annotation imports # Annotation imports
from typing import ( from typing import (
@ -102,30 +101,11 @@ class PrinterPower:
return pstate == "printing" return pstate == "printing"
async def component_init(self) -> None: async def component_init(self) -> None:
event_loop = self.server.get_event_loop() for dev in self.devices.values():
# Wait up to 5 seconds for the machine component to init if not dev.initialize():
machine_cmp: Machine = self.server.lookup_component("machine")
await machine_cmp.wait_for_init(5.)
cur_time = event_loop.get_loop_time()
endtime = cur_time + 120.
query_devs = list(self.devices.values())
failed_devs: List[PowerDevice] = []
while cur_time < endtime:
for dev in query_devs:
if not await dev.initialize():
failed_devs.append(dev)
if not failed_devs:
logging.debug("All power devices initialized")
return
query_devs = failed_devs
failed_devs = []
await asyncio.sleep(2.)
cur_time = event_loop.get_loop_time()
if failed_devs:
failed_names = [d.get_name() for d in failed_devs]
self.server.add_warning( self.server.add_warning(
"The following power devices failed init:" f"Power device '{dev.get_name()}' failed to initialize"
f" {failed_names}") )
def _handle_klippy_shutdown(self) -> None: def _handle_klippy_shutdown(self) -> None:
for dev in self.devices.values(): for dev in self.devices.values():
@ -209,9 +189,13 @@ class PrinterPower:
if name in self.devices: if name in self.devices:
raise self.server.error( raise self.server.error(
f"Device [{name}] already configured") f"Device [{name}] already configured")
if not await device.initialize(): success = device.initialize()
if asyncio.iscoroutine(success):
success = await success
if not success:
self.server.add_warning( self.server.add_warning(
f"Failed to initialize power device: {device.get_name()}") f"Power device '{device.get_name()}' failed to initialize"
)
return return
self.devices[name] = device self.devices[name] = device
@ -232,6 +216,7 @@ class PowerDevice:
self.type: str = config.get('type') self.type: str = config.get('type')
self.state: str = "init" self.state: str = "init"
self.request_lock = asyncio.Lock() self.request_lock = asyncio.Lock()
self.init_task: Optional[asyncio.Task] = None
self.locked_while_printing = config.getboolean( self.locked_while_printing = config.getboolean(
'locked_while_printing', False) 'locked_while_printing', False)
self.off_when_shutdown = config.getboolean('off_when_shutdown', False) self.off_when_shutdown = config.getboolean('off_when_shutdown', False)
@ -351,11 +336,12 @@ class PowerDevice:
def init_state(self) -> Optional[Coroutine]: def init_state(self) -> Optional[Coroutine]:
return None return None
async def initialize(self) -> bool: def initialize(self) -> bool:
self._setup_bound_service() self._setup_bound_service()
ret = self.init_state() ret = self.init_state()
if ret is not None: if ret is not None:
await ret eventloop = self.server.get_event_loop()
self.init_task = eventloop.create_task(ret)
return self.state != "error" return self.state != "error"
async def process_request(self, req: str) -> str: async def process_request(self, req: str) -> str:
@ -401,7 +387,10 @@ class PowerDevice:
raise NotImplementedError raise NotImplementedError
def close(self) -> Optional[Coroutine]: def close(self) -> Optional[Coroutine]:
pass if self.init_task is not None:
self.init_task.cancel()
self.init_task = None
return None
class HTTPDevice(PowerDevice): class HTTPDevice(PowerDevice):
def __init__(self, def __init__(self,
@ -422,7 +411,23 @@ class HTTPDevice(PowerDevice):
async def init_state(self) -> None: async def init_state(self) -> None:
async with self.request_lock: async with self.request_lock:
await self.refresh_status() last_err: Exception = Exception()
while True:
try:
state = await self._send_status_request()
except asyncio.CancelledError:
raise
except Exception as e:
if type(last_err) != type(e) or last_err.args != e.args:
logging.info(f"Device Init Error: {self.name}\n{e}")
last_err = e
await asyncio.sleep(5.)
continue
else:
self.init_task = None
self.state = state
self.notify_power_changed()
return
async def _send_http_command(self, async def _send_http_command(self,
url: str, url: str,
@ -815,21 +820,39 @@ class TPLinkSmartPlug(PowerDevice):
res += chr(val) res += chr(val)
return res return res
async def init_state(self) -> None: async def _send_info_request(self) -> int:
async with self.request_lock:
await self.refresh_status()
async def refresh_status(self) -> None:
try:
state: str
res = await self._send_tplink_command("info") res = await self._send_tplink_command("info")
if self.output_id is not None: if self.output_id is not None:
# TPLink device controls multiple devices # TPLink device controls multiple devices
children: Dict[int, Any] children: Dict[int, Any]
children = res['system']['get_sysinfo']['children'] children = res['system']['get_sysinfo']['children']
state = children[self.output_id]['state'] return children[self.output_id]['state']
else: else:
state = res['system']['get_sysinfo']['relay_state'] return res['system']['get_sysinfo']['relay_state']
async def init_state(self) -> None:
async with self.request_lock:
last_err: Exception = Exception()
while True:
try:
state: int = await self._send_info_request()
except asyncio.CancelledError:
raise
except Exception as e:
if type(last_err) != type(e) or last_err.args != e.args:
logging.info(f"Device Init Error: {self.name}\n{e}")
last_err = e
await asyncio.sleep(5.)
continue
else:
self.init_task = None
self.state = "on" if state else "off"
self.notify_power_changed()
return
async def refresh_status(self) -> None:
try:
state: int = await self._send_info_request()
except Exception: except Exception:
self.state = "error" self.state = "error"
msg = f"Error Refeshing Device Status: {self.name}" msg = f"Error Refeshing Device Status: {self.name}"