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 asyncio
import time
from tornado.escape import json_decode
# Annotation imports
from typing import (
@ -102,30 +101,11 @@ class PrinterPower:
return pstate == "printing"
async def component_init(self) -> None:
event_loop = self.server.get_event_loop()
# Wait up to 5 seconds for the machine component to init
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(
"The following power devices failed init:"
f" {failed_names}")
for dev in self.devices.values():
if not dev.initialize():
self.server.add_warning(
f"Power device '{dev.get_name()}' failed to initialize"
)
def _handle_klippy_shutdown(self) -> None:
for dev in self.devices.values():
@ -209,9 +189,13 @@ class PrinterPower:
if name in self.devices:
raise self.server.error(
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(
f"Failed to initialize power device: {device.get_name()}")
f"Power device '{device.get_name()}' failed to initialize"
)
return
self.devices[name] = device
@ -232,6 +216,7 @@ class PowerDevice:
self.type: str = config.get('type')
self.state: str = "init"
self.request_lock = asyncio.Lock()
self.init_task: Optional[asyncio.Task] = None
self.locked_while_printing = config.getboolean(
'locked_while_printing', False)
self.off_when_shutdown = config.getboolean('off_when_shutdown', False)
@ -351,11 +336,12 @@ class PowerDevice:
def init_state(self) -> Optional[Coroutine]:
return None
async def initialize(self) -> bool:
def initialize(self) -> bool:
self._setup_bound_service()
ret = self.init_state()
if ret is not None:
await ret
eventloop = self.server.get_event_loop()
self.init_task = eventloop.create_task(ret)
return self.state != "error"
async def process_request(self, req: str) -> str:
@ -401,7 +387,10 @@ class PowerDevice:
raise NotImplementedError
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):
def __init__(self,
@ -422,7 +411,23 @@ class HTTPDevice(PowerDevice):
async def init_state(self) -> None:
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,
url: str,
@ -815,21 +820,39 @@ class TPLinkSmartPlug(PowerDevice):
res += chr(val)
return res
async def _send_info_request(self) -> int:
res = await self._send_tplink_command("info")
if self.output_id is not None:
# TPLink device controls multiple devices
children: Dict[int, Any]
children = res['system']['get_sysinfo']['children']
return children[self.output_id]['state']
else:
return res['system']['get_sysinfo']['relay_state']
async def init_state(self) -> None:
async with self.request_lock:
await self.refresh_status()
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: str
res = await self._send_tplink_command("info")
if self.output_id is not None:
# TPLink device controls multiple devices
children: Dict[int, Any]
children = res['system']['get_sysinfo']['children']
state = children[self.output_id]['state']
else:
state = res['system']['get_sysinfo']['relay_state']
state: int = await self._send_info_request()
except Exception:
self.state = "error"
msg = f"Error Refeshing Device Status: {self.name}"