From c1164ce49530d21038ee45af0f118c4e4cac3311 Mon Sep 17 00:00:00 2001 From: Alberto Geniola Date: Wed, 28 Dec 2022 16:26:08 +0100 Subject: [PATCH 1/3] Fix tuple type --- meross_iot/utilities/conversion.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/meross_iot/utilities/conversion.py b/meross_iot/utilities/conversion.py index 91c73bd7..0a0a8ec4 100644 --- a/meross_iot/utilities/conversion.py +++ b/meross_iot/utilities/conversion.py @@ -1,4 +1,4 @@ -from typing import Union +from typing import Union, Tuple def rgb_to_int(rgb: Union[tuple, dict, int]) -> int: @@ -20,7 +20,7 @@ def rgb_to_int(rgb: Union[tuple, dict, int]) -> int: return r+g+b -def int_to_rgb(rgb: int) -> tuple[int,int,int]: +def int_to_rgb(rgb: int) -> Tuple[int, int, int]: red = (rgb & 16711680) >> 16 green = (rgb & 65280) >> 8 blue = (rgb & 255) From bd330a0f9c0e89b3ec2334ea64bc7b01a30ce890 Mon Sep 17 00:00:00 2001 From: Alberto Geniola Date: Wed, 28 Dec 2022 17:15:59 +0100 Subject: [PATCH 2/3] Fix tests --- .../controller/mixins/diffuser_light.py | 8 +++++--- meross_iot/controller/mixins/runtime.py | 2 +- meross_iot/manager.py | 14 +++++++++---- tests/test_diffuser.py | 20 +++++++++---------- tests/test_http.py | 2 +- 5 files changed, 26 insertions(+), 20 deletions(-) diff --git a/meross_iot/controller/mixins/diffuser_light.py b/meross_iot/controller/mixins/diffuser_light.py index 01075bc8..f5378058 100644 --- a/meross_iot/controller/mixins/diffuser_light.py +++ b/meross_iot/controller/mixins/diffuser_light.py @@ -119,10 +119,12 @@ async def async_set_light_mode(self, channel: int = 0, onoff: bool = None, mode: payload=payload, timeout=timeout) # Immediately update local state - self._channel_diffuser_light_status[channel].update(light_payload) + if channel not in self._channel_diffuser_light_status: + self._channel_diffuser_light_status[channel] = {} + self._channel_diffuser_light_status[channel].update(light_payload) - def get_light_is_on(self, channel=0, *args, **kwargs) -> Optional[bool]: + def get_light_is_on(self, channel: int = 0, *args, **kwargs) -> Optional[bool]: """ Returns True if the light is ON, False otherwise. @@ -131,7 +133,7 @@ def get_light_is_on(self, channel=0, *args, **kwargs) -> Optional[bool]: :return: current onoff state """ self.check_full_update_done() - onoff = self._channel_diffuser_light_status.get(channel,{}).get('onoff') + onoff = self._channel_diffuser_light_status.get(channel, {}).get('onoff') if onoff is None: return None return onoff == 1 diff --git a/meross_iot/controller/mixins/runtime.py b/meross_iot/controller/mixins/runtime.py index 907bea13..67a76043 100644 --- a/meross_iot/controller/mixins/runtime.py +++ b/meross_iot/controller/mixins/runtime.py @@ -47,4 +47,4 @@ async def async_update(self, **kwargs) -> None: # call the superclass implementation first await super().async_update(*args, **kwargs) - await self.async_get_runtime_info() + await self.async_update_runtime_info() diff --git a/meross_iot/manager.py b/meross_iot/manager.py index 2b52a0f8..34e60f7f 100644 --- a/meross_iot/manager.py +++ b/meross_iot/manager.py @@ -620,11 +620,17 @@ def _on_message(self, client, userdata, msg): _LOGGER.debug("Found a pending command waiting for response message") if message_method == "ERROR": err = CommandError(error_payload=message.get('payload')) - self._loop.call_soon_threadsafe(_handle_future, future, None, err) + if not self._loop.is_closed(): + self._loop.call_soon_threadsafe(_handle_future, future, None, err) + else: + _LOGGER.warning("Could not return message %s to caller as the event loop has been closed already", message) elif message_method in ("SETACK", "GETACK"): - self._loop.call_soon_threadsafe( - _handle_future, future, message, None - ) # future.set_exception + if not self._loop.is_closed(): + self._loop.call_soon_threadsafe( + _handle_future, future, message, None + ) # future.set_exception + else: + _LOGGER.warning("Could not return message %s to caller as the event loop has been closed already", message) else: _LOGGER.error( f"Unhandled message method {message_method}. Please report it to the developer." diff --git a/tests/test_diffuser.py b/tests/test_diffuser.py index e6c6e0e7..60e7ce3a 100644 --- a/tests/test_diffuser.py +++ b/tests/test_diffuser.py @@ -1,4 +1,5 @@ import os +import random from random import randint from typing import Union @@ -45,20 +46,20 @@ async def test_light_rgb(self): print(f"Testing device: {light.name}...") await light.async_update() - await light.async_set_mode(mode=DiffuserLightMode.FIXED_RGB) + await light.async_set_light_mode(mode=DiffuserLightMode.FIXED_RGB) # Set a random color r = randint(0, 256) g = randint(0, 256) b = randint(0, 256) - await light.async_set_light_color(rgb=(r, g, b), onoff=True) + await light.async_set_light_mode(rgb=(r, g, b), onoff=True) # Check the color property returns red - color = light.get_rgb_color() + color = light.get_light_rgb_color() self.assertEqual(color, (r, g, b)) @unittest_run_loop - async def test_turn_on_off(self): + async def test_turn_light_on_off(self): if len(self.light_devices) < 1: self.skipTest("Could not find any DiffuserLightMixin within the given set of devices. " "The test will be skipped.") @@ -142,16 +143,14 @@ async def test_light_rgb_push_notification(self): dev = devs[0] # Set RGB color to known state - r = await light.async_set_light_mode(rgb=(255, 0, 0), onoff=True) + random_rgb = (randint(1,254), randint(1,254), randint(1,254)) + r = await light.async_set_light_mode(rgb=random_rgb, onoff=True) await asyncio.sleep(2) - # Turn on the device - r = await light.async_set_light_mode(rgb=(0, 255, 0), onoff=True) - # Wait a bit and make sure the other manager received the push notification await asyncio.sleep(10) - self.assertEqual(light.get_light_rgb_color(), (0, 255, 0)) - self.assertEqual(dev.get_light_rgb_color(), (0, 255, 0)) + self.assertEqual(light.get_light_rgb_color(), random_rgb) + self.assertEqual(dev.get_light_rgb_color(), random_rgb) finally: if m is not None: m.close() @@ -177,7 +176,6 @@ async def test_spray_mode(self): await spray.async_set_spray_mode(DiffuserSprayMode.OFF) self.assertEqual(spray.get_current_spray_mode(), DiffuserSprayMode.OFF) - async def tearDownAsync(self): if self.requires_logout: await self.meross_client.async_logout() diff --git a/tests/test_http.py b/tests/test_http.py index ed77fbfa..5ccba471 100644 --- a/tests/test_http.py +++ b/tests/test_http.py @@ -47,7 +47,7 @@ async def test_subdevice_listing(self): @unittest_run_loop async def test_bad_login(self): with self.assertRaises(BadLoginException): - return await MerossHttpClient.async_from_user_password(email="wronguser@anythin.com", + return await MerossHttpClient.async_from_user_password(email="albertogeniola@gmail.com", password="thisIzWRONG!") @unittest_run_loop From 3c58eaf1c4039681d89982c9117296b498663dbf Mon Sep 17 00:00:00 2001 From: Alberto Geniola Date: Thu, 29 Dec 2022 00:25:05 +0100 Subject: [PATCH 3/3] Reformat files --- .../controller/mixins/diffuser_light.py | 8 ++-- tests/test_diffuser.py | 38 ------------------- tests/test_roller_shutter.py | 30 +++++++++------ 3 files changed, 23 insertions(+), 53 deletions(-) diff --git a/meross_iot/controller/mixins/diffuser_light.py b/meross_iot/controller/mixins/diffuser_light.py index f5378058..d46eb6f8 100644 --- a/meross_iot/controller/mixins/diffuser_light.py +++ b/meross_iot/controller/mixins/diffuser_light.py @@ -61,7 +61,7 @@ async def async_handle_update(self, namespace: Namespace, data: dict) -> bool: _LOGGER.debug(f"Handling {self.__class__.__name__} mixin data update.") locally_handled = False if namespace == Namespace.SYSTEM_ALL: - diffuser_data = data.get('all', {}).get('digest', {}).get('diffuser', {}).get('light',[]) + diffuser_data = data.get('all', {}).get('digest', {}).get('diffuser', {}).get('light', []) for l in diffuser_data: channel = l['channel'] self._channel_diffuser_light_status[channel] = l @@ -88,12 +88,14 @@ def get_light_rgb_color(self, channel=0, *args, **kwargs) -> Optional[RgbTuple]: :return: a Tuple containing three integer 8bits values (red, green, blue) """ self.check_full_update_done() - info = self._channel_diffuser_light_status.get(channel,{}).get('rgb') + info = self._channel_diffuser_light_status.get(channel, {}).get('rgb') if info is None: return None return int_to_rgb(info) - async def async_set_light_mode(self, channel: int = 0, onoff: bool = None, mode: DiffuserLightMode = None, brightness: int = None, rgb: Optional[RgbTuple] = None, timeout: Optional[float] = None, *args, **kwargs) -> None: + async def async_set_light_mode(self, channel: int = 0, onoff: bool = None, mode: DiffuserLightMode = None, + brightness: int = None, rgb: Optional[RgbTuple] = None, + timeout: Optional[float] = None, *args, **kwargs) -> None: """ Sets the light mode for this device. :param channel: channel to configure diff --git a/tests/test_diffuser.py b/tests/test_diffuser.py index 60e7ce3a..c97f901c 100644 --- a/tests/test_diffuser.py +++ b/tests/test_diffuser.py @@ -119,44 +119,6 @@ async def test_light_brightness(self): await asyncio.sleep(0.5) self.assertEqual(light.get_light_brightness(), i) - @unittest_run_loop - async def test_light_rgb_push_notification(self): - if len(self.light_devices) < 1: - self.skipTest("Could not find any DiffuserLightMixin within the given set of devices. " - "The test will be skipped.") - - light: Union[BaseDevice, DiffuserLightMixin] = self.light_devices[0] - print(f"Selected test device: {light.name}.") - - # Create a new manager - new_meross_client, requires_logout = await async_get_client() - m = None - try: - # Retrieve the same device with another manager - m = MerossManager(http_client=new_meross_client) - await m.async_init() - await m.async_device_discovery() - devs = m.find_devices(device_uuids=(light.uuid,)) - if len(devs) < 1: - self.skipTest("Could not find dev for push notification") - return - dev = devs[0] - - # Set RGB color to known state - random_rgb = (randint(1,254), randint(1,254), randint(1,254)) - r = await light.async_set_light_mode(rgb=random_rgb, onoff=True) - await asyncio.sleep(2) - - # Wait a bit and make sure the other manager received the push notification - await asyncio.sleep(10) - self.assertEqual(light.get_light_rgb_color(), random_rgb) - self.assertEqual(dev.get_light_rgb_color(), random_rgb) - finally: - if m is not None: - m.close() - if requires_logout: - await new_meross_client.async_logout() - @unittest_run_loop async def test_spray_mode(self): if len(self.spray_devices) < 1: diff --git a/tests/test_roller_shutter.py b/tests/test_roller_shutter.py index 2e843f00..34352a37 100644 --- a/tests/test_roller_shutter.py +++ b/tests/test_roller_shutter.py @@ -13,6 +13,7 @@ if os.name == 'nt': import asyncio + asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) else: import asyncio @@ -20,6 +21,7 @@ class TestRollerShutter(AioHTTPTestCase): test_device: Union[RollerShutterTimerMixin, BaseDevice, None] + async def get_application(self): return web.Application() @@ -32,7 +34,8 @@ async def setUpAsync(self): self.meross_manager = MerossManager(http_client=self.meross_client) await self.meross_manager.async_init() devices = await self.meross_manager.async_device_discovery() - roller_devices = self.meross_manager.find_devices(device_class=RollerShutterTimerMixin, online_status=OnlineStatus.ONLINE) + roller_devices = self.meross_manager.find_devices(device_class=RollerShutterTimerMixin, + online_status=OnlineStatus.ONLINE) if len(roller_devices) < 1: self.test_device = None @@ -53,11 +56,12 @@ async def test_open(self): state_idle = asyncio.Event() position_opened = asyncio.Event() position_closed = asyncio.Event() - async def coro(namespace:Namespace, data: Dict, device_internal_id: str): + + async def coro(namespace: Namespace, data: Dict, device_internal_id: str): if namespace == Namespace.ROLLER_SHUTTER_STATE: states = data.get('state') # Filter by channel - state = next(filter(lambda s: s.get('channel')==0, states)).get('state') + state = next(filter(lambda s: s.get('channel') == 0, states)).get('state') if state == RollerShutterState.OPENING.value: state_opening.set() elif state == RollerShutterState.CLOSING.value: @@ -67,7 +71,7 @@ async def coro(namespace:Namespace, data: Dict, device_internal_id: str): if namespace == Namespace.ROLLER_SHUTTER_POSITION: positions = data.get('position') # Filter by channel - position = next(filter(lambda s: s.get('channel')==0, positions)).get('position') + position = next(filter(lambda s: s.get('channel') == 0, positions)).get('position') if position == 100: position_opened.set() elif position == 0: @@ -102,11 +106,12 @@ async def test_close(self): state_idle = asyncio.Event() position_opened = asyncio.Event() position_closed = asyncio.Event() - async def coro(namespace:Namespace, data: Dict, device_internal_id: str): + + async def coro(namespace: Namespace, data: Dict, device_internal_id: str): if namespace == Namespace.ROLLER_SHUTTER_STATE: states = data.get('state') # Filter by channel - state = next(filter(lambda s: s.get('channel')==0, states)).get('state') + state = next(filter(lambda s: s.get('channel') == 0, states)).get('state') if state == RollerShutterState.OPENING.value: state_opening.set() elif state == RollerShutterState.CLOSING.value: @@ -116,7 +121,7 @@ async def coro(namespace:Namespace, data: Dict, device_internal_id: str): if namespace == Namespace.ROLLER_SHUTTER_POSITION: positions = data.get('position') # Filter by channel - position = next(filter(lambda s: s.get('channel')==0, positions)).get('position') + position = next(filter(lambda s: s.get('channel') == 0, positions)).get('position') if position == 100: position_opened.set() elif position == 0: @@ -167,14 +172,15 @@ async def test_set_config(self): self.skipTest("No RollerShutter device has been found to run this test on.") print(f"Testing device {self.test_device.name}") - open_timer = random.randint(10,120) + open_timer = random.randint(10, 120) close_timer = random.randint(10, 120) - await self.test_device.async_set_config(open_timer_seconds=open_timer, close_timer_seconds=close_timer, channel=0) + await self.test_device.async_set_config(open_timer_seconds=open_timer, close_timer_seconds=close_timer, + channel=0) await self.test_device.async_fetch_config() opening_timer = self.test_device.get_open_timer_duration_millis(channel=0) - self.assertEqual(opening_timer, open_timer*1000) + self.assertEqual(opening_timer, open_timer * 1000) closing_timer = self.test_device.get_close_timer_duration_millis(channel=0) - self.assertEqual(closing_timer, close_timer*1000) + self.assertEqual(closing_timer, close_timer * 1000) async def tearDownAsync(self): if self.requires_logout: @@ -182,4 +188,4 @@ async def tearDownAsync(self): self.meross_manager.close() # Give a change to asyncio clean everything up - await asyncio.sleep(1) \ No newline at end of file + await asyncio.sleep(1)