Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/ClusterM/skykettle-ha.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAlexey 'Cluster' Avdyukhin <clusterrr@clusterrr.com>2022-04-02 16:46:58 +0300
committerAlexey 'Cluster' Avdyukhin <clusterrr@clusterrr.com>2022-04-02 16:46:58 +0300
commitec9962f7e7b1fa275799f34abfb23535e4fce70b (patch)
treec9bed5aa07b69786c568a986c94b9b8035341510
parent9834fbf5fc4b377710eee61897559c8137282b57 (diff)
Adapter selection, bugfixes
-rw-r--r--custom_components/skykettle/__init__.py1
-rw-r--r--custom_components/skykettle/ble_scan.py44
-rw-r--r--custom_components/skykettle/config_flow.py89
-rw-r--r--custom_components/skykettle/kettle_connection.py7
-rw-r--r--custom_components/skykettle/translations/en.json9
-rw-r--r--custom_components/skykettle/translations/ru.json13
-rw-r--r--custom_components/skykettle/water_heater.py2
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,