123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287 |
- # 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 <fabian@hammerle.me>
- #
- # 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 <https://www.gnu.org/licenses/>.
- 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",
- ),
- ]
|