PySwitchbot 0.74.0__tar.gz → 0.76.0__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.74.0 → pyswitchbot-0.76.0}/PKG-INFO +1 -1
- {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/PySwitchbot.egg-info/PKG-INFO +1 -1
- {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/PySwitchbot.egg-info/SOURCES.txt +6 -0
- {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/setup.py +1 -1
- {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/__init__.py +4 -0
- {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/adv_parser.py +38 -0
- pyswitchbot-0.76.0/switchbot/adv_parsers/art_frame.py +35 -0
- pyswitchbot-0.76.0/switchbot/adv_parsers/keypad_vision.py +79 -0
- {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/adv_parsers/presence_sensor.py +3 -1
- {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/const/__init__.py +3 -0
- pyswitchbot-0.76.0/switchbot/devices/art_frame.py +140 -0
- {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/devices/base_cover.py +0 -13
- {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/devices/base_light.py +0 -13
- {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/devices/device.py +32 -5
- pyswitchbot-0.76.0/switchbot/devices/keypad_vision.py +167 -0
- {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/tests/test_adv_parser.py +136 -0
- pyswitchbot-0.76.0/tests/test_art_frame.py +221 -0
- {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/tests/test_device.py +39 -2
- pyswitchbot-0.76.0/tests/test_keypad_vision.py +259 -0
- {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/LICENSE +0 -0
- {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/MANIFEST.in +0 -0
- {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/PySwitchbot.egg-info/dependency_links.txt +0 -0
- {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/PySwitchbot.egg-info/requires.txt +0 -0
- {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/PySwitchbot.egg-info/top_level.txt +0 -0
- {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/README.md +0 -0
- {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/pyproject.toml +0 -0
- {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/setup.cfg +0 -0
- {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/adv_parsers/__init__.py +0 -0
- {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/adv_parsers/air_purifier.py +0 -0
- {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/adv_parsers/blind_tilt.py +0 -0
- {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/adv_parsers/bot.py +0 -0
- {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/adv_parsers/bulb.py +0 -0
- {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/adv_parsers/ceiling_light.py +0 -0
- {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/adv_parsers/climate_panel.py +0 -0
- {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/adv_parsers/contact.py +0 -0
- {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/adv_parsers/curtain.py +0 -0
- {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/adv_parsers/fan.py +0 -0
- {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/adv_parsers/hub2.py +0 -0
- {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/adv_parsers/hub3.py +0 -0
- {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/adv_parsers/hubmini_matter.py +0 -0
- {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/adv_parsers/humidifier.py +0 -0
- {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/adv_parsers/keypad.py +0 -0
- {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/adv_parsers/leak.py +0 -0
- {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/adv_parsers/light_strip.py +0 -0
- {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/adv_parsers/lock.py +0 -0
- {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/adv_parsers/meter.py +0 -0
- {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/adv_parsers/motion.py +0 -0
- {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/adv_parsers/plug.py +0 -0
- {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/adv_parsers/relay_switch.py +0 -0
- {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/adv_parsers/remote.py +0 -0
- {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/adv_parsers/roller_shade.py +0 -0
- {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/adv_parsers/smart_thermostat_radiator.py +0 -0
- {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/adv_parsers/vacuum.py +0 -0
- {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/api_config.py +0 -0
- {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/const/air_purifier.py +0 -0
- {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/const/climate.py +0 -0
- {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/const/evaporative_humidifier.py +0 -0
- {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/const/fan.py +0 -0
- {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/const/hub2.py +0 -0
- {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/const/hub3.py +0 -0
- {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/const/light.py +0 -0
- {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/const/lock.py +0 -0
- {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/const/presence_sensor.py +0 -0
- {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/devices/__init__.py +0 -0
- {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/devices/air_purifier.py +0 -0
- {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/devices/blind_tilt.py +0 -0
- {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/devices/bot.py +0 -0
- {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/devices/bulb.py +0 -0
- {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/devices/ceiling_light.py +0 -0
- {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/devices/contact.py +0 -0
- {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/devices/curtain.py +0 -0
- {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/devices/evaporative_humidifier.py +0 -0
- {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/devices/fan.py +0 -0
- {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/devices/humidifier.py +0 -0
- {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/devices/keypad.py +0 -0
- {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/devices/light_strip.py +0 -0
- {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/devices/lock.py +0 -0
- {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/devices/meter.py +0 -0
- {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/devices/motion.py +0 -0
- {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/devices/plug.py +0 -0
- {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/devices/relay_switch.py +0 -0
- {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/devices/roller_shade.py +0 -0
- {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/devices/smart_thermostat_radiator.py +0 -0
- {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/devices/vacuum.py +0 -0
- {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/discovery.py +0 -0
- {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/enum.py +0 -0
- {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/helpers.py +0 -0
- {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/models.py +0 -0
- {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/switchbot/utils.py +0 -0
- {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/tests/test_air_purifier.py +0 -0
- {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/tests/test_base_cover.py +0 -0
- {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/tests/test_blind_tilt.py +0 -0
- {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/tests/test_bulb.py +0 -0
- {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/tests/test_ceiling_light.py +0 -0
- {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/tests/test_colormode_imports.py +0 -0
- {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/tests/test_curtain.py +0 -0
- {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/tests/test_encrypted_device.py +0 -0
- {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/tests/test_evaporative_humidifier.py +0 -0
- {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/tests/test_fan.py +0 -0
- {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/tests/test_helpers.py +0 -0
- {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/tests/test_hub2.py +0 -0
- {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/tests/test_hub3.py +0 -0
- {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/tests/test_lock.py +0 -0
- {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/tests/test_relay_switch.py +0 -0
- {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/tests/test_roller_shade.py +0 -0
- {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/tests/test_smart_thermostat_radiator.py +0 -0
- {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/tests/test_strip_light.py +0 -0
- {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/tests/test_utils.py +0 -0
- {pyswitchbot-0.74.0 → pyswitchbot-0.76.0}/tests/test_vacuum.py +0 -0
|
@@ -18,6 +18,7 @@ switchbot/models.py
|
|
|
18
18
|
switchbot/utils.py
|
|
19
19
|
switchbot/adv_parsers/__init__.py
|
|
20
20
|
switchbot/adv_parsers/air_purifier.py
|
|
21
|
+
switchbot/adv_parsers/art_frame.py
|
|
21
22
|
switchbot/adv_parsers/blind_tilt.py
|
|
22
23
|
switchbot/adv_parsers/bot.py
|
|
23
24
|
switchbot/adv_parsers/bulb.py
|
|
@@ -31,6 +32,7 @@ switchbot/adv_parsers/hub3.py
|
|
|
31
32
|
switchbot/adv_parsers/hubmini_matter.py
|
|
32
33
|
switchbot/adv_parsers/humidifier.py
|
|
33
34
|
switchbot/adv_parsers/keypad.py
|
|
35
|
+
switchbot/adv_parsers/keypad_vision.py
|
|
34
36
|
switchbot/adv_parsers/leak.py
|
|
35
37
|
switchbot/adv_parsers/light_strip.py
|
|
36
38
|
switchbot/adv_parsers/lock.py
|
|
@@ -55,6 +57,7 @@ switchbot/const/lock.py
|
|
|
55
57
|
switchbot/const/presence_sensor.py
|
|
56
58
|
switchbot/devices/__init__.py
|
|
57
59
|
switchbot/devices/air_purifier.py
|
|
60
|
+
switchbot/devices/art_frame.py
|
|
58
61
|
switchbot/devices/base_cover.py
|
|
59
62
|
switchbot/devices/base_light.py
|
|
60
63
|
switchbot/devices/blind_tilt.py
|
|
@@ -68,6 +71,7 @@ switchbot/devices/evaporative_humidifier.py
|
|
|
68
71
|
switchbot/devices/fan.py
|
|
69
72
|
switchbot/devices/humidifier.py
|
|
70
73
|
switchbot/devices/keypad.py
|
|
74
|
+
switchbot/devices/keypad_vision.py
|
|
71
75
|
switchbot/devices/light_strip.py
|
|
72
76
|
switchbot/devices/lock.py
|
|
73
77
|
switchbot/devices/meter.py
|
|
@@ -79,6 +83,7 @@ switchbot/devices/smart_thermostat_radiator.py
|
|
|
79
83
|
switchbot/devices/vacuum.py
|
|
80
84
|
tests/test_adv_parser.py
|
|
81
85
|
tests/test_air_purifier.py
|
|
86
|
+
tests/test_art_frame.py
|
|
82
87
|
tests/test_base_cover.py
|
|
83
88
|
tests/test_blind_tilt.py
|
|
84
89
|
tests/test_bulb.py
|
|
@@ -92,6 +97,7 @@ tests/test_fan.py
|
|
|
92
97
|
tests/test_helpers.py
|
|
93
98
|
tests/test_hub2.py
|
|
94
99
|
tests/test_hub3.py
|
|
100
|
+
tests/test_keypad_vision.py
|
|
95
101
|
tests/test_lock.py
|
|
96
102
|
tests/test_relay_switch.py
|
|
97
103
|
tests/test_roller_shade.py
|
|
@@ -29,6 +29,7 @@ from .const import (
|
|
|
29
29
|
SwitchbotModel,
|
|
30
30
|
)
|
|
31
31
|
from .devices.air_purifier import SwitchbotAirPurifier
|
|
32
|
+
from .devices.art_frame import SwitchbotArtFrame
|
|
32
33
|
from .devices.base_light import SwitchbotBaseLight
|
|
33
34
|
from .devices.blind_tilt import SwitchbotBlindTilt
|
|
34
35
|
from .devices.bot import Switchbot
|
|
@@ -44,6 +45,7 @@ from .devices.device import (
|
|
|
44
45
|
from .devices.evaporative_humidifier import SwitchbotEvaporativeHumidifier
|
|
45
46
|
from .devices.fan import SwitchbotFan
|
|
46
47
|
from .devices.humidifier import SwitchbotHumidifier
|
|
48
|
+
from .devices.keypad_vision import SwitchbotKeypadVision
|
|
47
49
|
from .devices.light_strip import (
|
|
48
50
|
SwitchbotLightStrip,
|
|
49
51
|
SwitchbotRgbicLight,
|
|
@@ -83,6 +85,7 @@ __all__ = [
|
|
|
83
85
|
"SwitchbotAccountConnectionError",
|
|
84
86
|
"SwitchbotAirPurifier",
|
|
85
87
|
"SwitchbotApiError",
|
|
88
|
+
"SwitchbotArtFrame",
|
|
86
89
|
"SwitchbotAuthenticationError",
|
|
87
90
|
"SwitchbotBaseLight",
|
|
88
91
|
"SwitchbotBlindTilt",
|
|
@@ -95,6 +98,7 @@ __all__ = [
|
|
|
95
98
|
"SwitchbotFan",
|
|
96
99
|
"SwitchbotGarageDoorOpener",
|
|
97
100
|
"SwitchbotHumidifier",
|
|
101
|
+
"SwitchbotKeypadVision",
|
|
98
102
|
"SwitchbotLightStrip",
|
|
99
103
|
"SwitchbotLock",
|
|
100
104
|
"SwitchbotModel",
|
|
@@ -12,6 +12,7 @@ from bleak.backends.device import BLEDevice
|
|
|
12
12
|
from bleak.backends.scanner import AdvertisementData
|
|
13
13
|
|
|
14
14
|
from .adv_parsers.air_purifier import process_air_purifier
|
|
15
|
+
from .adv_parsers.art_frame import process_art_frame
|
|
15
16
|
from .adv_parsers.blind_tilt import process_woblindtilt
|
|
16
17
|
from .adv_parsers.bot import process_wohand
|
|
17
18
|
from .adv_parsers.bulb import process_color_bulb
|
|
@@ -25,6 +26,7 @@ from .adv_parsers.hub3 import process_hub3
|
|
|
25
26
|
from .adv_parsers.hubmini_matter import process_hubmini_matter
|
|
26
27
|
from .adv_parsers.humidifier import process_evaporative_humidifier, process_wohumidifier
|
|
27
28
|
from .adv_parsers.keypad import process_wokeypad
|
|
29
|
+
from .adv_parsers.keypad_vision import process_keypad_vision, process_keypad_vision_pro
|
|
28
30
|
from .adv_parsers.leak import process_leak
|
|
29
31
|
from .adv_parsers.light_strip import process_light, process_rgbic_light, process_wostrip
|
|
30
32
|
from .adv_parsers.lock import (
|
|
@@ -717,6 +719,42 @@ SUPPORTED_TYPES: dict[str | bytes, SwitchbotSupportedType] = {
|
|
|
717
719
|
"func": process_presence_sensor,
|
|
718
720
|
"manufacturer_id": 2409,
|
|
719
721
|
},
|
|
722
|
+
b"\x00\x11>\x10": {
|
|
723
|
+
"modelName": SwitchbotModel.ART_FRAME,
|
|
724
|
+
"modelFriendlyName": "Art Frame",
|
|
725
|
+
"func": process_art_frame,
|
|
726
|
+
"manufacturer_id": 2409,
|
|
727
|
+
},
|
|
728
|
+
b"\x01\x11>\x10": {
|
|
729
|
+
"modelName": SwitchbotModel.ART_FRAME,
|
|
730
|
+
"modelFriendlyName": "Art Frame",
|
|
731
|
+
"func": process_art_frame,
|
|
732
|
+
"manufacturer_id": 2409,
|
|
733
|
+
},
|
|
734
|
+
b"\x00\x11\x03x": {
|
|
735
|
+
"modelName": SwitchbotModel.KEYPAD_VISION,
|
|
736
|
+
"modelFriendlyName": "Keypad Vision",
|
|
737
|
+
"func": process_keypad_vision,
|
|
738
|
+
"manufacturer_id": 2409,
|
|
739
|
+
},
|
|
740
|
+
b"\x01\x11\x03x": {
|
|
741
|
+
"modelName": SwitchbotModel.KEYPAD_VISION,
|
|
742
|
+
"modelFriendlyName": "Keypad Vision",
|
|
743
|
+
"func": process_keypad_vision,
|
|
744
|
+
"manufacturer_id": 2409,
|
|
745
|
+
},
|
|
746
|
+
b"\x00\x11Q\x98": {
|
|
747
|
+
"modelName": SwitchbotModel.KEYPAD_VISION_PRO,
|
|
748
|
+
"modelFriendlyName": "Keypad Vision Pro",
|
|
749
|
+
"func": process_keypad_vision_pro,
|
|
750
|
+
"manufacturer_id": 2409,
|
|
751
|
+
},
|
|
752
|
+
b"\x01\x11Q\x98": {
|
|
753
|
+
"modelName": SwitchbotModel.KEYPAD_VISION_PRO,
|
|
754
|
+
"modelFriendlyName": "Keypad Vision Pro",
|
|
755
|
+
"func": process_keypad_vision_pro,
|
|
756
|
+
"manufacturer_id": 2409,
|
|
757
|
+
},
|
|
720
758
|
}
|
|
721
759
|
|
|
722
760
|
_SWITCHBOT_MODEL_TO_CHAR: defaultdict[SwitchbotModel, list[str | bytes]] = defaultdict(
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Art Frame advertisement data parser."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
_LOGGER = logging.getLogger(__name__)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def process_art_frame(
|
|
9
|
+
data: bytes | None, mfr_data: bytes | None
|
|
10
|
+
) -> dict[str, bool | int | str]:
|
|
11
|
+
"""Process Art Frame data."""
|
|
12
|
+
if mfr_data is None:
|
|
13
|
+
return {}
|
|
14
|
+
|
|
15
|
+
_seq_num = mfr_data[6]
|
|
16
|
+
battery_charging = bool(mfr_data[7] & 0x80)
|
|
17
|
+
battery = mfr_data[7] & 0x7F
|
|
18
|
+
image_index = mfr_data[8]
|
|
19
|
+
display_size = (mfr_data[9] >> 4) & 0x0F
|
|
20
|
+
display_mode = (mfr_data[9] >> 3) & 0x01
|
|
21
|
+
last_network_status = (mfr_data[9] >> 2) & 0x01
|
|
22
|
+
|
|
23
|
+
result = {
|
|
24
|
+
"sequence_number": _seq_num,
|
|
25
|
+
"battery_charging": battery_charging,
|
|
26
|
+
"battery": battery,
|
|
27
|
+
"image_index": image_index,
|
|
28
|
+
"display_size": display_size,
|
|
29
|
+
"display_mode": display_mode,
|
|
30
|
+
"last_network_status": last_network_status,
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
_LOGGER.debug("Art Frame mfr data: %s, result: %s", mfr_data.hex(), result)
|
|
34
|
+
|
|
35
|
+
return result
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Keypad Vision (Pro) device data parsers."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
_LOGGER = logging.getLogger(__name__)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def process_common_mfr_data(mfr_data: bytes | None) -> dict[str, bool | int]:
|
|
9
|
+
"""Process common Keypad Vision (Pro) manufacturer data."""
|
|
10
|
+
if mfr_data is None:
|
|
11
|
+
return {}
|
|
12
|
+
|
|
13
|
+
sequence_number = mfr_data[6]
|
|
14
|
+
battery_charging = bool(mfr_data[7] & 0b10000000)
|
|
15
|
+
battery = mfr_data[7] & 0b01111111
|
|
16
|
+
lockout_alarm = bool(mfr_data[8] & 0b00000001)
|
|
17
|
+
tamper_alarm = bool(mfr_data[8] & 0b00000010)
|
|
18
|
+
duress_alarm = bool(mfr_data[8] & 0b00000100)
|
|
19
|
+
low_temperature = bool(mfr_data[8] & 0b10000000)
|
|
20
|
+
high_temperature = bool(mfr_data[8] & 0b01000000)
|
|
21
|
+
doorbell = bool(mfr_data[12] & 0b00001000)
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
"sequence_number": sequence_number,
|
|
25
|
+
"battery_charging": battery_charging,
|
|
26
|
+
"battery": battery,
|
|
27
|
+
"lockout_alarm": lockout_alarm,
|
|
28
|
+
"tamper_alarm": tamper_alarm,
|
|
29
|
+
"duress_alarm": duress_alarm,
|
|
30
|
+
"low_temperature": low_temperature,
|
|
31
|
+
"high_temperature": high_temperature,
|
|
32
|
+
"doorbell": doorbell,
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def process_keypad_vision(
|
|
37
|
+
data: bytes | None, mfr_data: bytes | None
|
|
38
|
+
) -> dict[str, bool | int | str]:
|
|
39
|
+
"""Process Keypad Vision data."""
|
|
40
|
+
result = process_common_mfr_data(mfr_data)
|
|
41
|
+
|
|
42
|
+
if not result:
|
|
43
|
+
return {}
|
|
44
|
+
|
|
45
|
+
pir_triggered_level = mfr_data[13] & 0x03
|
|
46
|
+
|
|
47
|
+
result.update(
|
|
48
|
+
{
|
|
49
|
+
"pir_triggered_level": pir_triggered_level,
|
|
50
|
+
}
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
_LOGGER.debug("Keypad Vision mfr data: %s, result: %s", mfr_data.hex(), result)
|
|
54
|
+
|
|
55
|
+
return result
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def process_keypad_vision_pro(
|
|
59
|
+
data: bytes | None, mfr_data: bytes | None
|
|
60
|
+
) -> dict[str, bool | int | str]:
|
|
61
|
+
"""Process Keypad Vision Pro data."""
|
|
62
|
+
result = process_common_mfr_data(mfr_data)
|
|
63
|
+
|
|
64
|
+
if not result:
|
|
65
|
+
return {}
|
|
66
|
+
|
|
67
|
+
radar_triggered_level = mfr_data[13] & 0x03
|
|
68
|
+
radar_triggered_distance = (mfr_data[13] >> 2) & 0x03
|
|
69
|
+
|
|
70
|
+
result.update(
|
|
71
|
+
{
|
|
72
|
+
"radar_triggered_level": radar_triggered_level,
|
|
73
|
+
"radar_triggered_distance": radar_triggered_distance,
|
|
74
|
+
}
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
_LOGGER.debug("Keypad Vision Pro mfr data: %s, result: %s", mfr_data.hex(), result)
|
|
78
|
+
|
|
79
|
+
return result
|
|
@@ -21,15 +21,17 @@ def process_presence_sensor(
|
|
|
21
21
|
motion_detected = bool(mfr_data[7] & 0x40)
|
|
22
22
|
battery_bits = (mfr_data[7] >> 2) & 0x03
|
|
23
23
|
battery_range = BATTERY_LEVEL_MAP.get(battery_bits, "Unknown")
|
|
24
|
+
duration = (mfr_data[8] << 8) + mfr_data[9]
|
|
24
25
|
trigger_flag = mfr_data[10]
|
|
25
26
|
led_state = bool(mfr_data[11] & 0x80)
|
|
26
|
-
light_level = mfr_data[11] &
|
|
27
|
+
light_level = mfr_data[11] & 0x1F
|
|
27
28
|
|
|
28
29
|
result = {
|
|
29
30
|
"sequence_number": seq_number,
|
|
30
31
|
"adaptive_state": adaptive_state,
|
|
31
32
|
"motion_detected": motion_detected,
|
|
32
33
|
"battery_range": battery_range,
|
|
34
|
+
"duration": duration,
|
|
33
35
|
"trigger_flag": trigger_flag,
|
|
34
36
|
"led_state": led_state,
|
|
35
37
|
"lightLevel": light_level,
|
|
@@ -102,6 +102,9 @@ class SwitchbotModel(StrEnum):
|
|
|
102
102
|
SMART_THERMOSTAT_RADIATOR = "Smart Thermostat Radiator"
|
|
103
103
|
S20_VACUUM = "S20 Vacuum"
|
|
104
104
|
PRESENCE_SENSOR = "Presence Sensor"
|
|
105
|
+
ART_FRAME = "Art Frame"
|
|
106
|
+
KEYPAD_VISION = "Keypad Vision"
|
|
107
|
+
KEYPAD_VISION_PRO = "Keypad Vision Pro"
|
|
105
108
|
|
|
106
109
|
|
|
107
110
|
__all__ = [
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"""Device handler for the Art Frame."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from bleak.backends.device import BLEDevice
|
|
7
|
+
|
|
8
|
+
from ..const import SwitchbotModel
|
|
9
|
+
from .device import (
|
|
10
|
+
SwitchbotEncryptedDevice,
|
|
11
|
+
SwitchbotSequenceDevice,
|
|
12
|
+
update_after_operation,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
_LOGGER = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
COMMAND_SET_IMAGE = "570F7A02{}"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class SwitchbotArtFrame(SwitchbotSequenceDevice, SwitchbotEncryptedDevice):
|
|
21
|
+
"""Representation of a Switchbot Art Frame."""
|
|
22
|
+
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
device: BLEDevice,
|
|
26
|
+
key_id: str,
|
|
27
|
+
encryption_key: str,
|
|
28
|
+
interface: int = 0,
|
|
29
|
+
model: SwitchbotModel = SwitchbotModel.ART_FRAME,
|
|
30
|
+
**kwargs: Any,
|
|
31
|
+
) -> None:
|
|
32
|
+
super().__init__(device, key_id, encryption_key, model, interface, **kwargs)
|
|
33
|
+
self.response_flag = True
|
|
34
|
+
|
|
35
|
+
@classmethod
|
|
36
|
+
async def verify_encryption_key(
|
|
37
|
+
cls,
|
|
38
|
+
device: BLEDevice,
|
|
39
|
+
key_id: str,
|
|
40
|
+
encryption_key: str,
|
|
41
|
+
model: SwitchbotModel = SwitchbotModel.ART_FRAME,
|
|
42
|
+
**kwargs: Any,
|
|
43
|
+
) -> bool:
|
|
44
|
+
return await super().verify_encryption_key(
|
|
45
|
+
device, key_id, encryption_key, model, **kwargs
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
async def get_basic_info(self) -> dict[str, Any] | None:
|
|
49
|
+
"""Get device basic settings."""
|
|
50
|
+
if not (_data := await self._get_basic_info()):
|
|
51
|
+
return None
|
|
52
|
+
_LOGGER.debug("basic info data: %s", _data.hex())
|
|
53
|
+
|
|
54
|
+
battery_charging = bool(_data[1] & 0x80)
|
|
55
|
+
battery = _data[1] & 0x7F
|
|
56
|
+
firmware = _data[2] / 10.0
|
|
57
|
+
hardware = _data[3]
|
|
58
|
+
display_size = (_data[4] >> 4) & 0x0F
|
|
59
|
+
display_mode = (_data[4] >> 3) & 0x01
|
|
60
|
+
last_network_status = (_data[4] >> 2) & 0x01
|
|
61
|
+
current_image_index = _data[5]
|
|
62
|
+
total_num_of_images = _data[6]
|
|
63
|
+
all_images_index = [_data[x] for x in range(7, 7 + total_num_of_images)]
|
|
64
|
+
|
|
65
|
+
basic_info = {
|
|
66
|
+
"battery_charging": battery_charging,
|
|
67
|
+
"battery": battery,
|
|
68
|
+
"firmware": firmware,
|
|
69
|
+
"hardware": hardware,
|
|
70
|
+
"display_size": display_size,
|
|
71
|
+
"display_mode": display_mode,
|
|
72
|
+
"last_network_status": last_network_status,
|
|
73
|
+
"current_image_index": current_image_index,
|
|
74
|
+
"total_num_of_images": total_num_of_images,
|
|
75
|
+
"all_images_index": all_images_index,
|
|
76
|
+
}
|
|
77
|
+
_LOGGER.debug("Art Frame %s basic info: %s", self._device.address, basic_info)
|
|
78
|
+
return basic_info
|
|
79
|
+
|
|
80
|
+
def _select_image_index(self, offset: int) -> int:
|
|
81
|
+
"""Select the image index based on the current index and offset."""
|
|
82
|
+
current_index = self.get_current_image_index()
|
|
83
|
+
all_images_index = self.get_all_images_index()
|
|
84
|
+
|
|
85
|
+
if not all_images_index or len(all_images_index) <= 1:
|
|
86
|
+
raise RuntimeError("No images available to select from.")
|
|
87
|
+
|
|
88
|
+
new_position = (all_images_index.index(current_index) + offset) % len(
|
|
89
|
+
all_images_index
|
|
90
|
+
)
|
|
91
|
+
return all_images_index[new_position]
|
|
92
|
+
|
|
93
|
+
async def _get_current_image_index(self) -> None:
|
|
94
|
+
"""Validate the current image index."""
|
|
95
|
+
if not await self.get_basic_info():
|
|
96
|
+
raise RuntimeError("Failed to retrieve basic info for current image index.")
|
|
97
|
+
|
|
98
|
+
@update_after_operation
|
|
99
|
+
async def next_image(self) -> bool:
|
|
100
|
+
"""Display the next image."""
|
|
101
|
+
await self._get_current_image_index()
|
|
102
|
+
idx = self._select_image_index(1)
|
|
103
|
+
result = await self._send_command(COMMAND_SET_IMAGE.format(f"{idx:02X}"))
|
|
104
|
+
return self._check_command_result(result, 0, {1})
|
|
105
|
+
|
|
106
|
+
@update_after_operation
|
|
107
|
+
async def prev_image(self) -> bool:
|
|
108
|
+
"""Display the previous image."""
|
|
109
|
+
await self._get_current_image_index()
|
|
110
|
+
idx = self._select_image_index(-1)
|
|
111
|
+
result = await self._send_command(COMMAND_SET_IMAGE.format(f"{idx:02X}"))
|
|
112
|
+
return self._check_command_result(result, 0, {1})
|
|
113
|
+
|
|
114
|
+
@update_after_operation
|
|
115
|
+
async def set_image(self, index: int) -> bool:
|
|
116
|
+
"""Set the image by index."""
|
|
117
|
+
await self._get_current_image_index()
|
|
118
|
+
total_images = self.get_total_images()
|
|
119
|
+
|
|
120
|
+
if index < 0 or index >= total_images:
|
|
121
|
+
raise ValueError(
|
|
122
|
+
f"Image index {index} is out of range. Total images: {total_images}."
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
all_images_index = self.get_all_images_index()
|
|
126
|
+
img_index = all_images_index[index]
|
|
127
|
+
result = await self._send_command(COMMAND_SET_IMAGE.format(f"{img_index:02X}"))
|
|
128
|
+
return self._check_command_result(result, 0, {1})
|
|
129
|
+
|
|
130
|
+
def get_all_images_index(self) -> list[int] | None:
|
|
131
|
+
"""Return cached list of all image indexes."""
|
|
132
|
+
return self._get_adv_value("all_images_index")
|
|
133
|
+
|
|
134
|
+
def get_current_image_index(self) -> int | None:
|
|
135
|
+
"""Return cached current image index."""
|
|
136
|
+
return self._get_adv_value("current_image_index")
|
|
137
|
+
|
|
138
|
+
def get_total_images(self) -> int | None:
|
|
139
|
+
"""Return cached total number of images."""
|
|
140
|
+
return self._get_adv_value("total_num_of_images")
|
|
@@ -43,19 +43,6 @@ class SwitchbotBaseCover(SwitchbotDevice):
|
|
|
43
43
|
self._is_opening: bool = False
|
|
44
44
|
self._is_closing: bool = False
|
|
45
45
|
|
|
46
|
-
async def _send_multiple_commands(self, keys: list[str]) -> bool:
|
|
47
|
-
"""
|
|
48
|
-
Send multiple commands to device.
|
|
49
|
-
|
|
50
|
-
Since we current have no way to tell which command the device
|
|
51
|
-
needs we send both.
|
|
52
|
-
"""
|
|
53
|
-
final_result = False
|
|
54
|
-
for key in keys:
|
|
55
|
-
result = await self._send_command(key)
|
|
56
|
-
final_result |= self._check_command_result(result, 0, {1})
|
|
57
|
-
return final_result
|
|
58
|
-
|
|
59
46
|
@update_after_operation
|
|
60
47
|
async def stop(self) -> bool:
|
|
61
48
|
"""Send stop command to device."""
|
|
@@ -119,19 +119,6 @@ class SwitchbotBaseLight(SwitchbotDevice):
|
|
|
119
119
|
self._override_state({"effect": effect})
|
|
120
120
|
return result
|
|
121
121
|
|
|
122
|
-
async def _send_multiple_commands(self, keys: list[str]) -> bool:
|
|
123
|
-
"""
|
|
124
|
-
Send multiple commands to device.
|
|
125
|
-
|
|
126
|
-
Since we current have no way to tell which command the device
|
|
127
|
-
needs we send both.
|
|
128
|
-
"""
|
|
129
|
-
final_result = False
|
|
130
|
-
for key in keys:
|
|
131
|
-
result = await self._send_command(key)
|
|
132
|
-
final_result |= self._check_command_result(result, 0, {1})
|
|
133
|
-
return final_result
|
|
134
|
-
|
|
135
122
|
async def _get_multi_commands_results(
|
|
136
123
|
self, commands: list[str]
|
|
137
124
|
) -> tuple[bytes, bytes] | None:
|
|
@@ -104,6 +104,7 @@ API_MODEL_TO_ENUM: dict[str, SwitchbotModel] = {
|
|
|
104
104
|
"W1104000": SwitchbotModel.PLUG_MINI_EU,
|
|
105
105
|
"W1128000": SwitchbotModel.SMART_THERMOSTAT_RADIATOR,
|
|
106
106
|
"W1111000": SwitchbotModel.CLIMATE_PANEL,
|
|
107
|
+
"W1130000": SwitchbotModel.ART_FRAME,
|
|
107
108
|
}
|
|
108
109
|
|
|
109
110
|
REQ_HEADER = "570f"
|
|
@@ -224,7 +225,6 @@ class SwitchbotBaseDevice:
|
|
|
224
225
|
self._write_char: BleakGATTCharacteristic | None = None
|
|
225
226
|
self._disconnect_timer: asyncio.TimerHandle | None = None
|
|
226
227
|
self._expected_disconnect = False
|
|
227
|
-
self.loop = asyncio.get_event_loop()
|
|
228
228
|
self._callbacks: list[Callable[[], None]] = []
|
|
229
229
|
self._notify_future: asyncio.Future[bytearray] | None = None
|
|
230
230
|
self._last_full_update: float = -PASSIVE_POLL_INTERVAL
|
|
@@ -546,7 +546,7 @@ class SwitchbotBaseDevice:
|
|
|
546
546
|
"""Reset disconnect timer."""
|
|
547
547
|
self._cancel_disconnect_timer()
|
|
548
548
|
self._expected_disconnect = False
|
|
549
|
-
self._disconnect_timer =
|
|
549
|
+
self._disconnect_timer = asyncio.get_running_loop().call_later(
|
|
550
550
|
DISCONNECT_DELAY, self._disconnect_from_timer
|
|
551
551
|
)
|
|
552
552
|
|
|
@@ -679,15 +679,16 @@ class SwitchbotBaseDevice:
|
|
|
679
679
|
assert self._client is not None
|
|
680
680
|
assert self._read_char is not None
|
|
681
681
|
assert self._write_char is not None
|
|
682
|
-
|
|
682
|
+
loop = asyncio.get_running_loop()
|
|
683
|
+
self._notify_future = loop.create_future()
|
|
683
684
|
client = self._client
|
|
684
685
|
|
|
685
686
|
_LOGGER.debug("%s: Sending command: %s", self.name, key)
|
|
686
687
|
await client.write_gatt_char(self._write_char, command, False)
|
|
687
688
|
|
|
688
689
|
timeout = 5
|
|
689
|
-
timeout_handle =
|
|
690
|
-
|
|
690
|
+
timeout_handle = loop.call_at(
|
|
691
|
+
loop.time() + timeout, _handle_timeout, self._notify_future
|
|
691
692
|
)
|
|
692
693
|
timeout_expired = False
|
|
693
694
|
try:
|
|
@@ -930,6 +931,32 @@ class SwitchbotDevice(SwitchbotBaseDevice):
|
|
|
930
931
|
super().update_from_advertisement(advertisement)
|
|
931
932
|
self._set_advertisement_data(advertisement)
|
|
932
933
|
|
|
934
|
+
async def _send_multiple_commands(self, keys: list[str]) -> bool:
|
|
935
|
+
"""
|
|
936
|
+
Send multiple commands to device.
|
|
937
|
+
|
|
938
|
+
Returns True if any command succeeds. Used when we don't know
|
|
939
|
+
which command the device needs, so we send multiple and consider
|
|
940
|
+
it successful if any one works.
|
|
941
|
+
"""
|
|
942
|
+
final_result = False
|
|
943
|
+
for key in keys:
|
|
944
|
+
result = await self._send_command(key)
|
|
945
|
+
final_result |= self._check_command_result(result, 0, {1})
|
|
946
|
+
return final_result
|
|
947
|
+
|
|
948
|
+
async def _send_command_sequence(self, keys: list[str]) -> bool:
|
|
949
|
+
"""
|
|
950
|
+
Send a sequence of commands to device where all must succeed.
|
|
951
|
+
|
|
952
|
+
Returns True only if all commands succeed.
|
|
953
|
+
"""
|
|
954
|
+
for key in keys:
|
|
955
|
+
result = await self._send_command(key)
|
|
956
|
+
if not self._check_command_result(result, 0, {1}):
|
|
957
|
+
return False
|
|
958
|
+
return True
|
|
959
|
+
|
|
933
960
|
|
|
934
961
|
class SwitchbotEncryptedDevice(SwitchbotDevice):
|
|
935
962
|
"""A Switchbot device that uses encryption."""
|