Skip to content

Commit

Permalink
Add initial support for SwitchBot relay switch (#130863)
Browse files Browse the repository at this point in the history
* Support relay switch

* 更新下版本

* add test case

* change to async_abort

* Upgrade PySwitchbot to 0.53.2

* change unit to volt

* upgrade pySwitchbot dependency

* bump lib, will be split into a seperate PR after testing is finished

* dry

* dry

* dry

* dry

* dry

* dry

* dry

* update tests

* fixes

* fixes

* cleanups

* fixes

* fixes

* fixes

* bump again

---------

Co-authored-by: J. Nick Koston <[email protected]>
Co-authored-by: Joost Lekkerkerker <[email protected]>
  • Loading branch information
3 people authored Dec 20, 2024
1 parent b6819cb commit 861d9b3
Show file tree
Hide file tree
Showing 8 changed files with 379 additions and 62 deletions.
9 changes: 7 additions & 2 deletions homeassistant/components/switchbot/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
CONF_RETRY_COUNT,
CONNECTABLE_SUPPORTED_MODEL_TYPES,
DEFAULT_RETRY_COUNT,
ENCRYPTED_MODELS,
HASS_SENSOR_TYPE_TO_SWITCHBOT_MODEL,
SupportedModels,
)
Expand Down Expand Up @@ -61,6 +62,8 @@
Platform.SENSOR,
],
SupportedModels.HUB2.value: [Platform.SENSOR],
SupportedModels.RELAY_SWITCH_1PM.value: [Platform.SWITCH, Platform.SENSOR],
SupportedModels.RELAY_SWITCH_1.value: [Platform.SWITCH],
}
CLASS_BY_DEVICE = {
SupportedModels.CEILING_LIGHT.value: switchbot.SwitchbotCeilingLight,
Expand All @@ -73,6 +76,8 @@
SupportedModels.LOCK.value: switchbot.SwitchbotLock,
SupportedModels.LOCK_PRO.value: switchbot.SwitchbotLock,
SupportedModels.BLIND_TILT.value: switchbot.SwitchbotBlindTilt,
SupportedModels.RELAY_SWITCH_1PM.value: switchbot.SwitchbotRelaySwitch,
SupportedModels.RELAY_SWITCH_1.value: switchbot.SwitchbotRelaySwitch,
}


Expand Down Expand Up @@ -116,9 +121,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: SwitchbotConfigEntry) ->
)

cls = CLASS_BY_DEVICE.get(sensor_type, switchbot.SwitchbotDevice)
if cls is switchbot.SwitchbotLock:
if switchbot_model in ENCRYPTED_MODELS:
try:
device = switchbot.SwitchbotLock(
device = cls(
device=ble_device,
key_id=entry.data.get(CONF_KEY_ID),
encryption_key=entry.data.get(CONF_ENCRYPTION_KEY),
Expand Down
43 changes: 24 additions & 19 deletions homeassistant/components/switchbot/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
SwitchBotAdvertisement,
SwitchbotApiError,
SwitchbotAuthenticationError,
SwitchbotLock,
SwitchbotModel,
parse_advertisement_data,
)
import voluptuous as vol
Expand Down Expand Up @@ -44,8 +44,9 @@
DEFAULT_LOCK_NIGHTLATCH,
DEFAULT_RETRY_COUNT,
DOMAIN,
ENCRYPTED_MODELS,
ENCRYPTED_SWITCHBOT_MODEL_TO_CLASS,
NON_CONNECTABLE_SUPPORTED_MODEL_TYPES,
SUPPORTED_LOCK_MODELS,
SUPPORTED_MODEL_TYPES,
SupportedModels,
)
Expand Down Expand Up @@ -112,8 +113,8 @@ async def async_step_bluetooth(
"name": data["modelFriendlyName"],
"address": short_address(discovery_info.address),
}
if model_name in SUPPORTED_LOCK_MODELS:
return await self.async_step_lock_choose_method()
if model_name in ENCRYPTED_MODELS:
return await self.async_step_encrypted_choose_method()
if self._discovered_adv.data["isEncrypted"]:
return await self.async_step_password()
return await self.async_step_confirm()
Expand Down Expand Up @@ -171,16 +172,18 @@ async def async_step_password(
},
)

