PySwitchbot 0.67.0__tar.gz → 0.68.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pyswitchbot-0.67.0 → pyswitchbot-0.68.1}/PKG-INFO +1 -1
- {pyswitchbot-0.67.0 → pyswitchbot-0.68.1}/PySwitchbot.egg-info/PKG-INFO +1 -1
- {pyswitchbot-0.67.0 → pyswitchbot-0.68.1}/setup.py +1 -1
- {pyswitchbot-0.67.0 → pyswitchbot-0.68.1}/switchbot/__init__.py +6 -1
- {pyswitchbot-0.67.0 → pyswitchbot-0.68.1}/switchbot/devices/device.py +15 -1
- {pyswitchbot-0.67.0 → pyswitchbot-0.68.1}/switchbot/devices/lock.py +6 -0
- {pyswitchbot-0.67.0 → pyswitchbot-0.68.1}/switchbot/devices/relay_switch.py +1 -0
- {pyswitchbot-0.67.0 → pyswitchbot-0.68.1}/tests/test_encrypted_device.py +19 -0
- {pyswitchbot-0.67.0 → pyswitchbot-0.68.1}/tests/test_lock.py +29 -0
- {pyswitchbot-0.67.0 → pyswitchbot-0.68.1}/tests/test_relay_switch.py +11 -0
- {pyswitchbot-0.67.0 → pyswitchbot-0.68.1}/LICENSE +0 -0
- {pyswitchbot-0.67.0 → pyswitchbot-0.68.1}/MANIFEST.in +0 -0
- {pyswitchbot-0.67.0 → pyswitchbot-0.68.1}/PySwitchbot.egg-info/SOURCES.txt +0 -0
- {pyswitchbot-0.67.0 → pyswitchbot-0.68.1}/PySwitchbot.egg-info/dependency_links.txt +0 -0
- {pyswitchbot-0.67.0 → pyswitchbot-0.68.1}/PySwitchbot.egg-info/requires.txt +0 -0
- {pyswitchbot-0.67.0 → pyswitchbot-0.68.1}/PySwitchbot.egg-info/top_level.txt +0 -0
- {pyswitchbot-0.67.0 → pyswitchbot-0.68.1}/README.md +0 -0
- {pyswitchbot-0.67.0 → pyswitchbot-0.68.1}/pyproject.toml +0 -0
- {pyswitchbot-0.67.0 → pyswitchbot-0.68.1}/setup.cfg +0 -0
- {pyswitchbot-0.67.0 → pyswitchbot-0.68.1}/switchbot/adv_parser.py +0 -0
- {pyswitchbot-0.67.0 → pyswitchbot-0.68.1}/switchbot/adv_parsers/__init__.py +0 -0
- {pyswitchbot-0.67.0 → pyswitchbot-0.68.1}/switchbot/adv_parsers/air_purifier.py +0 -0
- {pyswitchbot-0.67.0 → pyswitchbot-0.68.1}/switchbot/adv_parsers/blind_tilt.py +0 -0
- {pyswitchbot-0.67.0 → pyswitchbot-0.68.1}/switchbot/adv_parsers/bot.py +0 -0
- {pyswitchbot-0.67.0 → pyswitchbot-0.68.1}/switchbot/adv_parsers/bulb.py +0 -0
- {pyswitchbot-0.67.0 → pyswitchbot-0.68.1}/switchbot/adv_parsers/ceiling_light.py +0 -0
- {pyswitchbot-0.67.0 → pyswitchbot-0.68.1}/switchbot/adv_parsers/contact.py +0 -0
- {pyswitchbot-0.67.0 → pyswitchbot-0.68.1}/switchbot/adv_parsers/curtain.py +0 -0
- {pyswitchbot-0.67.0 → pyswitchbot-0.68.1}/switchbot/adv_parsers/fan.py +0 -0
- {pyswitchbot-0.67.0 → pyswitchbot-0.68.1}/switchbot/adv_parsers/hub2.py +0 -0
- {pyswitchbot-0.67.0 → pyswitchbot-0.68.1}/switchbot/adv_parsers/hub3.py +0 -0
- {pyswitchbot-0.67.0 → pyswitchbot-0.68.1}/switchbot/adv_parsers/hubmini_matter.py +0 -0
- {pyswitchbot-0.67.0 → pyswitchbot-0.68.1}/switchbot/adv_parsers/humidifier.py +0 -0
- {pyswitchbot-0.67.0 → pyswitchbot-0.68.1}/switchbot/adv_parsers/keypad.py +0 -0
- {pyswitchbot-0.67.0 → pyswitchbot-0.68.1}/switchbot/adv_parsers/leak.py +0 -0
- {pyswitchbot-0.67.0 → pyswitchbot-0.68.1}/switchbot/adv_parsers/light_strip.py +0 -0
- {pyswitchbot-0.67.0 → pyswitchbot-0.68.1}/switchbot/adv_parsers/lock.py +0 -0
- {pyswitchbot-0.67.0 → pyswitchbot-0.68.1}/switchbot/adv_parsers/meter.py +0 -0
- {pyswitchbot-0.67.0 → pyswitchbot-0.68.1}/switchbot/adv_parsers/motion.py +0 -0
- {pyswitchbot-0.67.0 → pyswitchbot-0.68.1}/switchbot/adv_parsers/plug.py +0 -0
- {pyswitchbot-0.67.0 → pyswitchbot-0.68.1}/switchbot/adv_parsers/relay_switch.py +0 -0
- {pyswitchbot-0.67.0 → pyswitchbot-0.68.1}/switchbot/adv_parsers/remote.py +0 -0
- {pyswitchbot-0.67.0 → pyswitchbot-0.68.1}/switchbot/adv_parsers/roller_shade.py +0 -0
- {pyswitchbot-0.67.0 → pyswitchbot-0.68.1}/switchbot/adv_parsers/vacuum.py +0 -0
- {pyswitchbot-0.67.0 → pyswitchbot-0.68.1}/switchbot/api_config.py +0 -0
- {pyswitchbot-0.67.0 → pyswitchbot-0.68.1}/switchbot/const/__init__.py +0 -0
- {pyswitchbot-0.67.0 → pyswitchbot-0.68.1}/switchbot/const/air_purifier.py +0 -0
- {pyswitchbot-0.67.0 → pyswitchbot-0.68.1}/switchbot/const/evaporative_humidifier.py +0 -0
- {pyswitchbot-0.67.0 → pyswitchbot-0.68.1}/switchbot/const/fan.py +0 -0
- {pyswitchbot-0.67.0 → pyswitchbot-0.68.1}/switchbot/const/hub2.py +0 -0
- {pyswitchbot-0.67.0 → pyswitchbot-0.68.1}/switchbot/const/hub3.py +0 -0
- {pyswitchbot-0.67.0 → pyswitchbot-0.68.1}/switchbot/const/light.py +0 -0
- {pyswitchbot-0.67.0 → pyswitchbot-0.68.1}/switchbot/const/lock.py +0 -0
- {pyswitchbot-0.67.0 → pyswitchbot-0.68.1}/switchbot/devices/__init__.py +0 -0
- {pyswitchbot-0.67.0 → pyswitchbot-0.68.1}/switchbot/devices/air_purifier.py +0 -0
- {pyswitchbot-0.67.0 → pyswitchbot-0.68.1}/switchbot/devices/base_cover.py +0 -0
- {pyswitchbot-0.67.0 → pyswitchbot-0.68.1}/switchbot/devices/base_light.py +0 -0
- {pyswitchbot-0.67.0 → pyswitchbot-0.68.1}/switchbot/devices/blind_tilt.py +0 -0
- {pyswitchbot-0.67.0 → pyswitchbot-0.68.1}/switchbot/devices/bot.py +0 -0
- {pyswitchbot-0.67.0 → pyswitchbot-0.68.1}/switchbot/devices/bulb.py +0 -0
- {pyswitchbot-0.67.0 → pyswitchbot-0.68.1}/switchbot/devices/ceiling_light.py +0 -0
- {pyswitchbot-0.67.0 → pyswitchbot-0.68.1}/switchbot/devices/contact.py +0 -0
- {pyswitchbot-0.67.0 → pyswitchbot-0.68.1}/switchbot/devices/curtain.py +0 -0
- {pyswitchbot-0.67.0 → pyswitchbot-0.68.1}/switchbot/devices/evaporative_humidifier.py +0 -0
- {pyswitchbot-0.67.0 → pyswitchbot-0.68.1}/switchbot/devices/fan.py +0 -0
- {pyswitchbot-0.67.0 → pyswitchbot-0.68.1}/switchbot/devices/humidifier.py +0 -0
- {pyswitchbot-0.67.0 → pyswitchbot-0.68.1}/switchbot/devices/keypad.py +0 -0
- {pyswitchbot-0.67.0 → pyswitchbot-0.68.1}/switchbot/devices/light_strip.py +0 -0
- {pyswitchbot-0.67.0 → pyswitchbot-0.68.1}/switchbot/devices/meter.py +0 -0
- {pyswitchbot-0.67.0 → pyswitchbot-0.68.1}/switchbot/devices/motion.py +0 -0
- {pyswitchbot-0.67.0 → pyswitchbot-0.68.1}/switchbot/devices/plug.py +0 -0
- {pyswitchbot-0.67.0 → pyswitchbot-0.68.1}/switchbot/devices/roller_shade.py +0 -0
- {pyswitchbot-0.67.0 → pyswitchbot-0.68.1}/switchbot/devices/vacuum.py +0 -0
- {pyswitchbot-0.67.0 → pyswitchbot-0.68.1}/switchbot/discovery.py +0 -0
- {pyswitchbot-0.67.0 → pyswitchbot-0.68.1}/switchbot/enum.py +0 -0
- {pyswitchbot-0.67.0 → pyswitchbot-0.68.1}/switchbot/helpers.py +0 -0
- {pyswitchbot-0.67.0 → pyswitchbot-0.68.1}/switchbot/models.py +0 -0
- {pyswitchbot-0.67.0 → pyswitchbot-0.68.1}/tests/test_adv_parser.py +0 -0
- {pyswitchbot-0.67.0 → pyswitchbot-0.68.1}/tests/test_air_purifier.py +0 -0
- {pyswitchbot-0.67.0 → pyswitchbot-0.68.1}/tests/test_base_cover.py +0 -0
- {pyswitchbot-0.67.0 → pyswitchbot-0.68.1}/tests/test_blind_tilt.py +0 -0
- {pyswitchbot-0.67.0 → pyswitchbot-0.68.1}/tests/test_bulb.py +0 -0
- {pyswitchbot-0.67.0 → pyswitchbot-0.68.1}/tests/test_ceiling_light.py +0 -0
- {pyswitchbot-0.67.0 → pyswitchbot-0.68.1}/tests/test_colormode_imports.py +0 -0
- {pyswitchbot-0.67.0 → pyswitchbot-0.68.1}/tests/test_curtain.py +0 -0
- {pyswitchbot-0.67.0 → pyswitchbot-0.68.1}/tests/test_evaporative_humidifier.py +0 -0
- {pyswitchbot-0.67.0 → pyswitchbot-0.68.1}/tests/test_fan.py +0 -0
- {pyswitchbot-0.67.0 → pyswitchbot-0.68.1}/tests/test_helpers.py +0 -0
- {pyswitchbot-0.67.0 → pyswitchbot-0.68.1}/tests/test_hub2.py +0 -0
- {pyswitchbot-0.67.0 → pyswitchbot-0.68.1}/tests/test_hub3.py +0 -0
- {pyswitchbot-0.67.0 → pyswitchbot-0.68.1}/tests/test_roller_shade.py +0 -0
- {pyswitchbot-0.67.0 → pyswitchbot-0.68.1}/tests/test_strip_light.py +0 -0
- {pyswitchbot-0.67.0 → pyswitchbot-0.68.1}/tests/test_vacuum.py +0 -0
|
@@ -32,7 +32,11 @@ from .devices.bot import Switchbot
|
|
|
32
32
|
from .devices.bulb import SwitchbotBulb
|
|
33
33
|
from .devices.ceiling_light import SwitchbotCeilingLight
|
|
34
34
|
from .devices.curtain import SwitchbotCurtain
|
|
35
|
-
from .devices.device import
|
|
35
|
+
from .devices.device import (
|
|
36
|
+
SwitchbotDevice,
|
|
37
|
+
SwitchbotEncryptedDevice,
|
|
38
|
+
SwitchbotOperationError,
|
|
39
|
+
)
|
|
36
40
|
from .devices.evaporative_humidifier import SwitchbotEvaporativeHumidifier
|
|
37
41
|
from .devices.fan import SwitchbotFan
|
|
38
42
|
from .devices.humidifier import SwitchbotHumidifier
|
|
@@ -78,6 +82,7 @@ __all__ = [
|
|
|
78
82
|
"SwitchbotLock",
|
|
79
83
|
"SwitchbotModel",
|
|
80
84
|
"SwitchbotModel",
|
|
85
|
+
"SwitchbotOperationError",
|
|
81
86
|
"SwitchbotPlugMini",
|
|
82
87
|
"SwitchbotPlugMini",
|
|
83
88
|
"SwitchbotRelaySwitch",
|
|
@@ -127,6 +127,7 @@ class SwitchbotBaseDevice:
|
|
|
127
127
|
|
|
128
128
|
_turn_on_command: str | None = None
|
|
129
129
|
_turn_off_command: str | None = None
|
|
130
|
+
_press_command: str | None = None
|
|
130
131
|
|
|
131
132
|
def __init__(
|
|
132
133
|
self,
|
|
@@ -294,7 +295,7 @@ class SwitchbotBaseDevice:
|
|
|
294
295
|
"""Return RSSI of device."""
|
|
295
296
|
if self._sb_adv_data:
|
|
296
297
|
return self._sb_adv_data.rssi
|
|
297
|
-
return
|
|
298
|
+
return -127
|
|
298
299
|
|
|
299
300
|
async def _ensure_connected(self):
|
|
300
301
|
"""Ensure connection to device is established."""
|
|
@@ -718,6 +719,13 @@ class SwitchbotBaseDevice:
|
|
|
718
719
|
result = await self._send_command(self._turn_off_command)
|
|
719
720
|
return self._check_command_result(result, 0, {1})
|
|
720
721
|
|
|
722
|
+
@update_after_operation
|
|
723
|
+
async def press(self) -> bool:
|
|
724
|
+
"""Press the device."""
|
|
725
|
+
self._check_function_support(self._press_command)
|
|
726
|
+
result = await self._send_command(self._press_command)
|
|
727
|
+
return self._check_command_result(result, 0, {1})
|
|
728
|
+
|
|
721
729
|
|
|
722
730
|
class SwitchbotDevice(SwitchbotBaseDevice):
|
|
723
731
|
"""
|
|
@@ -947,6 +955,12 @@ class SwitchbotEncryptedDevice(SwitchbotDevice):
|
|
|
947
955
|
if len(data) == 0:
|
|
948
956
|
return b""
|
|
949
957
|
if self._iv is None:
|
|
958
|
+
if self._expected_disconnect:
|
|
959
|
+
_LOGGER.debug(
|
|
960
|
+
"%s: Cannot decrypt, IV is None during expected disconnect",
|
|
961
|
+
self.name,
|
|
962
|
+
)
|
|
963
|
+
return b""
|
|
950
964
|
raise RuntimeError("Cannot decrypt: IV is None")
|
|
951
965
|
decryptor = self._get_cipher().decryptor()
|
|
952
966
|
return decryptor.update(data) + decryptor.finalize()
|
|
@@ -214,6 +214,12 @@ class SwitchbotLock(SwitchbotSequenceDevice, SwitchbotEncryptedDevice):
|
|
|
214
214
|
|
|
215
215
|
def _notification_handler(self, _sender: int, data: bytearray) -> None:
|
|
216
216
|
if self._notifications_enabled and self._check_command_result(data, 0, {0xF}):
|
|
217
|
+
if self._expected_disconnect:
|
|
218
|
+
_LOGGER.debug(
|
|
219
|
+
"%s: Ignoring lock notification during expected disconnect",
|
|
220
|
+
self.name,
|
|
221
|
+
)
|
|
222
|
+
return
|
|
217
223
|
self._update_lock_status(data)
|
|
218
224
|
else:
|
|
219
225
|
super()._notification_handler(_sender, data)
|
|
@@ -61,6 +61,7 @@ class SwitchbotRelaySwitch(SwitchbotSequenceDevice, SwitchbotEncryptedDevice):
|
|
|
61
61
|
|
|
62
62
|
_turn_on_command = f"{COMMAND_CONTROL}010100"
|
|
63
63
|
_turn_off_command = f"{COMMAND_CONTROL}010000"
|
|
64
|
+
_press_command = f"{COMMAND_CONTROL}110329" # for garage door opener toggle
|
|
64
65
|
|
|
65
66
|
def __init__(
|
|
66
67
|
self,
|
|
@@ -365,3 +365,22 @@ async def test_empty_data_encryption_decryption() -> None:
|
|
|
365
365
|
# Test empty decryption
|
|
366
366
|
decrypted = device._decrypt(bytearray())
|
|
367
367
|
assert decrypted == b""
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
@pytest.mark.asyncio
|
|
371
|
+
async def test_decrypt_with_none_iv_during_disconnect() -> None:
|
|
372
|
+
"""Test that decryption returns empty bytes when IV is None during expected disconnect."""
|
|
373
|
+
device = create_encrypted_device()
|
|
374
|
+
|
|
375
|
+
# Simulate disconnection in progress
|
|
376
|
+
device._expected_disconnect = True
|
|
377
|
+
device._iv = None
|
|
378
|
+
|
|
379
|
+
# Should return empty bytes instead of raising
|
|
380
|
+
result = device._decrypt(bytearray(b"encrypted_data"))
|
|
381
|
+
assert result == b""
|
|
382
|
+
|
|
383
|
+
# Verify it still raises when not disconnecting
|
|
384
|
+
device._expected_disconnect = False
|
|
385
|
+
with pytest.raises(RuntimeError, match="Cannot decrypt: IV is None"):
|
|
386
|
+
device._decrypt(bytearray(b"encrypted_data"))
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import logging
|
|
1
2
|
from unittest.mock import AsyncMock, Mock, patch
|
|
2
3
|
|
|
3
4
|
import pytest
|
|
@@ -478,6 +479,34 @@ def test_notification_handler_not_enabled(model: str):
|
|
|
478
479
|
mock_super.assert_called_once()
|
|
479
480
|
|
|
480
481
|
|
|
482
|
+
@pytest.mark.parametrize(
|
|
483
|
+
"model",
|
|
484
|
+
[
|
|
485
|
+
SwitchbotModel.LOCK,
|
|
486
|
+
SwitchbotModel.LOCK_LITE,
|
|
487
|
+
SwitchbotModel.LOCK_PRO,
|
|
488
|
+
SwitchbotModel.LOCK_ULTRA,
|
|
489
|
+
],
|
|
490
|
+
)
|
|
491
|
+
def test_notification_handler_during_disconnect(
|
|
492
|
+
model: str, caplog: pytest.LogCaptureFixture
|
|
493
|
+
) -> None:
|
|
494
|
+
"""Test _notification_handler during expected disconnect."""
|
|
495
|
+
device = create_device_for_command_testing(model)
|
|
496
|
+
device._notifications_enabled = True
|
|
497
|
+
device._expected_disconnect = True
|
|
498
|
+
data = bytearray(b"\x0f\x00\x00\x00\x80\x00\x00\x00\x00\x00")
|
|
499
|
+
with (
|
|
500
|
+
patch.object(device, "_update_lock_status") as mock_update,
|
|
501
|
+
caplog.at_level(logging.DEBUG),
|
|
502
|
+
):
|
|
503
|
+
device._notification_handler(0, data)
|
|
504
|
+
# Should not update lock status during disconnect
|
|
505
|
+
mock_update.assert_not_called()
|
|
506
|
+
# Should log debug message
|
|
507
|
+
assert "Ignoring lock notification during expected disconnect" in caplog.text
|
|
508
|
+
|
|
509
|
+
|
|
481
510
|
@pytest.mark.parametrize(
|
|
482
511
|
"model",
|
|
483
512
|
[
|
|
@@ -451,3 +451,14 @@ def test_merge_data(old_data, new_data, expected_result):
|
|
|
451
451
|
"""Test merging of data dictionaries."""
|
|
452
452
|
result = merge_data(old_data, new_data)
|
|
453
453
|
assert result == expected_result
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
@pytest.mark.asyncio
|
|
457
|
+
async def test_press():
|
|
458
|
+
"""Test the press command for garage door opener."""
|
|
459
|
+
device = create_device_for_command_testing(
|
|
460
|
+
b">\x00\x00\x00", SwitchbotModel.GARAGE_DOOR_OPENER
|
|
461
|
+
)
|
|
462
|
+
|
|
463
|
+
await device.press()
|
|
464
|
+
device._send_command.assert_awaited_once_with(device._press_command)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|