From e78a7eb7ab7a13cb2d5966dace82bec20b47cf62 Mon Sep 17 00:00:00 2001 From: hcoohb Date: Sat, 26 Sep 2020 22:30:00 +1000 Subject: [PATCH] Allow for scan in UI --- custom_components/yeelight_bt/config_flow.py | 90 +++++++++++++------ custom_components/yeelight_bt/const.py | 3 + .../yeelight_bt/translations/en.json | 24 ++++- custom_components/yeelight_bt/yeelightbt.py | 11 ++- 4 files changed, 95 insertions(+), 33 deletions(-) diff --git a/custom_components/yeelight_bt/config_flow.py b/custom_components/yeelight_bt/config_flow.py index d628d3d..447beea 100644 --- a/custom_components/yeelight_bt/config_flow.py +++ b/custom_components/yeelight_bt/config_flow.py @@ -3,57 +3,93 @@ from homeassistant import config_entries from homeassistant.const import CONF_NAME, CONF_MAC import voluptuous as vol -from homeassistant.helpers import device_registry as dr, config_validation as cv +from homeassistant.helpers import device_registry as dr -from .const import DOMAIN +from .const import DOMAIN, CONF_ENTRY_METHOD, CONF_ENTRY_SCAN, CONF_ENTRY_MANUAL +from .yeelightbt import ( + discover_yeelight_lamps, + BTLEDisconnectError, + BTLEManagementError, +) _LOGGER = logging.getLogger(__name__) -class SimpleConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class Yeelight_btConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): # type: ignore """Handle a config flow for yeelight_bt.""" - VERSION = 1 + VERSION = 2 CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL @property def data_schema(self): """Return the data schema for integration.""" - return vol.Schema( - { - vol.Required(CONF_NAME): str, - vol.Required(CONF_MAC): str, - } - ) + return vol.Schema({vol.Required(CONF_NAME): str, vol.Required(CONF_MAC): str}) async def async_step_user(self, user_input=None): - """Handle the initial step.""" + """Handle a flow initialized by the user.""" + + if user_input is None: + schema = { + vol.Required(CONF_ENTRY_METHOD): vol.In( + [CONF_ENTRY_SCAN, CONF_ENTRY_MANUAL] + ) + } + return self.async_show_form(step_id="user", data_schema=vol.Schema(schema)) + method = user_input[CONF_ENTRY_METHOD] + _LOGGER.debug(f"Method selected: {method}") + if method == CONF_ENTRY_SCAN: + return await self.async_step_scan() + else: + self.devices = [] + return await self.async_step_device() + + async def async_step_scan(self, user_input=None): + """Handle the discovery by scanning.""" errors = {} - _LOGGER.debug(f"User_input: {user_input}") + if user_input is None: + return self.async_show_form(step_id="scan") + _LOGGER.debug("Starting a scan for Yeelight Bt devices") + try: + devices = await self.hass.async_add_executor_job(discover_yeelight_lamps) + except BTLEDisconnectError as err: + _LOGGER.error(f"Bluetooth connection error while trying to scan: {err}") + errors["base"] = "BTLEDisconnectError" + return self.async_show_form(step_id="scan", errors=errors) + except BTLEManagementError as err: + _LOGGER.error(f"Bluetooth connection error while trying to scan: {err}") + errors["base"] = "BTLEManagementError" + return self.async_show_form(step_id="scan", errors=errors) + + if not devices: + return self.async_abort(reason="no_devices_found") + self.devices = [f"{dev['mac']} ({dev['model']})" for dev in devices] + # TODO: filter existing devices ? + + return await self.async_step_device() + + async def async_step_device(self, user_input=None): + """Handle setting up a device.""" + # _LOGGER.debug(f"User_input: {user_input}") if not user_input: - return self.async_show_form(step_id="user", data_schema=self.data_schema) + schema_mac = str + if self.devices: + schema_mac = vol.In(self.devices) + schema = vol.Schema( + {vol.Required(CONF_NAME): str, vol.Required(CONF_MAC): schema_mac} + ) + return self.async_show_form(step_id="device", data_schema=schema) + user_input[CONF_MAC] = user_input[CONF_MAC][:17] unique_id = dr.format_mac(user_input[CONF_MAC]) - _LOGGER.debug(f"UniqueID: {unique_id}") + _LOGGER.debug(f"Yeelight UniqueID: {unique_id}") await self.async_set_unique_id(unique_id) self._abort_if_unique_id_configured() - # try: - # #Check if we can connect to the lamp here - # await client.cdc_reports.status_by_coordinates( - # user_input[CONF_LATITUDE], user_input[CONF_LONGITUDE] - # ) - # except FluNearYouError as err: - # _LOGGER.error("Error while configuring integration: %s", err) - # errors["base"] = "auth_error" - # return self.async_show_form( - # step_id="user", errors=errors - # ) - return self.async_create_entry(title=user_input[CONF_NAME], data=user_input) async def async_step_import(self, import_info): """Handle import from config file.""" - return await self.async_step_user(import_info) \ No newline at end of file + return await self.async_step_device(import_info) diff --git a/custom_components/yeelight_bt/const.py b/custom_components/yeelight_bt/const.py index 0db5ff1..f8f6966 100644 --- a/custom_components/yeelight_bt/const.py +++ b/custom_components/yeelight_bt/const.py @@ -4,3 +4,6 @@ DOMAIN = "yeelight_bt" PLATFORM = "light" +CONF_ENTRY_METHOD = "entry_method" +CONF_ENTRY_SCAN = "Scan" +CONF_ENTRY_MANUAL = "Enter MAC manually" diff --git a/custom_components/yeelight_bt/translations/en.json b/custom_components/yeelight_bt/translations/en.json index 35431b6..4598589 100644 --- a/custom_components/yeelight_bt/translations/en.json +++ b/custom_components/yeelight_bt/translations/en.json @@ -3,14 +3,32 @@ "step": { "user": { "title": "Yeelight Bluetooth", - "description": "Control a Xiaomi Yeelight bluetooth bedside lamp.", + "description": "Control a Yeelight bluetooth bedside lamp. Select to scan for devices or enter the mac Address manually.", + "data": { + "entry_method": "Method to be used:" + } + }, + "scan": { + "title": "Yeelight Bluetooth", + "description": "Make sure the lamp is not connected to other devices or it may not be discoverable (A reset may also help). Are you ready to start scanning?" + }, + "device": { + "title": "Yeelight Bluetooth", + "description": "Enter a name for the device and the MAC address for the lamp. To finish the pairing step, you may need to push the small button on the lamp if it `pulses`.", "data": { "name": "Lamp Name", "mac": "MAC address" } } }, - "error": {"general_error": "There was an unknown error." }, - "abort": {"already_configured": "This mac address is already registered."} + "error": { + "general_error": "There was an unknown error.", + "BTLEDisconnectError": "Bluetooth connection error while trying to scan. Make sure nothing else is using bluetooth.", + "BTLEManagementError": "Could not start a bluetooth scan. It is very likely some permission errors. Follow directions from github repo." + }, + "abort": { + "already_configured": "This mac address is already registered.", + "no_devices_found": "No devices found during this scan. Enusre the lamp is not connected to another app. Reseting the lamp may help" + } } } \ No newline at end of file diff --git a/custom_components/yeelight_bt/yeelightbt.py b/custom_components/yeelight_bt/yeelightbt.py index deb2a7b..f19740e 100644 --- a/custom_components/yeelight_bt/yeelightbt.py +++ b/custom_components/yeelight_bt/yeelightbt.py @@ -12,7 +12,7 @@ # 3rd party imports import bluepy # for BLE transmission - +from bluepy.btle import BTLEDisconnectError, BTLEManagementError # noqa NOTIFY_UUID = "8f65073d-9f57-4aaa-afea-397d19d5bbeb" CONTROL_UUID = "aa7d3f34-2d4f-41e0-807f-52fbf8cf7443" @@ -537,9 +537,14 @@ def __init__(self): for dev in devices: # _LOGGER.debug(f"found {dev.addr} = {dev.getScanData()}") for (adtype, desc, value) in dev.getScanData(): + model = "" if "XMCTD" in value: - _LOGGER.info(f"found Yeelight lamp with mac: {dev.addr}") - lamp_list.append(Lamp(dev.addr)) + model = MODEL_BEDSIDE + if "yeelight_ms" in value: + model = MODEL_CANDELA + if model: + lamp_list.append({"mac": dev.addr, "model": model}) + _LOGGER.info(f"found {model} with mac: {dev.addr}, value:{value}") return lamp_list