async def async_step_lock_auth(
async def async_step_encrypted_auth(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle the SwitchBot API auth step."""
errors = {}
assert self._discovered_adv is not None
description_placeholders = {}
if user_input is not None:
model: SwitchbotModel = self._discovered_adv.data["modelName"]
cls = ENCRYPTED_SWITCHBOT_MODEL_TO_CLASS[model]
try:
key_details = await SwitchbotLock.async_retrieve_encryption_key(
key_details = await cls.async_retrieve_encryption_key(
async_get_clientsession(self.hass),
self._discovered_adv.address,
user_input[CONF_USERNAME],
Expand All @@ -198,11 +201,11 @@ async def async_step_lock_auth(
errors = {"base": "auth_failed"}
description_placeholders = {"error_detail": str(ex)}
else:
return await self.async_step_lock_key(key_details)
return await self.async_step_encrypted_key(key_details)

user_input = user_input or {}
return self.async_show_form(
step_id="lock_auth",
step_id="encrypted_auth",
errors=errors,
data_schema=vol.Schema(
{
Expand All @@ -218,32 +221,34 @@ async def async_step_lock_auth(
},
)

async def async_step_lock_choose_method(
async def async_step_encrypted_choose_method(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle the SwitchBot API chose method step."""
assert self._discovered_adv is not None

return self.async_show_menu(
step_id="lock_choose_method",
menu_options=["lock_auth", "lock_key"],
step_id="encrypted_choose_method",
menu_options=["encrypted_auth", "encrypted_key"],
description_placeholders={
"name": name_from_discovery(self._discovered_adv),
},
)

async def async_step_lock_key(
async def async_step_encrypted_key(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle the encryption key step."""
errors = {}
assert self._discovered_adv is not None
if user_input is not None:
if not await SwitchbotLock.verify_encryption_key(
model: SwitchbotModel = self._discovered_adv.data["modelName"]
cls = ENCRYPTED_SWITCHBOT_MODEL_TO_CLASS[model]
if not await cls.verify_encryption_key(
self._discovered_adv.device,
user_input[CONF_KEY_ID],
user_input[CONF_ENCRYPTION_KEY],
model=self._discovered_adv.data["modelName"],
model=model,
):
errors = {
"base": "encryption_key_invalid",
Expand All @@ -252,7 +257,7 @@ async def async_step_lock_key(
return await self._async_create_entry_from_discovery(user_input)

return self.async_show_form(
step_id="lock_key",
step_id="encrypted_key",
errors=errors,
data_schema=vol.Schema(
{
Expand Down Expand Up @@ -309,8 +314,8 @@ async def async_step_user(
if user_input is not None:
device_adv = self._discovered_advs[user_input[CONF_ADDRESS]]
await self._async_set_device(device_adv)
if device_adv.data.get("modelName") in SUPPORTED_LOCK_MODELS:
return await self.async_step_lock_choose_method()
if device_adv.data.get("modelName") in ENCRYPTED_MODELS:
return await self.async_step_encrypted_choose_method()
if device_adv.data["isEncrypted"]:
return await self.async_step_password()
return await self._async_create_entry_from_discovery(user_input)
Expand All @@ -321,8 +326,8 @@ async def async_step_user(
# or simply confirm it
device_adv = list(self._discovered_advs.values())[0]
await self._async_set_device(device_adv)
if device_adv.data.get("modelName") in SUPPORTED_LOCK_MODELS:
return await self.async_step_lock_choose_method()
if device_adv.data.get("modelName") in ENCRYPTED_MODELS:
return await self.async_step_encrypted_choose_method()
if device_adv.data["isEncrypted"]:
return await self.async_step_password()
return await self.async_step_confirm()
Expand Down
21 changes: 20 additions & 1 deletion homeassistant/components/switchbot/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from enum import StrEnum

import switchbot
from switchbot import SwitchbotModel

DOMAIN = "switchbot"
Expand Down Expand Up @@ -30,6 +31,8 @@ class SupportedModels(StrEnum):
LOCK_PRO = "lock_pro"
BLIND_TILT = "blind_tilt"
HUB2 = "hub2"
RELAY_SWITCH_1PM = "relay_switch_1pm"
RELAY_SWITCH_1 = "relay_switch_1"


CONNECTABLE_SUPPORTED_MODEL_TYPES = {
Expand All @@ -44,6 +47,8 @@ class SupportedModels(StrEnum):
SwitchbotModel.LOCK_PRO: SupportedModels.LOCK_PRO,
SwitchbotModel.BLIND_TILT: SupportedModels.BLIND_TILT,
SwitchbotModel.HUB2: SupportedModels.HUB2,
SwitchbotModel.RELAY_SWITCH_1PM: SupportedModels.RELAY_SWITCH_1PM,
SwitchbotModel.RELAY_SWITCH_1: SupportedModels.RELAY_SWITCH_1,
}

NON_CONNECTABLE_SUPPORTED_MODEL_TYPES = {
Expand All @@ -59,7 +64,21 @@ class SupportedModels(StrEnum):
CONNECTABLE_SUPPORTED_MODEL_TYPES | NON_CONNECTABLE_SUPPORTED_MODEL_TYPES
)

SUPPORTED_LOCK_MODELS = {SwitchbotModel.LOCK, SwitchbotModel.LOCK_PRO}
ENCRYPTED_MODELS = {
SwitchbotModel.RELAY_SWITCH_1,
SwitchbotModel.RELAY_SWITCH_1PM,
SwitchbotModel.LOCK,
SwitchbotModel.LOCK_PRO,
}

ENCRYPTED_SWITCHBOT_MODEL_TO_CLASS: dict[
SwitchbotModel, switchbot.SwitchbotEncryptedDevice
] = {
SwitchbotModel.LOCK: switchbot.SwitchbotLock,
SwitchbotModel.LOCK_PRO: switchbot.SwitchbotLock,
SwitchbotModel.RELAY_SWITCH_1PM: switchbot.SwitchbotRelaySwitch,
SwitchbotModel.RELAY_SWITCH_1: switchbot.SwitchbotRelaySwitch,
}

HASS_SENSOR_TYPE_TO_SWITCHBOT_MODEL = {
str(v): k for k, v in SUPPORTED_MODEL_TYPES.items()
Expand Down
14 changes: 14 additions & 0 deletions homeassistant/components/switchbot/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
PERCENTAGE,
SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
EntityCategory,
UnitOfElectricCurrent,
UnitOfElectricPotential,
UnitOfPower,
UnitOfTemperature,
)
Expand Down Expand Up @@ -82,6 +84,18 @@
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.POWER,
),
"current": SensorEntityDescription(
key="current",
native_unit_of_measurement=UnitOfElectricCurrent.MILLIAMPERE,
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.CURRENT,
),
"voltage": SensorEntityDescription(
key="voltage",
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.VOLTAGE,
),
}


Expand Down
14 changes: 7 additions & 7 deletions homeassistant/components/switchbot/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,25 +16,25 @@
"password": "[%key:common::config_flow::data::password%]"
}
},
"lock_key": {
"encrypted_key": {
"description": "The {name} device requires encryption key, details on how to obtain it can be found in the documentation.",
"data": {
"key_id": "Key ID",
"encryption_key": "Encryption key"
}
},
"lock_auth": {
"description": "Please provide your SwitchBot app username and password. This data won't be saved and only used to retrieve your locks encryption key. Usernames and passwords are case sensitive.",
"encrypted_auth": {
"description": "Please provide your SwitchBot app username and password. This data won't be saved and only used to retrieve your device's encryption key. Usernames and passwords are case sensitive.",
"data": {
"username": "[%key:common::config_flow::data::username%]",
"password": "[%key:common::config_flow::data::password%]"
}
},
"lock_choose_method": {
"description": "A SwitchBot lock can be set up in Home Assistant in two different ways.\n\nYou can enter the key id and encryption key yourself, or Home Assistant can import them from your SwitchBot account.",
"encrypted_choose_method": {
"description": "An encrypted SwitchBot device can be set up in Home Assistant in two different ways.\n\nYou can enter the key id and encryption key yourself, or Home Assistant can import them from your SwitchBot account.",
"menu_options": {
"lock_auth": "SwitchBot account (recommended)",
"lock_key": "Enter lock encryption key manually"
"encrypted_auth": "SwitchBot account (recommended)",
"encrypted_key": "Enter encryption key manually"
}
}
},
Expand Down
20 changes: 20 additions & 0 deletions tests/components/switchbot/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -230,3 +230,23 @@ async def init_integration(hass: HomeAssistant) -> MockConfigEntry:
connectable=True,
tx_power=-127,
)

WORELAY_SWITCH_1PM_SERVICE_INFO = BluetoothServiceInfoBleak(
name="W1080000",
manufacturer_data={2409: b"$X|\x0866G\x81\x00\x00\x001\x00\x00\x00\x00"},
service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"<\x00\x00\x00"},
service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"],
address="AA:BB:CC:DD:EE:FF",
rssi=-60,
source="local",
advertisement=generate_advertisement_data(
local_name="W1080000",
manufacturer_data={2409: b"$X|\x0866G\x81\x00\x00\x001\x00\x00\x00\x00"},
service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"<\x00\x00\x00"},
service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"],
),
device=generate_ble_device("AA:BB:CC:DD:EE:FF", "W1080000"),
time=0,
connectable=True,
tx_power=-127,
)
Loading

0 comments on commit 861d9b3

Please sign in to comment.