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 3 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 @@ async def async_setup_entry(hass: HomeAssistant, entry: SwitchbotConfigEntry) ->
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(
"Invalid encryption configuration provided"
) from error
else:
device = cls(
device=ble_device,
Expand Down
89 changes: 89 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,90 @@ 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(
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
)
raise AbortFlow(
"api_error", description_placeholders={"error_detail": str(ex)}
) from ex
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you can use return self.async_abort( instead here

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks,already changed to return self.async_abort(

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for your feedback. I have made the requested changes and addressed the issues. Please review again.

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 +410,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_PLUS: 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_PLUS, SwitchbotModel.RELAY_SWITCH_1PM}

HASS_SENSOR_TYPE_TO_SWITCHBOT_MODEL = {
str(v): k for k, v in SUPPORTED_MODEL_TYPES.items()
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/switchbot/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,5 +39,5 @@
"documentation": "https://www.home-assistant.io/integrations/switchbot",
"iot_class": "local_push",
"loggers": ["switchbot"],
"requirements": ["PySwitchbot==0.51.0"]
"requirements": ["PySwitchbot==0.53.0"]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please bump the dependency in a preliminary PR

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have added a link to the changelog in the PR description.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But can you please open a prelimary PR where we bump the version? Check point 7 at https://developers.home-assistant.io/docs/review-process#creating-the-perfect-pr for the explanation behind it :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I opened a prelimary PR here. #130869

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for your feedback. I have made the requested changes and addressed the issues. Please review again.

}
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.MILLIVOLT,
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.VOLTAGE,
),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In what unit of measurement is this the most usable? Because if this always measures 100V or 230V, we should not have the shown unit of measurement be millivolts. Rather we should set the suggested_unit_of_measurement to VOLT, so it will automatically come up like that

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for your valuable advice, I have fixed this problem. I also created a PR to bump the PySwitchbot at #131783. Could you please review the updated code at your earliest convenience?

}


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)",
"key": "Enter lock encryption key manually"
}
}
},
"error": {
Expand Down
2 changes: 1 addition & 1 deletion requirements_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ PyQRCode==1.2.1
PyRMVtransport==0.3.3

# homeassistant.components.switchbot
PySwitchbot==0.51.0
PySwitchbot==0.53.0

# homeassistant.components.switchmate
PySwitchmate==0.5.1
Expand Down
2 changes: 1 addition & 1 deletion requirements_test_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ PyQRCode==1.2.1
PyRMVtransport==0.3.3

# homeassistant.components.switchbot
PySwitchbot==0.51.0
PySwitchbot==0.53.0

# homeassistant.components.syncthru
PySyncThru==0.7.10
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