# switchbot-mqtt - MQTT client controlling SwitchBot button & curtain automators, # compatible with home-assistant.io's MQTT Switch & Cover platform # # Copyright (C) 2022 Fabian Peter Hammerle # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import logging import unittest.mock import aiomqtt import bleak import _pytest.logging # pylint: disable=import-private-name; typing import pytest # pylint: disable=import-private-name; internal from switchbot_mqtt._actors import _CurtainMotor # pylint: disable=protected-access @pytest.mark.asyncio @pytest.mark.parametrize( ("topic", "payload", "expected_mac_address", "expected_position_percent"), [ ( "home/cover/switchbot-curtain/aa:bb:cc:dd:ee:ff/position/set-percent", b"42", "aa:bb:cc:dd:ee:ff", 42, ), ( "home/cover/switchbot-curtain/11:22:33:44:55:66/position/set-percent", b"0", "11:22:33:44:55:66", 0, ), ( "home/cover/switchbot-curtain/11:22:33:44:55:66/position/set-percent", b"100", "11:22:33:44:55:66", 100, ), ], ) @pytest.mark.parametrize("retry_count", (3, 42)) async def test__mqtt_set_position_callback( caplog: _pytest.logging.LogCaptureFixture, topic: str, payload: bytes, expected_mac_address: str, retry_count: int, expected_position_percent: int, ) -> None: message = aiomqtt.Message( topic=topic, payload=payload, qos=0, retain=False, mid=0, properties=None ) device = unittest.mock.Mock() device.address = expected_mac_address with unittest.mock.patch.object( bleak.BleakScanner, "find_device_by_address", return_value=device ), unittest.mock.patch( "switchbot.SwitchbotCurtain.__init__", return_value=None ) as device_init_mock, unittest.mock.patch( "switchbot.SwitchbotCurtain.set_position" ) as set_position_mock, caplog.at_level( logging.DEBUG ): await _CurtainMotor._mqtt_set_position_callback( mqtt_client=unittest.mock.Mock(), message=message, retry_count=retry_count, device_passwords={}, fetch_device_info=False, mqtt_topic_prefix="home/", ) device_init_mock.assert_called_once_with( device=device, password=None, retry_count=retry_count, reverse_mode=True ) set_position_mock.assert_called_once_with(expected_position_percent) assert caplog.record_tuples == [ ( "switchbot_mqtt._actors", logging.DEBUG, f"received topic=home/cover/switchbot-curtain/{expected_mac_address}" f"/position/set-percent payload=b'{expected_position_percent}'", ), ( "switchbot_mqtt._actors", logging.INFO, f"set position of switchbot curtain {expected_mac_address}" f" to {expected_position_percent}%", ), ] @pytest.mark.asyncio async def test__mqtt_set_position_callback_ignore_retained( caplog: _pytest.logging.LogCaptureFixture, ) -> None: message = aiomqtt.Message( topic="homeassistant/cover/switchbot-curtain/aa:bb:cc:dd:ee:ff/position/set-percent", payload=b"42", qos=0, retain=True, mid=0, properties=None, ) with unittest.mock.patch( "switchbot.SwitchbotCurtain" ) as device_init_mock, caplog.at_level(logging.INFO): await _CurtainMotor._mqtt_set_position_callback( mqtt_client=unittest.mock.Mock(), message=message, retry_count=3, device_passwords={}, fetch_device_info=False, mqtt_topic_prefix="whatever", ) device_init_mock.assert_not_called() assert caplog.record_tuples == [ ( "switchbot_mqtt._actors", logging.INFO, "ignoring retained message on topic" " homeassistant/cover/switchbot-curtain/aa:bb:cc:dd:ee:ff/position/set-percent", ), ] @pytest.mark.asyncio async def test__mqtt_set_position_callback_unexpected_topic( caplog: _pytest.logging.LogCaptureFixture, ) -> None: message = aiomqtt.Message( topic="switchbot-curtain/aa:bb:cc:dd:ee:ff/position/set", payload=b"42", qos=0, retain=False, mid=0, properties=None, ) with unittest.mock.patch( "switchbot.SwitchbotCurtain" ) as device_init_mock, caplog.at_level(logging.INFO): await _CurtainMotor._mqtt_set_position_callback( mqtt_client=unittest.mock.Mock(), message=message, retry_count=3, device_passwords={}, fetch_device_info=False, mqtt_topic_prefix="", ) device_init_mock.assert_not_called() assert caplog.record_tuples == [ ( "switchbot_mqtt._actors.base", logging.WARN, "unexpected topic switchbot-curtain/aa:bb:cc:dd:ee:ff/position/set", ), ] @pytest.mark.asyncio async def test__mqtt_set_position_callback_invalid_mac_address( caplog: _pytest.logging.LogCaptureFixture, ) -> None: message = aiomqtt.Message( topic="tnatsissaemoh/cover/switchbot-curtain/aa:bb:cc:dd:ee/position/set-percent", payload=b"42", qos=0, retain=False, mid=0, properties=None, ) with unittest.mock.patch( "switchbot.SwitchbotCurtain" ) as device_init_mock, caplog.at_level(logging.INFO): await _CurtainMotor._mqtt_set_position_callback( mqtt_client=unittest.mock.Mock(), message=message, retry_count=3, device_passwords={}, fetch_device_info=False, mqtt_topic_prefix="tnatsissaemoh/", ) device_init_mock.assert_not_called() assert caplog.record_tuples == [ ( "switchbot_mqtt._actors.base", logging.WARN, "invalid mac address aa:bb:cc:dd:ee", ), ] @pytest.mark.asyncio @pytest.mark.parametrize("payload", [b"-1", b"123"]) async def test__mqtt_set_position_callback_invalid_position( caplog: _pytest.logging.LogCaptureFixture, payload: bytes, ) -> None: message = aiomqtt.Message( topic="homeassistant/cover/switchbot-curtain/aa:bb:cc:dd:ee:ff/position/set-percent", payload=payload, qos=0, retain=False, mid=0, properties=None, ) with unittest.mock.patch.object( bleak.BleakScanner, "find_device_by_address" ), unittest.mock.patch( "switchbot.SwitchbotCurtain" ) as device_init_mock, caplog.at_level( logging.INFO ): await _CurtainMotor._mqtt_set_position_callback( mqtt_client=unittest.mock.Mock(), message=message, retry_count=3, device_passwords={}, fetch_device_info=False, mqtt_topic_prefix="homeassistant/", ) device_init_mock.assert_called_once() device_init_mock().set_position.assert_not_called() assert caplog.record_tuples == [ ( "switchbot_mqtt._actors", logging.WARN, f"invalid position {payload.decode()}%, ignoring message", ), ] @pytest.mark.asyncio async def test__mqtt_set_position_callback_command_failed( caplog: _pytest.logging.LogCaptureFixture, ) -> None: message = aiomqtt.Message( topic="cover/switchbot-curtain/aa:bb:cc:dd:ee:ff/position/set-percent", payload=b"21", qos=0, retain=False, mid=0, properties=None, ) device = unittest.mock.Mock() device.address = "aa:bb:cc:dd:ee:ff" with unittest.mock.patch.object( bleak.BleakScanner, "find_device_by_address", return_value=device ), unittest.mock.patch( "switchbot.SwitchbotCurtain.__init__", return_value=None ) as device_init_mock, unittest.mock.patch( "switchbot.SwitchbotCurtain.set_position", return_value=False ) as set_position_mock, caplog.at_level( logging.INFO ): await _CurtainMotor._mqtt_set_position_callback( mqtt_client=unittest.mock.Mock(), message=message, retry_count=3, device_passwords={}, fetch_device_info=False, mqtt_topic_prefix="", ) device_init_mock.assert_called_once() set_position_mock.assert_awaited_with(21) assert caplog.record_tuples == [ ( "switchbot_mqtt._actors", logging.ERROR, "failed to set position of switchbot curtain aa:bb:cc:dd:ee:ff", ), ]