1
0
mirror of https://github.com/home-assistant/core.git synced 2026-04-17 07:34:07 +01:00

Bump PySwitchbot to 2.0.0 (#165995)

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
This commit is contained in:
Retha Runolfsson
2026-03-24 21:29:02 +08:00
committed by GitHub
parent 583524e841
commit 422d69f2b3
15 changed files with 189 additions and 34 deletions

View File

@@ -30,6 +30,8 @@ from .const import (
CONNECTABLE_SUPPORTED_MODEL_TYPES,
DEFAULT_CURTAIN_SPEED,
DEFAULT_RETRY_COUNT,
DEPRECATED_SENSOR_TYPE_AIR_PURIFIER,
DEPRECATED_SENSOR_TYPE_AIR_PURIFIER_TABLE,
DOMAIN,
ENCRYPTED_MODELS,
HASS_SENSOR_TYPE_TO_SWITCHBOT_MODEL,
@@ -107,8 +109,10 @@ PLATFORMS_BY_TYPE = {
Platform.LOCK,
Platform.SENSOR,
],
SupportedModels.AIR_PURIFIER.value: [Platform.FAN, Platform.SENSOR],
SupportedModels.AIR_PURIFIER_TABLE.value: [Platform.FAN, Platform.SENSOR],
SupportedModels.AIR_PURIFIER_JP.value: [Platform.FAN, Platform.SENSOR],
SupportedModels.AIR_PURIFIER_US.value: [Platform.FAN, Platform.SENSOR],
SupportedModels.AIR_PURIFIER_TABLE_JP.value: [Platform.FAN, Platform.SENSOR],
SupportedModels.AIR_PURIFIER_TABLE_US.value: [Platform.FAN, Platform.SENSOR],
SupportedModels.EVAPORATIVE_HUMIDIFIER: [Platform.HUMIDIFIER, Platform.SENSOR],
SupportedModels.FLOOR_LAMP.value: [Platform.LIGHT, Platform.SENSOR],
SupportedModels.STRIP_LIGHT_3.value: [Platform.LIGHT, Platform.SENSOR],
@@ -162,8 +166,10 @@ CLASS_BY_DEVICE = {
SupportedModels.K20_VACUUM.value: switchbot.SwitchbotVacuum,
SupportedModels.LOCK_LITE.value: switchbot.SwitchbotLock,
SupportedModels.LOCK_ULTRA.value: switchbot.SwitchbotLock,
SupportedModels.AIR_PURIFIER.value: switchbot.SwitchbotAirPurifier,
SupportedModels.AIR_PURIFIER_TABLE.value: switchbot.SwitchbotAirPurifier,
SupportedModels.AIR_PURIFIER_JP.value: switchbot.SwitchbotAirPurifier,
SupportedModels.AIR_PURIFIER_US.value: switchbot.SwitchbotAirPurifier,
SupportedModels.AIR_PURIFIER_TABLE_JP.value: switchbot.SwitchbotAirPurifier,
SupportedModels.AIR_PURIFIER_TABLE_US.value: switchbot.SwitchbotAirPurifier,
SupportedModels.EVAPORATIVE_HUMIDIFIER: switchbot.SwitchbotEvaporativeHumidifier,
SupportedModels.FLOOR_LAMP.value: switchbot.SwitchbotStripLight3,
SupportedModels.STRIP_LIGHT_3.value: switchbot.SwitchbotStripLight3,
@@ -183,6 +189,40 @@ CLASS_BY_DEVICE = {
_LOGGER = logging.getLogger(__name__)
def _migrate_deprecated_air_purifier_type(
hass: HomeAssistant, entry: SwitchbotConfigEntry
) -> bool:
"""Migrate deprecated air purifier sensor types introduced before pySwitchbot 2.0.0.
The old library used a single AIR_PURIFIER/AIR_PURIFIER_TABLE type; the new
library distinguishes by region (JP/US). The correct type can be detected from
the BLE advertisement local name without connecting or using encryption keys.
Returns True if migration succeeded, False if device is not in range yet.
"""
address: str = entry.data[CONF_ADDRESS]
if service_info := bluetooth.async_last_service_info(
hass, address.upper(), connectable=True
):
parsed_adv = switchbot.parse_advertisement_data(
service_info.device, service_info.advertisement
)
if (
parsed_adv
and (adv_model := parsed_adv.data.get("modelName"))
in CONNECTABLE_SUPPORTED_MODEL_TYPES
):
hass.config_entries.async_update_entry(
entry,
data={
**entry.data,
CONF_SENSOR_TYPE: str(CONNECTABLE_SUPPORTED_MODEL_TYPES[adv_model]),
},
)
return True
return False
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the Switchbot Devices component."""
async_setup_services(hass)
@@ -203,6 +243,21 @@ async def async_setup_entry(hass: HomeAssistant, entry: SwitchbotConfigEntry) ->
data={**entry.data, CONF_ADDRESS: mac},
)
# Migrate deprecated air purifier sensor types introduced before pySwitchbot 2.0.0.
if entry.data.get(CONF_SENSOR_TYPE) in (
DEPRECATED_SENSOR_TYPE_AIR_PURIFIER,
DEPRECATED_SENSOR_TYPE_AIR_PURIFIER_TABLE,
) and not _migrate_deprecated_air_purifier_type(hass, entry):
# Device was not in range; retry when it starts advertising again.
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="device_not_found_error",
translation_placeholders={
"sensor_type": entry.data[CONF_SENSOR_TYPE],
"address": entry.data[CONF_ADDRESS],
},
)
sensor_type: str = entry.data[CONF_SENSOR_TYPE]
switchbot_model = HASS_SENSOR_TYPE_TO_SWITCHBOT_MODEL[sensor_type]
# connectable means we can make connections to the device

View File

@@ -47,8 +47,10 @@ class SupportedModels(StrEnum):
HUB3 = "hub3"
LOCK_LITE = "lock_lite"
LOCK_ULTRA = "lock_ultra"
AIR_PURIFIER = "air_purifier"
AIR_PURIFIER_TABLE = "air_purifier_table"
AIR_PURIFIER_JP = "air_purifier_jp"
AIR_PURIFIER_US = "air_purifier_us"
AIR_PURIFIER_TABLE_JP = "air_purifier_table_jp"
AIR_PURIFIER_TABLE_US = "air_purifier_table_us"
EVAPORATIVE_HUMIDIFIER = "evaporative_humidifier"
FLOOR_LAMP = "floor_lamp"
STRIP_LIGHT_3 = "strip_light_3"
@@ -90,8 +92,10 @@ CONNECTABLE_SUPPORTED_MODEL_TYPES = {
SwitchbotModel.K10_PRO_COMBO_VACUUM: SupportedModels.K10_PRO_COMBO_VACUUM,
SwitchbotModel.LOCK_LITE: SupportedModels.LOCK_LITE,
SwitchbotModel.LOCK_ULTRA: SupportedModels.LOCK_ULTRA,
SwitchbotModel.AIR_PURIFIER: SupportedModels.AIR_PURIFIER,
SwitchbotModel.AIR_PURIFIER_TABLE: SupportedModels.AIR_PURIFIER_TABLE,
SwitchbotModel.AIR_PURIFIER_JP: SupportedModels.AIR_PURIFIER_JP,
SwitchbotModel.AIR_PURIFIER_US: SupportedModels.AIR_PURIFIER_US,
SwitchbotModel.AIR_PURIFIER_TABLE_JP: SupportedModels.AIR_PURIFIER_TABLE_JP,
SwitchbotModel.AIR_PURIFIER_TABLE_US: SupportedModels.AIR_PURIFIER_TABLE_US,
SwitchbotModel.EVAPORATIVE_HUMIDIFIER: SupportedModels.EVAPORATIVE_HUMIDIFIER,
SwitchbotModel.FLOOR_LAMP: SupportedModels.FLOOR_LAMP,
SwitchbotModel.STRIP_LIGHT_3: SupportedModels.STRIP_LIGHT_3,
@@ -134,8 +138,10 @@ ENCRYPTED_MODELS = {
SwitchbotModel.LOCK_PRO,
SwitchbotModel.LOCK_LITE,
SwitchbotModel.LOCK_ULTRA,
SwitchbotModel.AIR_PURIFIER,
SwitchbotModel.AIR_PURIFIER_TABLE,
SwitchbotModel.AIR_PURIFIER_JP,
SwitchbotModel.AIR_PURIFIER_US,
SwitchbotModel.AIR_PURIFIER_TABLE_JP,
SwitchbotModel.AIR_PURIFIER_TABLE_US,
SwitchbotModel.EVAPORATIVE_HUMIDIFIER,
SwitchbotModel.FLOOR_LAMP,
SwitchbotModel.STRIP_LIGHT_3,
@@ -159,8 +165,10 @@ ENCRYPTED_SWITCHBOT_MODEL_TO_CLASS: dict[
SwitchbotModel.RELAY_SWITCH_1: switchbot.SwitchbotRelaySwitch,
SwitchbotModel.LOCK_LITE: switchbot.SwitchbotLock,
SwitchbotModel.LOCK_ULTRA: switchbot.SwitchbotLock,
SwitchbotModel.AIR_PURIFIER: switchbot.SwitchbotAirPurifier,
SwitchbotModel.AIR_PURIFIER_TABLE: switchbot.SwitchbotAirPurifier,
SwitchbotModel.AIR_PURIFIER_JP: switchbot.SwitchbotAirPurifier,
SwitchbotModel.AIR_PURIFIER_US: switchbot.SwitchbotAirPurifier,
SwitchbotModel.AIR_PURIFIER_TABLE_JP: switchbot.SwitchbotAirPurifier,
SwitchbotModel.AIR_PURIFIER_TABLE_US: switchbot.SwitchbotAirPurifier,
SwitchbotModel.EVAPORATIVE_HUMIDIFIER: switchbot.SwitchbotEvaporativeHumidifier,
SwitchbotModel.FLOOR_LAMP: switchbot.SwitchbotStripLight3,
SwitchbotModel.STRIP_LIGHT_3: switchbot.SwitchbotStripLight3,
@@ -179,6 +187,11 @@ HASS_SENSOR_TYPE_TO_SWITCHBOT_MODEL = {
str(v): k for k, v in SUPPORTED_MODEL_TYPES.items()
}
# Deprecated sensor type values used before pySwitchbot 2.0.0.
# AIR_PURIFIER and AIR_PURIFIER_TABLE were split into JP/US variants.
DEPRECATED_SENSOR_TYPE_AIR_PURIFIER = "air_purifier"
DEPRECATED_SENSOR_TYPE_AIR_PURIFIER_TABLE = "air_purifier_table"
# Config Defaults
DEFAULT_RETRY_COUNT = 3
DEFAULT_LOCK_NIGHTLATCH = False

View File

@@ -7,8 +7,7 @@ import logging
from typing import Any, Concatenate
import switchbot
from switchbot import Switchbot, SwitchbotDevice
from switchbot.devices.device import SwitchbotOperationError
from switchbot import Switchbot, SwitchbotDevice, SwitchbotOperationError
from homeassistant.components.bluetooth.passive_update_coordinator import (
PassiveBluetoothCoordinatorEntity,

View File

@@ -42,5 +42,5 @@
"iot_class": "local_push",
"loggers": ["switchbot"],
"quality_scale": "gold",
"requirements": ["PySwitchbot==1.1.0"]
"requirements": ["PySwitchbot==2.0.0"]
}

View File

@@ -6,7 +6,7 @@ from datetime import timedelta
import logging
import switchbot
from switchbot.devices.device import SwitchbotOperationError
from switchbot import SwitchbotOperationError
from homeassistant.components.select import SelectEntity
from homeassistant.const import EntityCategory

2
requirements_all.txt generated
View File

@@ -83,7 +83,7 @@ PyRMVtransport==0.3.3
PySrDaliGateway==0.19.3
# homeassistant.components.switchbot
PySwitchbot==1.1.0
PySwitchbot==2.0.0
# homeassistant.components.switchmate
PySwitchmate==0.5.1

View File

@@ -83,7 +83,7 @@ PyRMVtransport==0.3.3
PySrDaliGateway==0.19.3
# homeassistant.components.switchbot
PySwitchbot==1.1.0
PySwitchbot==2.0.0
# homeassistant.components.syncthru
PySyncThru==0.8.0

View File

@@ -761,7 +761,7 @@ LOCK_ULTRA_SERVICE_INFO = BluetoothServiceInfoBleak(
)
AIR_PURIFIER_TBALE_PM25_SERVICE_INFO = BluetoothServiceInfoBleak(
AIR_PURIFIER_TABLE_PM25_SERVICE_INFO = BluetoothServiceInfoBleak(
name="Air Purifier Table PM25",
manufacturer_data={
2409: b"\xf0\x9e\x9e\x96j\xd6\xa1\x81\x88\xe4\x00\x01\x95\x00\x00",

View File

@@ -4,7 +4,7 @@ from collections.abc import Callable
from unittest.mock import AsyncMock, patch
import pytest
from switchbot.devices.device import SwitchbotOperationError
from switchbot import SwitchbotOperationError
from homeassistant.components.bluetooth import BluetoothServiceInfoBleak
from homeassistant.components.cover import (

View File

@@ -4,7 +4,7 @@ from collections.abc import Callable
from unittest.mock import AsyncMock, patch
import pytest
from switchbot.devices.device import SwitchbotOperationError
from switchbot import SwitchbotOperationError
from homeassistant.components.bluetooth import BluetoothServiceInfoBleak
from homeassistant.components.fan import (
@@ -22,8 +22,8 @@ from homeassistant.exceptions import HomeAssistantError
from . import (
AIR_PURIFIER_PM25_SERVICE_INFO,
AIR_PURIFIER_TABLE_PM25_SERVICE_INFO,
AIR_PURIFIER_TABLE_VOC_SERVICE_INFO,
AIR_PURIFIER_TBALE_PM25_SERVICE_INFO,
AIR_PURIFIER_VOC_SERVICE_INFO,
CIRCULATOR_FAN_SERVICE_INFO,
)
@@ -103,10 +103,10 @@ async def test_circulator_fan_controlling(
@pytest.mark.parametrize(
("service_info", "sensor_type"),
[
(AIR_PURIFIER_VOC_SERVICE_INFO, "air_purifier"),
(AIR_PURIFIER_TABLE_VOC_SERVICE_INFO, "air_purifier_table"),
(AIR_PURIFIER_PM25_SERVICE_INFO, "air_purifier"),
(AIR_PURIFIER_TBALE_PM25_SERVICE_INFO, "air_purifier_table"),
(AIR_PURIFIER_VOC_SERVICE_INFO, "air_purifier_jp"),
(AIR_PURIFIER_TABLE_VOC_SERVICE_INFO, "air_purifier_table_jp"),
(AIR_PURIFIER_PM25_SERVICE_INFO, "air_purifier_us"),
(AIR_PURIFIER_TABLE_PM25_SERVICE_INFO, "air_purifier_table_us"),
],
)
@pytest.mark.parametrize(
@@ -169,10 +169,10 @@ async def test_air_purifier_controlling(
@pytest.mark.parametrize(
("service_info", "sensor_type"),
[
(AIR_PURIFIER_VOC_SERVICE_INFO, "air_purifier"),
(AIR_PURIFIER_TABLE_VOC_SERVICE_INFO, "air_purifier_table"),
(AIR_PURIFIER_PM25_SERVICE_INFO, "air_purifier"),
(AIR_PURIFIER_TBALE_PM25_SERVICE_INFO, "air_purifier_table"),
(AIR_PURIFIER_VOC_SERVICE_INFO, "air_purifier_jp"),
(AIR_PURIFIER_TABLE_VOC_SERVICE_INFO, "air_purifier_table_jp"),
(AIR_PURIFIER_PM25_SERVICE_INFO, "air_purifier_us"),
(AIR_PURIFIER_TABLE_PM25_SERVICE_INFO, "air_purifier_table_us"),
],
)
@pytest.mark.parametrize(

View File

@@ -4,7 +4,7 @@ from collections.abc import Callable
from unittest.mock import AsyncMock, patch
import pytest
from switchbot.devices.device import SwitchbotOperationError
from switchbot import SwitchbotOperationError
from homeassistant.components.humidifier import (
ATTR_HUMIDITY,

View File

@@ -5,17 +5,25 @@ from unittest.mock import AsyncMock, patch
import pytest
from homeassistant.components.bluetooth import BluetoothServiceInfoBleak
from homeassistant.components.switchbot.const import (
CONF_CURTAIN_SPEED,
CONF_RETRY_COUNT,
DEFAULT_CURTAIN_SPEED,
DEFAULT_RETRY_COUNT,
DEPRECATED_SENSOR_TYPE_AIR_PURIFIER,
DEPRECATED_SENSOR_TYPE_AIR_PURIFIER_TABLE,
DOMAIN,
)
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import CONF_ADDRESS, CONF_NAME, CONF_SENSOR_TYPE
from homeassistant.core import HomeAssistant
from . import (
AIR_PURIFIER_PM25_SERVICE_INFO,
AIR_PURIFIER_TABLE_PM25_SERVICE_INFO,
AIR_PURIFIER_TABLE_VOC_SERVICE_INFO,
AIR_PURIFIER_VOC_SERVICE_INFO,
HUBMINI_MATTER_SERVICE_INFO,
LOCK_SERVICE_INFO,
WOCURTAIN_SERVICE_INFO,
@@ -211,3 +219,83 @@ async def test_migrate_entry_fails_for_future_version(
# Entry should not be loaded due to failed migration
assert entry.version == 2
assert entry.minor_version == 1
@pytest.mark.parametrize(
("old_sensor_type", "service_info", "expected_sensor_type"),
[
(
DEPRECATED_SENSOR_TYPE_AIR_PURIFIER,
AIR_PURIFIER_VOC_SERVICE_INFO,
"air_purifier_jp",
),
(
DEPRECATED_SENSOR_TYPE_AIR_PURIFIER,
AIR_PURIFIER_PM25_SERVICE_INFO,
"air_purifier_us",
),
(
DEPRECATED_SENSOR_TYPE_AIR_PURIFIER_TABLE,
AIR_PURIFIER_TABLE_VOC_SERVICE_INFO,
"air_purifier_table_jp",
),
(
DEPRECATED_SENSOR_TYPE_AIR_PURIFIER_TABLE,
AIR_PURIFIER_TABLE_PM25_SERVICE_INFO,
"air_purifier_table_us",
),
],
)
async def test_migrate_deprecated_air_purifier_sensor_type(
hass: HomeAssistant,
old_sensor_type: str,
service_info: BluetoothServiceInfoBleak,
expected_sensor_type: str,
) -> None:
"""Test that deprecated air_purifier sensor types are migrated via BLE advertisement."""
inject_bluetooth_service_info(hass, service_info)
entry = MockConfigEntry(
domain=DOMAIN,
data={
CONF_ADDRESS: "aa:bb:cc:dd:ee:ff",
CONF_NAME: "test-name",
CONF_SENSOR_TYPE: old_sensor_type,
},
unique_id="aabbccddeeff",
version=1,
minor_version=2,
options={CONF_RETRY_COUNT: DEFAULT_RETRY_COUNT},
)
entry.add_to_hass(hass)
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
assert entry.data[CONF_SENSOR_TYPE] == expected_sensor_type
async def test_migrate_deprecated_air_purifier_sensor_type_device_not_in_range(
hass: HomeAssistant,
) -> None:
"""Test deprecated air_purifier type entry is not loaded when device is out of range."""
entry = MockConfigEntry(
domain=DOMAIN,
data={
CONF_ADDRESS: "aa:bb:cc:dd:ee:ff",
CONF_NAME: "test-name",
CONF_SENSOR_TYPE: DEPRECATED_SENSOR_TYPE_AIR_PURIFIER,
},
unique_id="aabbccddeeff",
version=1,
minor_version=2,
options={CONF_RETRY_COUNT: DEFAULT_RETRY_COUNT},
)
entry.add_to_hass(hass)
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
# sensor_type unchanged and entry not loaded; will retry when device advertises
assert entry.data[CONF_SENSOR_TYPE] == DEPRECATED_SENSOR_TYPE_AIR_PURIFIER
assert entry.state is ConfigEntryState.SETUP_RETRY

View File

@@ -5,7 +5,7 @@ from typing import Any
from unittest.mock import AsyncMock, patch
import pytest
from switchbot.devices.device import SwitchbotOperationError
from switchbot import SwitchbotOperationError
from homeassistant.components.bluetooth import BluetoothServiceInfoBleak
from homeassistant.components.light import (

View File

@@ -4,7 +4,7 @@ from collections.abc import Callable
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from switchbot.devices.device import SwitchbotOperationError
from switchbot import SwitchbotOperationError
from homeassistant.components.bluetooth import BluetoothServiceInfoBleak
from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN

View File

@@ -4,7 +4,7 @@ from collections.abc import Callable
from unittest.mock import AsyncMock, patch
import pytest
from switchbot.devices.device import SwitchbotOperationError
from switchbot import SwitchbotOperationError
from homeassistant.components.bluetooth import BluetoothServiceInfoBleak
from homeassistant.components.switch import (