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 16 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
18 changes: 18 additions & 0 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 @@ -129,6 +134,19 @@
raise ConfigEntryNotReady(
"Invalid encryption configuration provided"
) from error
elif switchbot_model in ENCRYPTED_MODELS:
try:
device = cls(
device=ble_device,
key_id=entry.data.get(CONF_KEY_ID),
encryption_key=entry.data.get(CONF_ENCRYPTION_KEY),
retry_count=entry.options[CONF_RETRY_COUNT],
model=switchbot_model,
)
except ValueError as error:
raise ConfigEntryNotReady(

Check warning on line 147 in homeassistant/components/switchbot/__init__.py

View check run for this annotation

Codecov / codecov/patch

homeassistant/components/switchbot/__init__.py#L146-L147

Added lines #L146 - L147 were not covered by tests
"Invalid encryption configuration provided"
) from error
else:
device = cls(
device=ble_device,
Expand Down
90 changes: 90 additions & 0 deletions homeassistant/components/switchbot/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
DEFAULT_LOCK_NIGHTLATCH,
DEFAULT_RETRY_COUNT,
DOMAIN,
ENCRYPTED_MODELS,
NON_CONNECTABLE_SUPPORTED_MODEL_TYPES,
SUPPORTED_LOCK_MODELS,
SUPPORTED_MODEL_TYPES,
Expand Down Expand Up @@ -114,6 +115,8 @@ async def async_step_bluetooth(
}
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_choose_method()
if self._discovered_adv.data["isEncrypted"]:
return await self.async_step_password()
return await self.async_step_confirm()
Expand Down Expand Up @@ -265,6 +268,91 @@ async def async_step_lock_key(
},
)

async def async_step_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="choose_method",
menu_options=["auth", "key"],
description_placeholders={
"name": name_from_discovery(self._discovered_adv),
},
)

async def async_step_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:
try:
key_details = await SwitchbotLock.async_retrieve_encryption_key(
bdraco marked this conversation as resolved.
Show resolved Hide resolved
async_get_clientsession(self.hass),
self._discovered_adv.address,
user_input[CONF_USERNAME],
user_input[CONF_PASSWORD],
)
except (SwitchbotApiError, SwitchbotAccountConnectionError) as ex:
_LOGGER.debug(
"Failed to connect to SwitchBot API: %s", ex, exc_info=True
)
return self.async_abort(
reason="api_error",
description_placeholders={"error_detail": str(ex)},
)
except SwitchbotAuthenticationError as ex:
_LOGGER.debug("Authentication failed: %s", ex, exc_info=True)
errors = {"base": "auth_failed"}
description_placeholders = {"error_detail": str(ex)}
else:
return await self.async_step_key(key_details)

user_input = user_input or {}
return self.async_show_form(
step_id="auth",
errors=errors,
data_schema=vol.Schema(
{
vol.Required(
CONF_USERNAME, default=user_input.get(CONF_USERNAME)
): str,
vol.Required(CONF_PASSWORD): str,
}
),
description_placeholders={
"name": name_from_discovery(self._discovered_adv),
**description_placeholders,
},
)

async def async_step_key(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle the encryption key step."""
errors: dict[str, str] = {}
assert self._discovered_adv is not None
if user_input is not None:
return await self._async_create_entry_from_discovery(user_input)

return self.async_show_form(
step_id="key",
errors=errors,
data_schema=vol.Schema(
{
vol.Required(CONF_KEY_ID): str,
vol.Required(CONF_ENCRYPTION_KEY): str,
}
),
description_placeholders={
"name": name_from_discovery(self._discovered_adv),
},
)

@callback
def _async_discover_devices(self) -> None:
current_addresses = self._async_current_ids()
Expand Down Expand Up @@ -323,6 +411,8 @@ async def async_step_user(
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_choose_method()
if device_adv.data["isEncrypted"]:
return await self.async_step_password()
return await self.async_step_confirm()
Expand Down
5 changes: 5 additions & 0 deletions homeassistant/components/switchbot/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,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 +46,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 @@ -60,6 +64,7 @@ class SupportedModels(StrEnum):
)

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

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
21 changes: 21 additions & 0 deletions homeassistant/components/switchbot/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,27 @@
"lock_auth": "SwitchBot account (recommended)",
"lock_key": "Enter lock encryption key manually"
}
},
"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"
}
},
"auth": {
"description": "Please provide your SwitchBot app username and password. This data won't be saved and only used to retrieve your device encryption key. Usernames and passwords are case sensitive.",
"data": {
"username": "[%key:common::config_flow::data::username%]",
"password": "[%key:common::config_flow::data::password%]"
}
},
"choose_method": {
"description": "A 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": {
"auth": "SwitchBot account (recommended)",
bdraco marked this conversation as resolved.
Show resolved Hide resolved
"key": "Enter lock encryption key manually"
}
}
},
"error": {
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