Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add initial support for SwitchBot relay switch #130863

Merged
merged 34 commits into from
Dec 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
e581d25
Support relay switch
greyeee Nov 15, 2024
60f1e83
更新下版本
greyeee Nov 18, 2024
4b1cb67
add test case
greyeee Nov 18, 2024
eb2b5de
Merge branch 'dev' into feature/relay-switch
greyeee Nov 18, 2024
87a706d
change to async_abort
greyeee Nov 18, 2024
9f49927
Merge branch 'feature/relay-switch' of https://github.com/greyeee/cor…
greyeee Nov 18, 2024
b29a7e4
Merge branch 'dev' into feature/relay-switch
greyeee Nov 18, 2024
74bf688
Upgrade PySwitchbot to 0.53.2
greyeee Nov 19, 2024
dab6e8c
Merge branch 'feature/relay-switch' of https://github.com/greyeee/cor…
greyeee Nov 19, 2024
d74f14f
Merge branch 'dev' into feature/relay-switch
greyeee Nov 19, 2024
239bb00
Merge branch 'dev' into feature/relay-switch
bdraco Nov 19, 2024
6dd242a
change unit to volt
greyeee Nov 28, 2024
8fabc07
upgrade pySwitchbot dependency
greyeee Nov 28, 2024
082a64e
Merge branch 'feature/relay-switch' of https://github.com/greyeee/cor…
greyeee Nov 28, 2024
4a750d7
Merge branch 'dev' into feature/relay-switch
joostlek Dec 12, 2024
6101335
Merge branch 'dev' into feature/relay-switch
greyeee Dec 16, 2024
690dbfe
bump lib, will be split into a seperate PR after testing is finished
bdraco Dec 20, 2024
54a2689
Merge branch 'dev' into feature/relay-switch
bdraco Dec 20, 2024
ffd1196
dry
bdraco Dec 20, 2024
efb6ff9
dry
bdraco Dec 20, 2024
56478df
dry
bdraco Dec 20, 2024
6c46cde
dry
bdraco Dec 20, 2024
7aaa1a6
dry
bdraco Dec 20, 2024
062634f
dry
bdraco Dec 20, 2024
fb5e040
dry
bdraco Dec 20, 2024
9c4bab3
update tests
bdraco Dec 20, 2024
45e30ba
fixes
bdraco Dec 20, 2024
47ddf59
fixes
bdraco Dec 20, 2024
9904851
cleanups
bdraco Dec 20, 2024
ca5bae7
fixes
bdraco Dec 20, 2024
c42faf7
fixes
bdraco Dec 20, 2024
cd9bbfa
fixes
bdraco Dec 20, 2024
5edb5e7
bump again
bdraco Dec 20, 2024
07bca62
Merge branch 'dev' into feature/relay-switch
bdraco Dec 20, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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