power: add support for gcode_macro Klipper devices
Signed-off-by: Eric Callahan <arksine.code@gmail.com>
This commit is contained in:
parent
aca7cb78b9
commit
0b13e2da44
|
@ -32,7 +32,6 @@ if TYPE_CHECKING:
|
||||||
from .machine import Machine
|
from .machine import Machine
|
||||||
from . import klippy_apis
|
from . import klippy_apis
|
||||||
from .mqtt import MQTTClient
|
from .mqtt import MQTTClient
|
||||||
from .template import TemplateFactory
|
|
||||||
from .template import JinjaTemplate
|
from .template import JinjaTemplate
|
||||||
APIComp = klippy_apis.KlippyAPI
|
APIComp = klippy_apis.KlippyAPI
|
||||||
|
|
||||||
|
@ -516,32 +515,40 @@ class GpioDevice(PowerDevice):
|
||||||
self.timer_handle = None
|
self.timer_handle = None
|
||||||
|
|
||||||
class KlipperDevice(PowerDevice):
|
class KlipperDevice(PowerDevice):
|
||||||
def __init__(self,
|
def __init__(self, config: ConfigHelper) -> None:
|
||||||
config: ConfigHelper,
|
super().__init__(config)
|
||||||
initial_val: Optional[int] = None
|
if self.off_when_shutdown:
|
||||||
) -> None:
|
|
||||||
if config.getboolean('off_when_shutdown', None) is not None:
|
|
||||||
raise config.error(
|
raise config.error(
|
||||||
"Option 'off_when_shutdown' in section "
|
"Option 'off_when_shutdown' in section "
|
||||||
f"[{config.get_name()}] is unsupported for 'klipper_device'")
|
f"[{config.get_name()}] is unsupported for 'klipper_device'")
|
||||||
if config.getboolean('klipper_restart', None) is not None:
|
if self.klipper_restart:
|
||||||
raise config.error(
|
raise config.error(
|
||||||
"Option 'klipper_restart' in section "
|
"Option 'restart_klipper_when_powered' in section "
|
||||||
f"[{config.get_name()}] is unsupported for 'klipper_device'")
|
f"[{config.get_name()}] is unsupported for 'klipper_device'")
|
||||||
super().__init__(config)
|
if (
|
||||||
self.off_when_shutdown = False
|
self.bound_service is not None and
|
||||||
self.klipper_restart = False
|
self.bound_service.startswith("klipper")
|
||||||
self.timer: Optional[float] = config.getfloat('timer', None)
|
):
|
||||||
if self.timer is not None and self.timer < 0.000001:
|
# Klipper devices cannot be bound to an instance of klipper or
|
||||||
|
# klipper_mcu
|
||||||
raise config.error(
|
raise config.error(
|
||||||
f"Option 'timer' in section [{config.get_name()}] must "
|
f"Option 'bound_service' cannot be set to {self.bound_service}"
|
||||||
"be above 0.0")
|
f" for 'klipper_device' [{config.get_name()}]")
|
||||||
|
self.is_shutdown: bool = False
|
||||||
|
self.update_fut: Optional[asyncio.Future] = None
|
||||||
|
self.request_mutex = asyncio.Lock()
|
||||||
|
self.timer: Optional[float] = config.getfloat(
|
||||||
|
'timer', None, above=0.000001)
|
||||||
self.timer_handle: Optional[asyncio.TimerHandle] = None
|
self.timer_handle: Optional[asyncio.TimerHandle] = None
|
||||||
self.object_name = config.get('object_name', '')
|
self.object_name = config.get('object_name')
|
||||||
if not self.object_name.startswith("output_pin "):
|
obj_parts = self.object_name.split()
|
||||||
|
self.gc_cmd = f"SET_PIN PIN={obj_parts[-1]} "
|
||||||
|
if obj_parts[0] == "gcode_macro":
|
||||||
|
self.gc_cmd = obj_parts[-1]
|
||||||
|
elif obj_parts[0] != "output_pin":
|
||||||
raise config.error(
|
raise config.error(
|
||||||
"Currently only Klipper 'output_pin' objects supported for "
|
"Klipper object must be either 'output_pin' or 'gcode_macro' "
|
||||||
f"'object_name' in section [{config.get_name()}]")
|
f"for option 'object_name' in section [{config.get_name()}]")
|
||||||
|
|
||||||
self.server.register_event_handler(
|
self.server.register_event_handler(
|
||||||
"server:status_update", self._status_update)
|
"server:status_update", self._status_update)
|
||||||
|
@ -553,53 +560,104 @@ class KlipperDevice(PowerDevice):
|
||||||
def _status_update(self, data: Dict[str, Any]) -> None:
|
def _status_update(self, data: Dict[str, Any]) -> None:
|
||||||
self._set_state_from_data(data)
|
self._set_state_from_data(data)
|
||||||
|
|
||||||
|
def get_device_info(self) -> Dict[str, Any]:
|
||||||
|
dev_info = super().get_device_info()
|
||||||
|
dev_info['is_shutdown'] = self.is_shutdown
|
||||||
|
return dev_info
|
||||||
|
|
||||||
async def _handle_ready(self) -> None:
|
async def _handle_ready(self) -> None:
|
||||||
kapis: APIComp = self.server.lookup_component('klippy_apis')
|
kapis: APIComp = self.server.lookup_component('klippy_apis')
|
||||||
sub: Dict[str, Optional[List[str]]] = {self.object_name: None}
|
sub: Dict[str, Optional[List[str]]] = {self.object_name: None}
|
||||||
try:
|
data = await kapis.subscribe_objects(sub, None)
|
||||||
data = await kapis.subscribe_objects(sub)
|
if not self._validate_data(data):
|
||||||
|
self.state == "error"
|
||||||
|
else:
|
||||||
|
assert data is not None
|
||||||
self._set_state_from_data(data)
|
self._set_state_from_data(data)
|
||||||
except self.server.error as e:
|
|
||||||
logging.info(f"Error subscribing to {self.object_name}")
|
|
||||||
|
|
||||||
async def _handle_disconnect(self) -> None:
|
async def _handle_disconnect(self) -> None:
|
||||||
|
self.is_shutdown = False
|
||||||
self._set_state("init")
|
self._set_state("init")
|
||||||
|
self._reset_timer()
|
||||||
|
|
||||||
def process_klippy_shutdown(self) -> None:
|
def process_klippy_shutdown(self) -> None:
|
||||||
self._set_state("init")
|
self.is_shutdown = True
|
||||||
|
self._set_state("error")
|
||||||
|
self._reset_timer()
|
||||||
|
|
||||||
def refresh_status(self) -> None:
|
async def refresh_status(self) -> None:
|
||||||
pass
|
if self.is_shutdown or self.state in ["on", "off", "init"]:
|
||||||
|
return
|
||||||
async def set_power(self, state) -> None:
|
async with self.request_mutex:
|
||||||
if self.timer_handle is not None:
|
|
||||||
self.timer_handle.cancel()
|
|
||||||
self.timer_handle = None
|
|
||||||
try:
|
|
||||||
kapis: APIComp = self.server.lookup_component('klippy_apis')
|
kapis: APIComp = self.server.lookup_component('klippy_apis')
|
||||||
object_name = self.object_name[len("output_pin "):]
|
req: Dict[str, Optional[List[str]]] = {self.object_name: None}
|
||||||
object_name_value = "1" if state == "on" else "0"
|
data: Optional[Dict[str, Any]]
|
||||||
await kapis.run_gcode(
|
data = await kapis.query_objects(req, None)
|
||||||
f"SET_PIN PIN={object_name} VALUE={object_name_value}")
|
if not self._validate_data(data):
|
||||||
except Exception:
|
self.state = "error"
|
||||||
self.state = "error"
|
else:
|
||||||
msg = f"Error Toggling Device Power: {self.name}"
|
assert data is not None
|
||||||
logging.exception(msg)
|
self._set_state_from_data(data)
|
||||||
raise self.server.error(msg) from None
|
|
||||||
self.state = state
|
|
||||||
self._check_timer()
|
|
||||||
|
|
||||||
def _set_state_from_data(self, data) -> None:
|
async def set_power(self, state: str) -> None:
|
||||||
|
if self.is_shutdown:
|
||||||
|
raise self.server.error(
|
||||||
|
f"Power Device {self.name}: Cannot set power for device "
|
||||||
|
f"when Klipper is shutdown")
|
||||||
|
async with self.request_mutex:
|
||||||
|
self._reset_timer()
|
||||||
|
eventloop = self.server.get_event_loop()
|
||||||
|
self.update_fut = eventloop.create_future()
|
||||||
|
try:
|
||||||
|
kapis: APIComp = self.server.lookup_component('klippy_apis')
|
||||||
|
value = "1" if state == "on" else "0"
|
||||||
|
await kapis.run_gcode(f"{self.gc_cmd} VALUE={value}")
|
||||||
|
await asyncio.wait_for(self.update_fut, 1.)
|
||||||
|
except TimeoutError:
|
||||||
|
self.state = "error"
|
||||||
|
raise self.server.error(
|
||||||
|
f"Power device {self.name}: Timeout "
|
||||||
|
"waiting for device state update")
|
||||||
|
except Exception:
|
||||||
|
self.state = "error"
|
||||||
|
msg = f"Error Toggling Device Power: {self.name}"
|
||||||
|
logging.exception(msg)
|
||||||
|
raise self.server.error(msg) from None
|
||||||
|
finally:
|
||||||
|
self.update_fut = None
|
||||||
|
self._check_timer()
|
||||||
|
|
||||||
|
def _validate_data(self, data: Optional[Dict[str, Any]]) -> bool:
|
||||||
|
if data is None:
|
||||||
|
logging.info("Error querying klipper object: "
|
||||||
|
f"{self.object_name}")
|
||||||
|
elif self.object_name not in data:
|
||||||
|
logging.info(
|
||||||
|
f"[power]: Invalid Klipper Device {self.object_name}, "
|
||||||
|
f"no response returned from subscription.")
|
||||||
|
elif 'value' not in data[self.object_name]:
|
||||||
|
logging.info(
|
||||||
|
f"[power]: Invalid Klipper Device {self.object_name}, "
|
||||||
|
f"response does not contain a 'value' parameter")
|
||||||
|
else:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _set_state_from_data(self, data: Dict[str, Any]) -> None:
|
||||||
if self.object_name not in data:
|
if self.object_name not in data:
|
||||||
return
|
return
|
||||||
is_on = data.get(self.object_name, {}).get('value', 0.0) > 0.0
|
value = data[self.object_name].get('value')
|
||||||
state = "on" if is_on else "off"
|
if value is not None:
|
||||||
self._set_state(state)
|
state = "on" if value else "off"
|
||||||
|
self._set_state(state)
|
||||||
|
if self.update_fut is not None:
|
||||||
|
self.update_fut.set_result(state)
|
||||||
|
|
||||||
def _set_state(self, state) -> None:
|
def _set_state(self, state: str) -> None:
|
||||||
|
in_event = self.update_fut is not None
|
||||||
last_state = self.state
|
last_state = self.state
|
||||||
self.state = state
|
self.state = state
|
||||||
if last_state != state:
|
if last_state != state and not in_event:
|
||||||
self.notify_power_changed()
|
self.notify_power_changed()
|
||||||
|
|
||||||
def _check_timer(self):
|
def _check_timer(self):
|
||||||
|
@ -609,6 +667,11 @@ class KlipperDevice(PowerDevice):
|
||||||
self.timer_handle = event_loop.delay_callback(
|
self.timer_handle = event_loop.delay_callback(
|
||||||
self.timer, power.set_device_power, self.name, "off")
|
self.timer, power.set_device_power, self.name, "off")
|
||||||
|
|
||||||
|
def _reset_timer(self):
|
||||||
|
if self.timer_handle is not None:
|
||||||
|
self.timer_handle.cancel()
|
||||||
|
self.timer_handle = None
|
||||||
|
|
||||||
def close(self) -> None:
|
def close(self) -> None:
|
||||||
if self.timer_handle is not None:
|
if self.timer_handle is not None:
|
||||||
self.timer_handle.cancel()
|
self.timer_handle.cancel()
|
||||||
|
|
Loading…
Reference in New Issue