From 8984100dc9b25c612df918b9d8197d9c06d5bdbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Szafer?= Date: Fri, 11 Aug 2023 08:52:21 +0200 Subject: [PATCH] add duration to event entity --- boneio/bonecli.py | 6 ++++-- boneio/helper/click_timer.py | 3 ++- boneio/helper/events.py | 11 ++++------ boneio/helper/gpio.py | 3 ++- boneio/helper/loader.py | 5 +++-- boneio/input/gpio_beta.py | 12 +++++------ boneio/manager.py | 17 ++++++++++----- boneio/mqtt_client.py | 42 +++++++++++++++++------------------- boneio/runner.py | 6 +----- 9 files changed, 54 insertions(+), 51 deletions(-) diff --git a/boneio/bonecli.py b/boneio/bonecli.py index b79ee88..f505145 100644 --- a/boneio/bonecli.py +++ b/boneio/bonecli.py @@ -15,6 +15,7 @@ from boneio.const import ACTION from boneio.helper import load_config_from_file from boneio.helper.exceptions import ConfigurationException, RestartRequestException +from boneio.helper.events import GracefulExit from boneio.helper.logger import configure_logger from boneio.runner import async_run from boneio.version import __version__ @@ -89,8 +90,9 @@ def run(config: str, debug: int, mqttusername: str = "", mqttpassword: str = "") ), ) return 0 - except RestartRequestException as err: - _LOGGER.info(err) + except (RestartRequestException, GracefulExit) as err: + if err is not None: + _LOGGER.info(err) return 0 except (ConfigurationException, MarkedYAMLError) as err: _LOGGER.error("Failed to load config. %s Exiting.", err) diff --git a/boneio/helper/click_timer.py b/boneio/helper/click_timer.py index b2e6244..222d009 100644 --- a/boneio/helper/click_timer.py +++ b/boneio/helper/click_timer.py @@ -24,9 +24,10 @@ def reset(self) -> None: self._remove_listener = None async def _start_async_timer(self) -> None: + start = time.time() await asyncio.sleep(self._delay) self._remove_listener = None - self._action(time.time()) + self._action(round(time.time() - start, 2)) def start_timer(self) -> None: """Start timer.""" diff --git a/boneio/helper/events.py b/boneio/helper/events.py index c48d83a..15aec47 100644 --- a/boneio/helper/events.py +++ b/boneio/helper/events.py @@ -77,11 +77,9 @@ def handle(self): class EventBus: """Simple event bus which ticks every second.""" - def __init__(self, last_will_mqtt = Callable) -> None: + def __init__(self, loop: asyncio.AbstractEventLoop) -> None: """Initialize handler""" - self._loop = asyncio.get_event_loop() - self._last_will = last_will_mqtt - + self._loop = loop or asyncio.get_event_loop() self._listeners = {} self._sigterm_listeners = [] self._haonline_listeners = [] @@ -89,7 +87,7 @@ def __init__(self, last_will_mqtt = Callable) -> None: for signame in {"SIGINT", "SIGTERM"}: self._loop.add_signal_handler( getattr(signal, signame), - lambda: asyncio.create_task(self.ask_exit()), + self.ask_exit, ) def _run_second_event(self, time): @@ -100,7 +98,7 @@ def _run_second_event(self, time): self._loop.call_soon(listener.target, time) ) - async def ask_exit(self): + def ask_exit(self): """Function to call on exit. Should invoke all sigterm listeners.""" _LOGGER.debug("Exiting process started.") self._listeners = {} @@ -108,7 +106,6 @@ async def ask_exit(self): target() self._timer_handle() - await self._last_will() _LOGGER.info("Shutdown gracefully.") raise GracefulExit(code=0) diff --git a/boneio/helper/gpio.py b/boneio/helper/gpio.py index 73ce675..f5590c2 100644 --- a/boneio/helper/gpio.py +++ b/boneio/helper/gpio.py @@ -1,3 +1,4 @@ +from __future__ import annotations import asyncio import logging @@ -104,7 +105,7 @@ class GpioBaseClass: """Base class for initialize GPIO""" def __init__( - self, pin: str, press_callback: Callable[[ClickTypes, str], None], **kwargs + self, pin: str, press_callback: Callable[[ClickTypes, str, bool | None], None], **kwargs ) -> None: """Setup GPIO Input Button""" self._pin = pin diff --git a/boneio/helper/loader.py b/boneio/helper/loader.py index 8b81819..474d81d 100644 --- a/boneio/helper/loader.py +++ b/boneio/helper/loader.py @@ -349,12 +349,13 @@ def configure_event_sensor( GpioEventButtonClass = GpioEventButton if gpio.get("detection_type", "stable") == "stable" else GpioEventButtonBeta GpioEventButtonClass( pin=pin, - press_callback=lambda x, i: press_callback( + press_callback=lambda x, i, z: press_callback( x=x, inpin=i, actions=gpio.get(ACTIONS, {}).get(x, []), input_type=INPUT, - empty_message_after=gpio.get("clear_message", False) + empty_message_after=gpio.get("clear_message", False), + duration=z ), **gpio, ) diff --git a/boneio/input/gpio_beta.py b/boneio/input/gpio_beta.py index 37ad0ff..3b0ac47 100644 --- a/boneio/input/gpio_beta.py +++ b/boneio/input/gpio_beta.py @@ -39,7 +39,7 @@ def __init__(self, **kwargs) -> None: ) self._timer_long = ClickTimer( delay=TimePeriod(milliseconds=LONG_PRESS_DURATION_MS), - action=lambda x: self.press_callback(click_type=LONG), + action=lambda x: self.press_callback(click_type=LONG, duration=x), ) self._double_click_ran = False self._is_waiting_for_second_click = False @@ -52,7 +52,7 @@ def __init__(self, **kwargs) -> None: def double_click_press_callback(self): self._is_waiting_for_second_click = False if not self._state and not self._timer_long.is_waiting(): - self.press_callback(click_type=SINGLE) + self.press_callback(click_type=SINGLE, duration=None) def check_state(self, channel) -> None: time_now = time.time() @@ -65,7 +65,7 @@ def check_state(self, channel) -> None: if self._timer_double.is_waiting(): self._timer_double.reset() self._double_click_ran = True - self.press_callback(click_type=DOUBLE) + self.press_callback(click_type=DOUBLE, duration=None) return self._timer_double.start_timer() self._is_waiting_for_second_click = True @@ -73,12 +73,12 @@ def check_state(self, channel) -> None: else: if not self._is_waiting_for_second_click and not self._double_click_ran: if self._timer_long.is_waiting(): - self.press_callback(click_type=SINGLE) + self.press_callback(click_type=SINGLE, duration=None) self._timer_long.reset() self._double_click_ran = False - def press_callback(self, click_type: ClickTypes): + def press_callback(self, click_type: ClickTypes, duration: float | None = None): self.click_count = 0 self._loop.call_soon_threadsafe( - partial(self._press_callback, click_type, self._pin) + partial(self._press_callback, click_type, self._pin, duration) ) diff --git a/boneio/manager.py b/boneio/manager.py index 731cfff..620942a 100644 --- a/boneio/manager.py +++ b/boneio/manager.py @@ -1,3 +1,4 @@ +from __future__ import annotations import asyncio import logging from collections import deque @@ -86,7 +87,6 @@ def __init__( stop_client: Callable[[], Awaitable[None]], state_manager: StateManager, config_helper: ConfigHelper, - event_bus: EventBus, config_file_path: str, relay_pins: List = [], event_pins: List = [], @@ -111,7 +111,7 @@ def __init__( self._host_data = None self._config_file_path = config_file_path self._state_manager = state_manager - self._event_bus = event_bus + self._event_bus = EventBus(loop=self._loop) self.send_message = send_message self.stop_client = stop_client @@ -480,13 +480,20 @@ def pcf(self): return self._pcf def press_callback( - self, x: ClickTypes, inpin: str, actions: List, input_type: InputTypes = INPUT, empty_message_after: bool = False + self, x: ClickTypes, inpin: str, actions: List, input_type: InputTypes = INPUT, empty_message_after: bool = False, duration: float | None = None ) -> None: """Press callback to use in input gpio. If relay input map is provided also toggle action on relay or cover or mqtt.""" topic = f"{self._config_helper.topic_prefix}/{input_type}/{inpin}" - payload = {"event_type": x} if input_type == INPUT else x - self.send_message(topic=topic, payload=payload, retain=False) + + def generate_payload(): + if input_type == INPUT: + if duration: + return {"event_type": x, "duration": duration} + return {"event_type": x} + return x + + self.send_message(topic=topic, payload=generate_payload(), retain=False) for action_definition in actions: _LOGGER.debug("Executing action %s", action_definition) if action_definition[ACTION] == OUTPUT: diff --git a/boneio/mqtt_client.py b/boneio/mqtt_client.py index 1ff062a..2336374 100644 --- a/boneio/mqtt_client.py +++ b/boneio/mqtt_client.py @@ -15,9 +15,10 @@ from paho.mqtt.properties import Properties from paho.mqtt.subscribeoptions import SubscribeOptions -from boneio.const import ONLINE, OFFLINE, PAHO, STATE +from boneio.const import OFFLINE, PAHO, STATE from boneio.helper import UniqueQueue from boneio.helper.config import ConfigHelper +from boneio.helper.events import GracefulExit from boneio.manager import Manager from boneio.helper.exceptions import RestartRequestException @@ -58,7 +59,7 @@ def create_client(self) -> None: self.port, will=Will( topic=f"{self._config_helper.topic_prefix}/{STATE}", - payload=ONLINE, + payload=OFFLINE, qos=0, retain=False, ), @@ -148,21 +149,23 @@ async def _handle_publish(self) -> None: async def start_client(self, manager: Manager) -> None: """Start the client with the manager.""" # Reconnect automatically until the client is stopped. - while True: - try: - await self._subscribe_manager(manager) - except MqttError as err: - self.reconnect_interval = min(self.reconnect_interval * 2, 900) - _LOGGER.error( - "MQTT error: %s. Reconnecting in %s seconds", - err, - self.reconnect_interval, - ) - self._connection_established = False - await asyncio.sleep(self.reconnect_interval) - self.create_client() # reset connect/reconnect futures - except asyncio.CancelledError: - _LOGGER.info("MQTT client task canceled.") + try: + while True: + try: + await self._subscribe_manager(manager) + except MqttError as err: + self.reconnect_interval = min(self.reconnect_interval * 2, 900) + _LOGGER.error( + "MQTT error: %s. Reconnecting in %s seconds", + err, + self.reconnect_interval, + ) + self._connection_established = False + await asyncio.sleep(self.reconnect_interval) + self.create_client() # reset connect/reconnect futures + except (asyncio.CancelledError, GracefulExit): + _LOGGER.info("MQTT client task canceled.") + pass async def stop_client(self) -> None: await self.unsubscribe( @@ -170,11 +173,6 @@ async def stop_client(self) -> None: ) raise RestartRequestException("Restart requested.") - async def last_will_mqtt(self) -> None: - _LOGGER.info("Sending offline state.") - topic = f"{self._config_helper.topic_prefix}/{STATE}" - self.send_message(topic=topic, payload=OFFLINE, retain=True) - async def _subscribe_manager(self, manager: Manager) -> None: """Connect and subscribe to manager topics + host stats.""" async with AsyncExitStack() as stack: diff --git a/boneio/runner.py b/boneio/runner.py index 2f7b9c0..c3e4f86 100644 --- a/boneio/runner.py +++ b/boneio/runner.py @@ -59,8 +59,6 @@ async def async_run( mqttpassword: str = "", ) -> list[Any]: """Run BoneIO.""" - _loop = asyncio.get_event_loop() - _config_helper = ConfigHelper( topic_prefix=config[MQTT].pop(TOPIC_PREFIX), ha_discovery=config[MQTT][HA_DISCOVERY].pop(ENABLED), @@ -74,7 +72,6 @@ async def async_run( port=config[MQTT].get(PORT, 1883), config_helper=_config_helper, ) - event_bus = EventBus(last_will_mqtt=client.last_will_mqtt) manager_kwargs = { item["name"]: config.get(item["name"], item["default"]) for item in config_modules @@ -83,7 +80,6 @@ async def async_run( manager = Manager( send_message=client.send_message, stop_client=client.stop_client, - event_bus=event_bus, relay_pins=config.get(OUTPUT, []), event_pins=config.get(EVENT_ENTITY, []), binary_pins=config.get(BINARY_SENSOR, []), @@ -104,4 +100,4 @@ async def async_run( tasks.update(manager.get_tasks()) _LOGGER.info("Connecting to MQTT.") tasks.add(client.start_client(manager)) - return await asyncio.gather(*tasks) + return await asyncio.gather(*tasks, return_exceptions=True)