From caebaaad27e9fe1646ec477ff1d56f65af230cb8 Mon Sep 17 00:00:00 2001 From: iloveicedgreentea <31193909+iloveicedgreentea@users.noreply.github.com> Date: Thu, 22 Dec 2022 17:48:46 -0500 Subject: [PATCH] add init protocol --- .github/workflows/build.yml | 26 ++ README.md | 6 +- madvr/commands.py | 168 +++++------- madvr/madvr.py | 516 ++++++++---------------------------- setup.py | 2 +- tests/testMadVR.py | 27 +- 6 files changed, 229 insertions(+), 516 deletions(-) create mode 100644 .github/workflows/build.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..37230f1 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,26 @@ +name: Upload Python Package + +on: + release: + types: [published] + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.9' + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install build + - name: Build package + run: python -m build + - name: Publish package + uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29 + with: + user: __token__ + password: ${{ secrets.PYPI_API_TOKEN }} diff --git a/README.md b/README.md index cc7df99..4fbcec6 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,7 @@ # Py MadVR -Support for IP based controls for Envys \ No newline at end of file +Support for IP based controls for Envys'' + +*Note: IP control does not work if there is no input signal, by design" + +Readme WIP \ No newline at end of file diff --git a/madvr/commands.py b/madvr/commands.py index 2df6627..a117e9d 100644 --- a/madvr/commands.py +++ b/madvr/commands.py @@ -7,11 +7,11 @@ # pylint: disable=missing-class-docstring invalid-name class Connections(Enum): welcome = b"WELCOME" - heartbeat = b"Heartbeat\r" + heartbeat = b"Heartbeat\r\n" bye = b"Bye\r\n" class Footer(Enum): - footer = b" \x0D \x0A" + footer = b"\x0D\x0A" class Headers(Enum): temperature = b"Temperatures" @@ -28,16 +28,31 @@ class Headers(Enum): class ACKs(Enum): reply = b"OK\r\n" error = b"ERROR" - -class Notifications(Enum): +class Temperatures(Enum): pass -class PowerCommands(Enum): - pass +class Notifications(Enum): + activate_profile = b"ActivateProfile" + incoming_signal = b"IncomingSignalInfo" + outgoing_signal = b"OngoingSignalInfo" + aspect_ratio = b"AspectRatio" + masking_ratio = b"MaskingRatio" -class MenuCommands(Enum): - pass +class KeyPress(Enum): + menu = b"MENU" + up = b"UP" + down = b"DOWN" + left = b"LEFT" + right = b"RIGHT" + ok = b"OK" + inp = b"INPUT" + settings = b"SETTINGS" + red = b"RED" + green = b"GREEN" + blue = b"BLUE" + yellow = b"YELLOW" + power = b"POWER" class DisplayAlert(Enum): pass @@ -54,24 +69,22 @@ class SettingsPages(Enum): class Toggle(Enum): pass -class Other(Enum): +class SingleCmd(Enum): + """for things that are single words""" pass +class IsInformational(Enum): + true = True + false = False class Commands(Enum): - # Notifications - activate_profile = b"ActivateProfile" - incoming_signal = b"IncomingSignalInfo" - outgoing_signal = b"OngoingSignalInfo" - aspect_ratio = b"AspectRatio" - masking_ratio = b"MaskingRatio" - # Power stuff - power_off = b"PowerOff", ACKs.reply - standby = b"Standby", ACKs.reply - restart = b"Restart", ACKs.reply - reload_software = b"ReloadSoftware", ACKs.reply - bye = b"Bye", ACKs.reply + power_off = b"PowerOff", SingleCmd, IsInformational.false + standby = b"Standby", SingleCmd, IsInformational.false + restart = b"Restart", SingleCmd, IsInformational.false + reload_software = b"ReloadSoftware", SingleCmd, IsInformational.false + bye = b"Bye", SingleCmd, IsInformational.false + # vb'KeyPress MENU\r\n' # b'CloseMenu\r\n' # b'OK\r\nIncomingSignalInfo 3840x2160 59.940p 2D 420 10bit SDR 709 TV 16:9\r\nReloadSoftware\r\nOutgoingSignalInfo 4096x2160 59.940p 2D RGB 8bit SDR 709 TV\r\nIncomingSignalInfo 3840x2160 59.940p 2D 420 10bit SDR 709 TV 16:9\r\nAspectRatio 3840:2160 1.778 178 "16:9"\r\nMaskingRatio 3044:1712 1.778 178\r\nKeyPress MENU\r\nOpenMenu Configuration\r\n' @@ -79,90 +92,33 @@ class Commands(Enum): # b'OK\r\nAspectRatio 3816:2146 1.778 178 "16:9"\r\nResetTemporary\r\nNoSignal\r\nOutgoingSignalInfo 4096x2160 59.940p 2D RGB 8bit SDR 709 TV\r\nIncomingSignalInfo 1280x720 59.940p 2D 422 12bit SDR 709 TV 16:9\r\nAspectRatio 1280:0720 1.778 178 "16:9"\r\nAspectRatio 1272:0525 2.423 240 "Panavision"\r\nMaskingRatio 4092:1689 2.423 240\r\n' # Menu - open_menu = b"OpenMenu", ACKs.reply - close_menu = b"CloseMenu", ACKs.reply - key_press = b"KeyPress", ACKs.reply - key_press_hold = b"KeyHold", ACKs.reply - # "POWER, MENU, LEFT, RIGHT, UP, DOWN, OK, INPUT, SETTINGS, RED, GREEN, BLUE, YELLOW" - - display_alert = b"DisplayAlertWindow", ACKs.reply - close_alert = b"CloseAlertWindow", ACKs.reply - display_message = b"DisplayMessage", ACKs.reply - - get_signal_info = b"GetIncomingSignalInfo" - get_aspect_ratio = b"GetAspectRatio" - get_masking_ratio = b"GetMaskingRatio" - get_temp = b"GetTemperatures" - get_mac = b"GetMacAddress" - - enum_settings = b"EnumSettingsPages" - enum_configs = b"EnumConfigPages" - enum_options = b"EnumOptions" - query_option = b"QueryOption" - change_option = b"ChangeOption" - reset_temp = b"ResetTemporary" - - toggle = b"Toggle" - tone_map_on = b"ToneMapOn" - tone_map_off = b"ToneMapOff" - - hotplug = b"Hotplug" - refresh_license = b"RefreshLicenseInfo" - force_1080p = b"Force1080p60Output" - - - # # these use ! unless otherwise indicated - # # power commands - # power = b"PW", PowerModes, ACKs.power_ack - - # # lens memory /installation mode commands - # installation_mode = b"INML", InstallationModes, ACKs.lens_ack - - # # input commands - # input_mode = b"IP", InputModes, ACKs.input_ack - - # # status commands - Reference: ? - # # These should not be used directly - # power_status = b"PW" - # current_output = b"IP" - # info = b"RC7374" - - # # picture mode commands - # picture_mode = b"PMPM", PictureModes, ACKs.picture_ack + open_menu = b"OpenMenu", SingleCmd, IsInformational.false + close_menu = b"CloseMenu", SingleCmd, IsInformational.false + key_press = b"KeyPress", KeyPress, IsInformational.false + key_press_hold = b"KeyHold", KeyPress, IsInformational.false - # # Color modes - # color_mode = b"ISHS", ColorSpaceModes, ACKs.hdmi_ack - - # # input_level like 0-255 - # input_level = b"ISIL", InputLevel, ACKs.hdmi_ack - - # # low latency enable/disable - # low_latency = b"PMLL", LowLatencyModes, ACKs.picture_ack - # # enhance - # enhance = b"PMEN", EnhanceModes, ACKs.picture_ack - # # motion enhance - # motion_enhance = b"PMME", MotionEnhanceModes, ACKs.picture_ack - # # graphic mode - # graphic_mode = b"PMGM", GraphicModeModes, ACKs.picture_ack - - # # mask commands - # mask = b"ISMA", MaskModes - - # # laser power commands - # laser_power = b"PMLP", LaserPowerModes, ACKs.picture_ack - - # # menu controls - # menu = b"RC73", MenuModes, ACKs.menu_ack - - # # NZ Series Laser Dimming commands - # laser_mode = b"PMDC", LaserDimModes, ACKs.picture_ack - - # # Lens Aperture commands - # aperture = b"PMDI", ApertureModes, ACKs.picture_ack - - # # Anamorphic commands - # # I don't use this, untested - # anamorphic = b"INVS", AnamorphicModes, ACKs.lens_ack - # # e-shift - # eshift_mode = b"PMUS", EshiftModes, ACKs.picture_ack + # display_alert = b"DisplayAlertWindow", ACKs.reply + # close_alert = b"CloseAlertWindow", ACKs.reply + # display_message = b"DisplayMessage", ACKs.reply + + get_signal_info = b"GetIncomingSignalInfo", SingleCmd, IsInformational.true + get_aspect_ratio = b"GetAspectRatio", SingleCmd, IsInformational.true + get_masking_ratio = b"GetMaskingRatio", SingleCmd, IsInformational.true + get_temp = b"GetTemperatures", SingleCmd, IsInformational.true + get_mac = b"GetMacAddress", SingleCmd, IsInformational.true + + # enum_settings = b"EnumSettingsPages" + # enum_configs = b"EnumConfigPages" + # enum_options = b"EnumOptions" + # query_option = b"QueryOption" + # change_option = b"ChangeOption" + # reset_temp = b"ResetTemporary" + + # toggle = b"Toggle" + tone_map_on = b"ToneMapOn", SingleCmd, IsInformational.false + tone_map_off = b"ToneMapOff", SingleCmd, IsInformational.false + + hotplug = b"Hotplug", SingleCmd, IsInformational.false + refresh_license = b"RefreshLicenseInfo", SingleCmd, IsInformational.false + force_1080p = b"Force1080p60Output", SingleCmd, IsInformational.false diff --git a/madvr/madvr.py b/madvr/madvr.py index 65ab217..cffce0c 100644 --- a/madvr/madvr.py +++ b/madvr/madvr.py @@ -3,11 +3,10 @@ """ import logging -from typing import Final, Union -import asyncio +from typing import Final import time -from madvr.commands import ACKs, Footer, Headers, Commands, Enum, Connections - +import socket +from madvr.commands import ACKs, Footer, Commands, Enum, Connections class Madvr: """MadVR Control""" @@ -24,433 +23,152 @@ def __init__( self.port = port self.connect_timeout: int = connect_timeout self.logger = logger - self._lock = asyncio.Lock() # Const values self.MADVR_OK: Final = Connections.welcome.value self.HEARTBEAT: Final = Connections.heartbeat.value - self.reader: asyncio.StreamReader = None - self.writer: asyncio.StreamWriter = None + self.client = None + self.is_closed = False self.read_limit = 8000 self.command_read_timeout = 3 self.logger.debug("Running in debug mode") - async def async_open_connection(self) -> bool: + + def close_connection(self): + """close the connection""" + self.client.close() + self.is_closed = True + + def open_connection(self) -> bool: """Open a connection""" self.logger.debug("Starting open connection") - msg, success = await self.reconnect() + msg, success = self.reconnect() if not success: self.logger.error(msg) return success - async def reconnect(self): + def reconnect(self): """Initiate keep-alive connection. This should handle any error and reconnect eventually.""" while True: try: - if self.writer is not None: - self.logger.debug("Closing writer") - self.writer.close() - await self.writer.wait_closed() self.logger.info( - "Connecting to MadVR: %s:%s", self.host, self.port + "Connecting to Envy: %s:%s", self.host, self.port ) - cor = asyncio.open_connection(self.host, self.port) - # wait for 10 sec to connect - self.reader, self.writer = await asyncio.wait_for(cor, 10) + self.client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.client.settimeout(10) + + self.client.connect((self.host, self.port)) self.logger.info("Connected to Envy") - # create a reader and writer to do handshake + + # Test heartbeat self.logger.debug("Handshaking") - result, success = await self._async_handshake() - if not success: - return result, success + + # Make sure first message says WELCOME + msg_envy = self.client.recv(self.read_limit) + self.logger.debug(self.MADVR_OK) + self.logger.debug(msg_envy) + + # Check if first 7 char match + if self.MADVR_OK != msg_envy[:7]: + result = f"Envy did not reply with correct greeting: {msg_envy}" + self.logger.error(result) + return result, False + + # envy needs some time to setup new connection + time.sleep(3) + + # confirm can send heartbeat, ready for commands + self.logger.debug("Sending heartbeat") + self.client.send(self.HEARTBEAT) + + # read first 4 bytes for ok\r\n + ack_reply = self.client.recv(4) + self.logger.debug(ack_reply) + + if ack_reply != ACKs.reply.value: + return "Ack OK not received", False + self.logger.debug("Handshake complete and we are connected") return "Connection done", True # includes conn refused + except socket.timeout: + self.logger.warning("Connection timed out, retrying in 2 seconds") + time.sleep(2) except OSError as err: self.logger.warning("Connecting failed, retrying in 2 seconds") self.logger.debug(err) - await asyncio.sleep(2) - except asyncio.TimeoutError: - self.logger.warning("Connection timed out, retrying in 2 seconds") - await asyncio.sleep(2) + time.sleep(2) - async def _async_handshake(self) -> tuple[str, bool]: + def _construct_command( + self, raw_command: str + ) -> tuple[bytes, bool]: """ - Make sure we got a handshake, make sure we can send heartbeat + Transform commands into their byte values from the string value """ - # Make sure first message says WELCOME - msg_envy = await self.reader.read(self.read_limit) - self.logger.debug(self.MADVR_OK) - self.logger.debug(msg_envy[:7]) - # Check if first 7 char match - if self.MADVR_OK != msg_envy[:7]: - result = f"Envy did not reply with correct greeting: {msg_envy}" - self.logger.error(result) - return result, False - - # # try sending heartbeat, if there's an error, raise exception - # try: - # time.sleep(20) - # self.logger.debug(self.HEARTBEAT) - # self.writer.write(self.HEARTBEAT) - # await self.writer.drain() - # except asyncio.TimeoutError as err: - # result = f"Timeout sending heartbeat {err}" - # self.logger.error(result) - # return result, False - - # # see if we receive PJACK, if not, raise exception - # self.logger.debug("Waiting for heartbeat ok") - # time.sleep(1) - # msg_hb = await self.reader.read(self.read_limit) - # if msg_hb != ACKs.reply.value: - # result = f"Exception with heartbeat: {msg_hb}" - # self.logger.error(result) - # return result, False - # self.logger.debug("Handshake successful") - return "ok", True - - async def async_keep_open(self): - await self.reconnect() - - i = 0 - while i < 10: - self.writer.write("Heartbeat\r".encode("utf-8")) - await self.writer.drain() - print(await self.reader.read(4000)) - time.sleep(20) - i += 1 - # await self.writer.close() - - async def async_send_command(self, command: str) -> str: - await self.reconnect() - self.writer.write(command.encode("utf-8")) + # split command into the base and the action like menu: left + self.logger.debug(raw_command) + skip_val = False try: - await self.writer.drain() - time.sleep(1) - # TODO: need a way to constantly read stuff from input somehow? - # TODO: maybe always sleep and then read a ton and filter from Response header to \r\n - return await self.reader.read(self.read_limit) - except asyncio.TimeoutError as err: - result = f"Timeout sending command {err}" - self.logger.error(result) - return result, False - except ConnectionError as err: - # reaching this means the writer was closed somewhere - self.logger.error(err) - self.logger.debug("Restarting connection") - # restart the connection - - await self.reconnect() - # async def _async_construct_command( - # self, raw_command: str - # ) -> tuple[bytes, ACKs]: - # """ - # Transform commands into their byte values from the string value - # """ - # # split command into the base and the action like command // params - # try: - # command, value = raw_command.split(",") - # except ValueError: - # command = raw_command - # value = "" - # # return "No value for command provided", False - - # # Check if command is implemented - # if not hasattr(Commands, command): - # self.logger.error("Command not implemented: %s", command) - # return "Not Implemented", False - - # # construct the command with nested Enums - # command_name, ack = Commands[command].value - # # Construct command based on required values - # command: bytes = ( - # command_name + Footer.footer.value - # ) - # self.logger.debug("command: %s", command) - - # return command, ack - - # async def _async_send_command( - # self, - # send_command: Union[list[bytes], bytes], - # ack: bytes = None, - # ) -> tuple[str, bool]: - # """ - # Sends a command with a flag to expect an ack. - - # The PJ API returns nothing if a command is in flight - # or if a command is not successful - - # send_command: A command to send - # ack: value of the ack we expect, like OK - - # Returns: - # ( - # ack or error message: str, - # success flag: bool - # ) - # """ - - # if self.writer is None: - # self.logger.error("Connection lost. Restarting") - - # await self.reconnect() - - # try: - # cons_command, ack = await self._async_construct_command( - # send_command - # ) - # except TypeError: - # cons_command = send_command - - # if not ack: - # return cons_command, ack - - # result, success = await self._async_do_command( - # cons_command, ack.value - # ) - - # return result, success - - # async def _async_do_command( - # self, - # command: bytes, - # ack: bytes, - # ) -> tuple[str, bool]: - # retry_count = 0 - # while retry_count < 5: - # self.logger.debug("do_command sending command: %s", command) - # # send the command - # self.writer.write(command) - # try: - # await self.writer.drain() - # except asyncio.TimeoutError as err: - # result = f"Timeout sending command {err}" - # self.logger.error(result) - # return result, False - # except ConnectionError as err: - # # reaching this means the writer was closed somewhere - # self.logger.error(err) - # self.logger.debug("Restarting connection") - # # restart the connection - - # await self.reconnect() - # self.logger.debug("Sending command again") - # # restart the loop - # retry_count += 1 - # continue - - # # if we send a command that returns info, the projector will send - # # an ack, followed by the actual message. Check to see if the ack sent by - # # projector is correct, then return the message. - # ack_value = ( - # ack + Footer.footer.value - # ) - # self.logger.debug("constructed ack_value: %s", ack_value) - - # # see if we receive PJACK, if not, raise exception - # self.logger.debug("Waiting for command ok") - # msg_hb = await self.reader.read(4000) - # if msg_hb != ACKs.reply.name: - # result = f"Exception with heartbeat: {msg_hb}" - # self.logger.error(result) - # return result, False - # self.logger.debug("Command successful") - # # # Receive the acknowledgement from PJ - # # try: - # # # seems like certain commands timeout when PJ is off - # # received_ack = await asyncio.wait_for( - # # self.reader.readline(), timeout=self.command_read_timeout - # # ) - # # except asyncio.TimeoutError: - # # # LL is used in async_update() and I don't want to spam HA logs so we skip - # # # if not command == b"?\x89\x01PMLL\n": - # # # Sometimes if you send a command that is greyed out, the PJ will just hang - # # self.logger.error( - # # "Connection timed out. Command %s is probably not allowed to run at this time.", - # # command, - # # ) - # # self.logger.debug("restarting connection") - - # # await self.reconnect() - # # retry_count += 1 - # # continue - - # # except ConnectionRefusedError: - # # self.logger.error("Connection Refused when getting ack") - # # self.logger.debug("restarting connection") - - # # await self.reconnect() - # # retry_count += 1 - # # continue - - # # self.logger.debug("received ack from PJ: %s", received_ack) - - # # # This will probably never happen since we are handling timeouts now - # # if received_ack == b"": - # # self.logger.error("Got a blank ack. Restarting connection") - - # # await self.reconnect() - # # retry_count += 1 - # # continue - - # # # get the ack for operation - # # if received_ack == ack_value: - # # return received_ack, True - - # # # if we got what we expect and this is a reference, - # # # receive the data we requested - # # if received_ack == ack_value: - # # message = await self.reader.readline() - # # self.logger.debug("received message from PJ: %s", message) - - # # return message, True - - # # # Otherwise, it failed - # # # Because this now reuses a connection, reaching this stage means catastrophic failure, or HA running as usual :) - # # self.logger.error( - # # "Recieved ack did not match expected ack: %s != %s", - # # received_ack, - # # ack_value, - # # ) - # # # Try to restart connection, if we got here somethihng is out of sync - - # await self.reconnect() - # retry_count += 1 - # continue - - # self.logger.error("retry count for running commands exceeded") - # return "retry count exceeded", False - - - # def exec_command( - # self, command: Union[list[str], str], command_type: bytes = b"!" - # ) -> tuple[str, bool]: - # """ - # Sync wrapper for async_exec_command - # """ - - # return asyncio.run(self.async_exec_command(command, command_type)) - - # async def async_exec_command( - # self, command: Union[list[str], str], command_type: bytes = b"!" - # ) -> tuple[str, bool]: - # """ - # Wrapper for _send_command() - - # command: a str of the command and value, separated by a comma ("power,on"). - # or a list of commands - # This is to make home assistant UI use easier - # command_type: which operation, like ! or ? - - # Returns - # ( - # ack or error message: str, - # success flag: bool - # ) - # """ - # self.logger.debug("exec_command Executing command: %s", command) - # return await self._async_send_command(command, command_type) - - # def power_off( - # self, - # ) -> tuple[str, bool]: - # """ - # sync wrapper for async_power_off - # """ - # return asyncio.run(self.async_power_off()) - - # async def async_power_off(self) -> tuple[str, bool]: - # """ - # Turns off PJ - # """ - # return await self.async_exec_command("power_off") - - # async def _async_replace_headers(self, item: bytes) -> bytes: - # """ - # Will strip all headers and returns the value itself - # """ - # headers = [x.value for x in Header] + [x.value for x in Footer] - # for header in headers: - # item = item.replace(header, b"") - - # return item - - # async def _async_do_reference_op(self, command: str, ack: ACKs) -> tuple[str, bool]: - # cmd = ( - # Header.reference.value - # + Header.pj_unit.value - # + Commands[command].value[0] - # + Footer.close.value - # ) - - # msg, success = await self._async_send_command( - # cmd, - # ack=ACKs[ack.name].value, - # command_type=Header.reference.value, - # ) - - # if success: - # msg = await self._async_replace_headers(msg) - - # return msg, success - - - # async def async_get_input_level(self) -> str: - # """ - # Get the current input level - # """ - # state, _ = await self._async_do_reference_op( - # "input_level", ACKs.hdmi_ack - # ) - # return InputLevel(state.replace(ACKs.hdmi_ack.value, b"")).name - - # async def _async_get_power_state(self) -> str: - # """ - # Return the current power state - - # Returns str: values of PowerStates - # """ - # success = False - - # cmd = ( - # Header.reference.value - # + Header.pj_unit.value - # + Commands.power_status.value - # + Footer.close.value - # ) - # # try in case we get conn refused - # # Try to prevent power state flapping - # msg, success = await self._async_send_command( - # cmd, - # ack=ACKs.power_ack.value, - # command_type=Header.reference.value, - # ) - - # # Handle error with unexpected acks - # if not success: - # self.logger.error("Error getting power state: %s", msg) - # return success - - # # remove the headers - # state = await self._async_replace_headers(msg) - - # return PowerStates(state.replace(ACKs.power_ack.value, b"")).name - - # async def async_is_on(self) -> bool: - # """ - # True if the current state is on|reserved - # """ - # pw_status = [PowerStates.on.name] - # return await self._async_get_power_state() in pw_status - - # async def async_is_ll_on(self) -> bool: - # """ - # True if LL mode is on - # """ - # return await self.async_get_low_latency_state() == LowLatencyModes.on.name + # key_press, menu + command, value = raw_command.split(",") + except ValueError: + command = raw_command + skip_val = True + + self.logger.debug(command) + # Check if command is implemented + if not hasattr(Commands, command): + self.logger.error("Command not implemented: %s", command) + return b"", None + + # construct the command with nested Enums + command_name, val, is_info = Commands[command].value + if not skip_val: + self.logger.debug("command_name: %s", command_name) + self.logger.debug("val: %s", val[value.lstrip(" ")].value) + self.logger.debug("is info: %s", is_info.value) + command_base: bytes = command_name + b" " + val[value.lstrip(" ")].value + # Construct command based on required values + command: bytes = ( + command_base + Footer.footer.value + ) + else: + command: bytes = ( + command + Footer.footer.value + ) + self.logger.debug("command: %s", command) + + return command, is_info.value + + def send_command(self, command: str) -> str: + """send a given command""" + + cmd, is_info = self._construct_command(command) + if cmd is False: + return "Command not found" + + self.logger.debug("Sending command: %s", cmd) + self.logger.debug("Is informational %s", is_info) + + # Send the command + self.client.send(cmd) + + # read ack which should be ok + ack_reply = self.client.recv(self.read_limit) + self.logger.debug(ack_reply) + if ack_reply != ACKs.reply.value: + self.logger.error(ack_reply) + return "Ack OK not received when sending command", False + + if is_info: + # TODO: check if the response matches stuff in Notifications + # read response + res = self.client.recv(self.read_limit) + self.logger.debug(res) + return res + + return "ok" def print_commands(self) -> str: """ diff --git a/setup.py b/setup.py index 00fc1be..ef1056e 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setuptools.setup( name="py_madvr", - version="0.0.1", + version="1.0.0", author="iloveicedgreentea", description="A package to control MadVR Envy over IP", long_description=long_description, diff --git a/tests/testMadVR.py b/tests/testMadVR.py index ff6f4d5..22e5764 100644 --- a/tests/testMadVR.py +++ b/tests/testMadVR.py @@ -2,7 +2,7 @@ import os from dotenv import load_dotenv from madvr.madvr import Madvr -import asyncio +import time # load .env load_dotenv() @@ -10,15 +10,24 @@ host = os.getenv("MADVR_HOST") madvr = Madvr(host=host, connect_timeout=10) +madvr.open_connection() class TestMenu(unittest.TestCase): - def test_02lowlat(self): - out = asyncio.run( - madvr.async_send_command("PowerOff\r\n") - ) - print(out) + """Test suite""" + def test__construct_command(self): + """ensure it can construct a command""" -if __name__ == '__main__': - unittest.main() + cmd, isin = madvr._construct_command("key_press, menu") + + self.assertEqual(cmd, b'KeyPress MENU\r\n') + self.assertEqual(isin, False) + + def test_menuopen(self): + """Verify menu opens and closes""" - \ No newline at end of file + madvr.send_command("key_press, menu") + time.sleep(1) + madvr.send_command("key_press, menu") + +if __name__ == '__main__': + unittest.main() \ No newline at end of file