import json import logging from dataclasses import dataclass import asyncio import os import argparse import yaml from rich.logging import RichHandler from internal_client import InternalClient, exceptions from button_logic import ButtonLogic, ButtonState, ButtonLedState MODULE_ID = 1000 @dataclass class ButtonClientConfig: device_name: str device_type: int device_role: str device_priority: int = 0 def __repr__(self) -> str: return f"(name={self.device_name}, type={self.device_type}, role={self.device_role}, priority={self.device_priority})" @staticmethod def load_config(file_name: str) -> list["ButtonClientConfig"]: if not os.path.isfile(file_name): raise FileNotFoundError(f"Config file not found: {file_name}") with open(file_name, "r") as f_in: try: cfg = yaml.safe_load(f_in) except yaml.parser.ParserError: raise ValueError(f"Config file can not be parsed as YAML") from None button_configs = [] for button in cfg: button_name = list(button.keys())[0] try: button_role = button[button_name]["role"] button_type = int(button[button_name].get("type", 0)) button_priority = int(button[button_name].get("priority", 0)) except KeyError: raise ValueError(f"Missing required field in {button_name} config.") from None except ValueError: raise ValueError(f"Invalid value in {button_name} config.") from None button_configs.append( ButtonClientConfig(button_name, button_type, button_role, button_priority) ) return button_configs class ButtonClient: def __init__(self, server_ip: str, server_port: int, button_config: ButtonClientConfig, manual_mode: bool) -> None: self.server_connection = (server_ip, server_port) self.device_name = button_config.device_name self.device_type = button_config.device_type self.device_role = button_config.device_role self.manual_mode = manual_mode self.device_priority = button_config.device_priority self.logger = logging.getLogger(f"{self.device_name}") self.button = ButtonLogic(logger=self.logger, manual_mode=manual_mode) async def start(self) -> None: if not self._connect(): return while True: state_data = self._get_state_data() try: self.client.send_status(state_data, timeout=5) except (exceptions.CommunicationExceptions, exceptions.ConnectExceptions) as e: self.logger.error(f"Send status unsuccessful: {e}") break command_data = self.client.get_command() try: self._handle_command_data(command_data) except ValueError as e: self.logger.error(f"Invalid Command: {e}") break if not self.manual_mode: await asyncio.sleep(5) self.client.destroy() def _connect(self) -> bool: try: self.client = InternalClient( MODULE_ID, self.server_connection[0], self.server_connection[1], self.device_name, self.device_type, self.device_role, self.device_priority, ) except exceptions.ConnectExceptions as e: self.logger.error( f"Button could not connect because server responsed with: {type(e)}" ) return False except exceptions.CommunicationExceptions as e: self.logger.error(f"Connection to server could not be established due to: {e}") return False return True def _get_state_data(self) -> bytes: button_state = self.button.get_button_state() binary_data = json.dumps({"pressed": button_state == ButtonState.PRESSED}).encode() return binary_data def _handle_command_data(self, command_data: bytes) -> None: try: data_json = json.loads(command_data) except json.JSONDecodeError: raise ValueError(f"Couldn't parse json from binary commandData") try: led_command = data_json["lit_up"] assert type(led_command) == bool except (KeyError, AssertionError): raise ValueError(f"commandData json has invalid structure") self.button.set_led_state(ButtonLedState(led_command)) async def run_buttons( server_ip: str, server_port: int, button_configs: list[ButtonClientConfig], manual_mode: bool ) -> None: logger = logging.getLogger("TaskRunner") buttons = [ButtonClient(server_ip, server_port, cfg, manual_mode) for cfg in button_configs] tasks = [] for button, button_cfg in zip(buttons, button_configs): logger.info(f"Starting button {button_cfg}") tasks.append(asyncio.create_task(button.start())) for task in tasks: await task logger.info("All buttons finished") if __name__ == "__main__": FORMAT = "%(name)s:%(message)s" logging.basicConfig( level=logging.DEBUG, format=FORMAT, datefmt="[%X]", handlers=[RichHandler()] ) parser = argparse.ArgumentParser() parser.add_argument( "-i", "--ip", type=str, default="127.0.0.1", help="IP address of server" ) parser.add_argument("-m", "--manual", type=bool, default=False, help="Set manual mode, buttons will expect input from keyboard in order to be pressed") parser.add_argument("-p", "--port", type=int, default=8888, help="Port of server") parser.add_argument( "-cfg", "--config", type=str, default="buttons.yaml", help="Path to yaml button config file", ) args = parser.parse_args() try: buttons = ButtonClientConfig.load_config(args.config) except (FileNotFoundError, ValueError) as e: logging.error(e) exit(1) try: asyncio.run(run_buttons(args.ip, args.port, buttons, args.manual)) except KeyboardInterrupt: logging.info("Shuting down.")