diff options
author | Alexey 'Cluster' Avdyukhin <clusterrr@clusterrr.com> | 2022-04-02 16:46:58 +0300 |
---|---|---|
committer | Alexey 'Cluster' Avdyukhin <clusterrr@clusterrr.com> | 2022-04-02 16:46:58 +0300 |
commit | ec9962f7e7b1fa275799f34abfb23535e4fce70b (patch) | |
tree | c9bed5aa07b69786c568a986c94b9b8035341510 | |
parent | 9834fbf5fc4b377710eee61897559c8137282b57 (diff) |
Adapter selection, bugfixes
-rw-r--r-- | custom_components/skykettle/__init__.py | 1 | ||||
-rw-r--r-- | custom_components/skykettle/ble_scan.py | 44 | ||||
-rw-r--r-- | custom_components/skykettle/config_flow.py | 89 | ||||
-rw-r--r-- | custom_components/skykettle/kettle_connection.py | 7 | ||||
-rw-r--r-- | custom_components/skykettle/translations/en.json | 9 | ||||
-rw-r--r-- | custom_components/skykettle/translations/ru.json | 13 | ||||
-rw-r--r-- | custom_components/skykettle/water_heater.py | 2 |
7 files changed, 107 insertions, 58 deletions
diff --git a/custom_components/skykettle/__init__.py b/custom_components/skykettle/__init__.py index 2f2366c..0f937d4 100644 --- a/custom_components/skykettle/__init__.py +++ b/custom_components/skykettle/__init__.py @@ -32,6 +32,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): mac=entry.data[CONF_MAC], key=entry.data[CONF_PASSWORD], persistent=entry.data[CONF_PERSISTENT_CONNECTION], + adapter=entry.data.get(CONF_DEVICE, None), hass=hass ) hass.data[DOMAIN][entry.entry_id][DATA_CONNECTION] = kettle diff --git a/custom_components/skykettle/ble_scan.py b/custom_components/skykettle/ble_scan.py index 3b1b446..7f9a11d 100644 --- a/custom_components/skykettle/ble_scan.py +++ b/custom_components/skykettle/ble_scan.py @@ -1,30 +1,23 @@ import subprocess import asyncio +import shlex import re -from threading import Thread from collections import namedtuple REGEX_MAC = r"^(([0-9a-fA-F]){2}[:-]?){5}[0-9a-fA-F]{2}$" BleDevice = namedtuple("BleDevice", ["mac", "name"]) +BleAdapter = namedtuple("BleAdapter", ["name", "mac"]) -class ScanThread(Thread): - def __init__(self, scan_time, *args, **kwargs): - super().__init__(*args, **kwargs) - self.scan_time = int(scan_time) - self.result = None - - def run(self): - proc = subprocess.Popen(["timeout", "-s", "INT", f"{self.scan_time}s", "hcitool", "lescan"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=False) - self.result = proc.communicate() - - -async def ble_scan(scan_time=3): - scan_thread = ScanThread(scan_time=scan_time, daemon=True) - scan_thread.start() - while not scan_thread.result: await asyncio.sleep(0.1) - stdout, stderr = scan_thread.result +async def ble_scan(device, scan_time=3): + devopt = "" + if device: devopt = f"-i {shlex.quote(device)}" + proc = await asyncio.create_subprocess_shell( + f"timeout -s INT {int(scan_time)}s hcitool {devopt} lescan", + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE) + stdout, stderr = await proc.communicate() out_lines = stdout.decode('utf-8').split('\n') err = stderr.decode('utf-8') @@ -45,3 +38,20 @@ async def ble_scan(scan_time=3): raise Exception(err) return res + +async def ble_get_adapters(): + proc = await asyncio.create_subprocess_shell( + "hcitool dev", + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE) + stdout, stderr = await proc.communicate() + out_lines = stdout.decode('utf-8').split('\n') + err = stderr.decode('utf-8') + if err: raise Exception(err) + + devices = [] + for line in out_lines: + cols = line.split() + if len(cols) >= 2 and re.match(REGEX_MAC, cols[1]): + devices.append(BleAdapter(cols[0], cols[1])) + return devices diff --git a/custom_components/skykettle/config_flow.py b/custom_components/skykettle/config_flow.py index df9ffe2..9721a47 100644 --- a/custom_components/skykettle/config_flow.py +++ b/custom_components/skykettle/config_flow.py @@ -12,6 +12,7 @@ from homeassistant import config_entries from homeassistant.core import callback from .const import * from .ble_scan import ble_scan +from .ble_scan import ble_get_adapters from .kettle_connection import KettleConnection _LOGGER = logging.getLogger(__name__) @@ -32,7 +33,6 @@ class SkyKettleConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Initialize a new SkyKettleConfigFlow.""" self.entry = entry self.config = {} if not entry else dict(entry.data.items()) - # _LOGGER.debug(f"initial config: {self.config}") async def init_mac(self, mac): mac = mac.upper() @@ -49,14 +49,9 @@ class SkyKettleConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user(self, user_input=None): """Handle the user step.""" - - if user_input is not None: - return await self.async_step_scan() - # Check OS if sys.platform != "linux": return self.async_abort(reason='linux_not_found') - # Test binaries try: subprocess.Popen(["timeout"], shell=False).kill() @@ -73,19 +68,52 @@ class SkyKettleConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): except FileNotFoundError: _LOGGER.error(traceback.format_exc()) return self.async_abort(reason='hcitool_not_found') - + return await self.async_step_select_adapter() + + async def async_step_select_adapter(self, user_input=None): + """Handle the select_adapter step.""" + errors = {} + if user_input is not None: + spl = user_input[CONF_DEVICE].split(' ', maxsplit=1) + name = None + if spl[0] != "auto": name = spl[0] + self.config[CONF_DEVICE] = name + # Continue to scan + return await self.async_step_scan_message() + + try: + adapters = await ble_get_adapters() + _LOGGER.debug(f"Adapters: {adapters}") + adapters_list = [f"{r.name} ({r.mac})" for r in adapters] + adapters_list = ["auto"] + adapters_list # Auto + schema = vol.Schema( + { + vol.Required(CONF_DEVICE): vol.In(adapters_list) + }) + except Exception: + _LOGGER.error(traceback.format_exc()) + return self.async_abort(reason='unknown') + return self.async_show_form( + step_id="select_adapter", + errors=errors, + data_schema=schema + ) + + async def async_step_scan_message(self, user_input=None): + """Handle the scan_message step.""" + if user_input is not None: + return await self.async_step_scan() return self.async_show_form( - step_id="user", + step_id="scan_message", data_schema=vol.Schema({}) ) async def async_step_scan(self, user_input=None): """Handle the scan step.""" - errors = {} if user_input is not None: if user_input[CONF_MAC] == "...": return await self.async_step_manual_mac() - spl = user_input[CONF_MAC].split(' ', maxsplit=2) + spl = user_input[CONF_MAC].split(' ', maxsplit=1) mac = spl[0] name = spl[1][1:-2] if len(spl) >= 2 else None if not await self.init_mac(mac): return self.async_abort(reason='already_configured') @@ -94,7 +122,7 @@ class SkyKettleConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return await self.async_step_connect() try: - macs = await ble_scan(scan_time=BLE_SCAN_TIME) + macs = await ble_scan(self.config.get(CONF_DEVICE, None), scan_time=BLE_SCAN_TIME) _LOGGER.debug(f"Scan result: {macs}") macs_filtered = [mac for mac in macs if mac.name and mac.name.startswith("RK-")] if len(macs_filtered) > 0: @@ -149,26 +177,29 @@ class SkyKettleConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_connect(self, user_input=None): """Handle the connect step.""" errors = {} - kettle = KettleConnection( - mac=self.config[CONF_MAC], - key=self.config[CONF_PASSWORD], - persistent=True, - hass=self.hass - ) - tries = 3 - while tries > 0 and not kettle._last_connect_ok: - await kettle.update() - tries = tries - 1 + if user_input is not None: + kettle = KettleConnection( + mac=self.config[CONF_MAC], + key=self.config[CONF_PASSWORD], + persistent=True, + adapter=self.config.get(CONF_DEVICE, None), + hass=self.hass + ) + tries = 3 + while tries > 0 and not kettle._last_connect_ok: + await kettle.update() + tries = tries - 1 - connect_ok = kettle._last_connect_ok - auth_ok = kettle._last_auth_ok + connect_ok = kettle._last_connect_ok + auth_ok = kettle._last_auth_ok + kettle.stop() - if not connect_ok: - errors["base"] = "cant_connect" - elif auth_ok: - return await self.async_step_init() - elif user_input != None: - errors["base"] = "cant_auth" + if not connect_ok: + errors["base"] = "cant_connect" + elif not auth_ok: + errors["base"] = "cant_auth" + else: + return await self.async_step_init() return self.async_show_form( step_id="connect", diff --git a/custom_components/skykettle/kettle_connection.py b/custom_components/skykettle/kettle_connection.py index 235fb55..6e0c3b7 100644 --- a/custom_components/skykettle/kettle_connection.py +++ b/custom_components/skykettle/kettle_connection.py @@ -22,13 +22,14 @@ class KettleConnection(SkyKettle): STATS_INTERVAL = 15 TARGET_TTL = 30 - def __init__(self, mac, key, persistent=True, hass=None): + def __init__(self, mac, key, persistent=True, adapter=None, hass=None): super().__init__() self._child = None self._mac = mac self._key = key - self.hass = hass self.persistent = persistent + self.adapter = adapter + self.hass = hass self._connected = False self._auth_ok = False self._iter = 0 @@ -96,7 +97,7 @@ class KettleConnection(SkyKettle): if self._connected and self._child and self._child.isalive(): return if not self._child or not self._child.isalive(): _LOGGER.debug("Starting \"gatttool\"...") - self._child = await self.hass.async_add_executor_job(pexpect.spawn, "gatttool", ['-I', '-t', 'random', '-b', self._mac], KettleConnection.BLE_TIMEOUT) + self._child = await self.hass.async_add_executor_job(pexpect.spawn, "gatttool", (['-i', self.adapter] if self.adapter else []) + ['-I', '-t', 'random', '-b', self._mac], KettleConnection.BLE_TIMEOUT) await self._child.expect(r"\[LE\]> ", async_=True) _LOGGER.debug("\"gatttool\" started") await self._sendline(f"connect") diff --git a/custom_components/skykettle/translations/en.json b/custom_components/skykettle/translations/en.json index 772d0e7..57f8c3f 100644 --- a/custom_components/skykettle/translations/en.json +++ b/custom_components/skykettle/translations/en.json @@ -17,19 +17,22 @@ "cant_auth": "Connected to the kettle but pairing failed. Please try again." }, "step": { - "user": { + "select_adapter" { + "title": "Select Bluetooth Adapter" + }, + "scan_message": { "title": "Automatic discovery of the kettle", "description": "Make sure that the Kettle is on the stand and it's plugged into the outlet then proceed." }, "scan": { - "title": "SkyKettle MAC Address", + "title": "MAC Address", "description": "Select your kettle.", "data": { "mac": "MAC address of the kettle" } }, "manual_mac": { - "title": "SkyKettle MAC Address", + "title": "MAC Address", "description": "Enter MAC address of your kettle.", "data": { "mac": "MAC address of the kettle" diff --git a/custom_components/skykettle/translations/ru.json b/custom_components/skykettle/translations/ru.json index ba348f5..835c4ad 100644 --- a/custom_components/skykettle/translations/ru.json +++ b/custom_components/skykettle/translations/ru.json @@ -17,22 +17,25 @@ "cant_auth": "Подключились к чайнику, но сопряжение не удалось. Попробуйте снова." }, "step": { - "user": { + "select_adapter" { + "title": "Выберите bluetooth адаптер" + }, + "scan_message": { "title": "Автоматическое обнаружение чайника", "description": "Пожалуйста, убедитесь, что чайник стоит на подставке и включен в розетку." }, "scan": { - "title": "SkyKettle MAC-адрес", + "title": "MAC адрес", "description": "Выберите ваш чайник.", "data": { - "mac": "MAC-адрес чайника" + "mac": "MAC адрес чайника" } }, "manual_mac": { - "title": "SkyKettle MAC-адрес", + "title": "MAC адрес", "description": "Введите MAC-адрес вашего чайника.", "data": { - "mac": "MAC-адрес чайника" + "mac": "MAC адрес чайника" } }, "connect": { diff --git a/custom_components/skykettle/water_heater.py b/custom_components/skykettle/water_heater.py index 90f0d21..966db86 100644 --- a/custom_components/skykettle/water_heater.py +++ b/custom_components/skykettle/water_heater.py @@ -113,7 +113,7 @@ class SkyWaterHeater(WaterHeaterEntity): "success_rate": self.kettle.success_rate, "persistent_connection": self.kettle.persistent, "poll_interval": self.entry.data.get(CONF_SCAN_INTERVAL, 0), - "ontime_seconds": self.kettle.ontime.total_seconds(), + "ontime_seconds": self.kettle.ontime.total_seconds() if self.kettle.ontime else None, "ontime_string": str(self.kettle.ontime), "energy_wh": self.kettle.energy_wh, "heater_on_count": self.kettle.heater_on_count, |