From 0c26fe7a6f8ac0603c8338154f9ebe6c39d3be3f Mon Sep 17 00:00:00 2001 From: Maksim Kovalyov Date: Mon, 9 Feb 2026 22:41:55 +0800 Subject: [PATCH 1/3] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=20=D0=B4=D0=B0=D1=82=D1=87=D0=B8=D0=BA=20=D1=82=D0=B5?= =?UTF-8?q?=D0=BA=D1=83=D1=89=D0=B5=D0=B9=20=D0=BC=D0=BE=D1=89=D0=BD=D0=BE?= =?UTF-8?q?=D1=81=D1=82=D0=B8=20=D0=B8=20=D1=83=D0=BB=D1=83=D1=87=D1=88?= =?UTF-8?q?=D0=B5=D0=BD=20=D0=B4=D0=B0=D1=82=D1=87=D0=B8=D0=BA=20=D1=8D?= =?UTF-8?q?=D0=BD=D0=B5=D1=80=D0=B3=D0=B8=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Добавлен датчик текущей мощности: вычисляется из прироста энергии между опросами (~15 с), показывает среднюю мощность в ваттах при нагреве - Изменены единицы датчика энергии с Wh на kWh для удобства чтения - Добавлен power_w в extra_state_attributes water_heater для Energy dashboard - Датчик мощности возвращает 0 при отсутствии данных (вместо None) - Доступность датчика мощности привязана к датчику энергии (требуются stats) Co-authored-by: Cursor --- .../skykettle/kettle_connection.py | 26 +++++++++++++++++++ custom_components/skykettle/sensor.py | 24 ++++++++++++++--- custom_components/skykettle/water_heater.py | 1 + 3 files changed, 48 insertions(+), 3 deletions(-) diff --git a/custom_components/skykettle/kettle_connection.py b/custom_components/skykettle/kettle_connection.py index aff76f1..df35c7d 100644 --- a/custom_components/skykettle/kettle_connection.py +++ b/custom_components/skykettle/kettle_connection.py @@ -52,6 +52,9 @@ def __init__(self, mac, key, persistent=True, adapter=None, hass=None, model=Non self._colors = {} self._disposed = False self._last_data = None + self._prev_energy_wh = None + self._prev_energy_timestamp = None + self._power_w = None async def command(self, command, params=[]): if self._disposed: @@ -221,6 +224,25 @@ async def update(self, tries=MAX_TRIES, force_stats=False, extra_action=None, co if self._last_get_stats + KettleConnection.STATS_INTERVAL < monotonic() or force_stats: self._last_get_stats = monotonic() self._stats = await self.get_stats() + # Compute power from energy delta + now = monotonic() + if self._stats and self._stats.energy_wh is not None: + energy_wh = self._stats.energy_wh + if self._prev_energy_wh is not None and self._prev_energy_timestamp is not None: + if energy_wh >= self._prev_energy_wh: + elapsed_hours = (now - self._prev_energy_timestamp) / 3600.0 + if elapsed_hours > 0: + self._power_w = (energy_wh - self._prev_energy_wh) / elapsed_hours + else: + self._power_w = 0.0 + else: + self._power_w = None + else: + self._power_w = None + self._prev_energy_wh = energy_wh + self._prev_energy_timestamp = now + else: + self._power_w = None self._light_switch_boil = await self.get_light_switch(SkyKettle.LIGHT_BOIL) self._light_switch_sync = await self.get_light_switch(SkyKettle.LIGHT_SYNC) self._lamp_auto_off_hours = await self.get_lamp_auto_off_hours() @@ -473,6 +495,10 @@ def energy_wh(self): if not self._stats: return None return self._stats.energy_wh + @property + def power_w(self): + return self._power_w + @property def heater_on_count(self): if not self._stats: return None diff --git a/custom_components/skykettle/sensor.py b/custom_components/skykettle/sensor.py index d0625ca..adb67a5 100644 --- a/custom_components/skykettle/sensor.py +++ b/custom_components/skykettle/sensor.py @@ -4,7 +4,7 @@ from homeassistant.components.sensor import (SensorDeviceClass, SensorEntity, SensorStateClass) from homeassistant.const import (CONF_FRIENDLY_NAME, PERCENTAGE, UnitOfEnergy, - UnitOfTime) + UnitOfPower, UnitOfTime) from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import EntityCategory @@ -17,6 +17,7 @@ SENSOR_TYPE_WATER_FRESHNESS = "water_freshness" SENSOR_TYPE_SUCCESS_RATE = "success_rate" SENSOR_TYPE_ENERGY = "energy" +SENSOR_TYPE_POWER = "power" SENSOR_TYPE_ONTIME = "ontime" SENSOR_TYPE_HEATER_ON_COUNT = "heater_on_count" SENSOR_TYPE_USER_ON_COUNT = "user_on_count" @@ -31,6 +32,7 @@ async def async_setup_entry(hass, entry, async_add_entities): if model_code in [SkyKettle.MODELS_4]: # RK-G2xxS, RK-M13xS, RK-M21xS, RK-M223S but not sure async_add_entities([ SkySensor(hass, entry, SENSOR_TYPE_ENERGY), + SkySensor(hass, entry, SENSOR_TYPE_POWER), SkySensor(hass, entry, SENSOR_TYPE_ONTIME), SkySensor(hass, entry, SENSOR_TYPE_HEATER_ON_COUNT), SkySensor(hass, entry, SENSOR_TYPE_USER_ON_COUNT), @@ -82,6 +84,8 @@ def name(self): """Name of the entity.""" if self.sensor_type == SENSOR_TYPE_ENERGY: return (FRIENDLY_NAME + " " + self.entry.data.get(CONF_FRIENDLY_NAME, "")).strip() + " total energy consumed" + if self.sensor_type == SENSOR_TYPE_POWER: + return (FRIENDLY_NAME + " " + self.entry.data.get(CONF_FRIENDLY_NAME, "")).strip() + " current power" if self.sensor_type == SENSOR_TYPE_ONTIME: return (FRIENDLY_NAME + " " + self.entry.data.get(CONF_FRIENDLY_NAME, "")).strip() + " total work time" if self.sensor_type == SENSOR_TYPE_HEATER_ON_COUNT: @@ -103,6 +107,8 @@ def icon(self): return "mdi:dip-switch" if self.sensor_type == SENSOR_TYPE_WATER_FRESHNESS: return "mdi:water-sync" + if self.sensor_type == SENSOR_TYPE_POWER: + return "mdi:flash" if self.sensor_type == SENSOR_TYPE_SUCCESS_RATE: return "mdi:bluetooth-connect" return None @@ -111,6 +117,8 @@ def icon(self): def available(self): if self.sensor_type == SENSOR_TYPE_ENERGY: return self.kettle.available and self.kettle.energy_wh != None + if self.sensor_type == SENSOR_TYPE_POWER: + return self.kettle.available and self.kettle.energy_wh is not None if self.sensor_type == SENSOR_TYPE_ONTIME: return self.kettle.available and self.kettle.ontime != None if self.sensor_type == SENSOR_TYPE_HEATER_ON_COUNT: @@ -132,6 +140,8 @@ def entity_category(self): def device_class(self): if self.sensor_type == SENSOR_TYPE_ENERGY: return SensorDeviceClass.ENERGY + if self.sensor_type == SENSOR_TYPE_POWER: + return SensorDeviceClass.POWER if self.sensor_type == SENSOR_TYPE_ONTIME: return SensorDeviceClass.DURATION if self.sensor_type == SENSOR_TYPE_WATER_FRESHNESS: @@ -142,6 +152,8 @@ def device_class(self): def state_class(self): if self.sensor_type == SENSOR_TYPE_ENERGY: return SensorStateClass.TOTAL_INCREASING + if self.sensor_type == SENSOR_TYPE_POWER: + return SensorStateClass.MEASUREMENT if self.sensor_type == SENSOR_TYPE_ONTIME: return SensorStateClass.TOTAL_INCREASING if self.sensor_type == SENSOR_TYPE_HEATER_ON_COUNT: @@ -156,7 +168,9 @@ def state_class(self): @property def native_unit_of_measurement(self): if self.sensor_type == SENSOR_TYPE_ENERGY: - return UnitOfEnergy.WATT_HOUR + return UnitOfEnergy.KILO_WATT_HOUR + if self.sensor_type == SENSOR_TYPE_POWER: + return UnitOfPower.WATT if self.sensor_type == SENSOR_TYPE_ONTIME: return UnitOfTime.SECONDS if self.sensor_type == SENSOR_TYPE_WATER_FRESHNESS: @@ -168,7 +182,11 @@ def native_unit_of_measurement(self): @property def native_value(self): if self.sensor_type == SENSOR_TYPE_ENERGY: - return self.kettle.energy_wh + e = self.kettle.energy_wh + return round(e / 1000.0, 2) if e is not None else None + if self.sensor_type == SENSOR_TYPE_POWER: + pw = self.kettle.power_w + return 0 if pw is None else pw if self.sensor_type == SENSOR_TYPE_ONTIME: return self.kettle.ontime.total_seconds() if self.sensor_type == SENSOR_TYPE_HEATER_ON_COUNT: diff --git a/custom_components/skykettle/water_heater.py b/custom_components/skykettle/water_heater.py index b6bf139..7937d94 100644 --- a/custom_components/skykettle/water_heater.py +++ b/custom_components/skykettle/water_heater.py @@ -137,6 +137,7 @@ def extra_state_attributes(self): "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, + "power_w": self.kettle.power_w, "heater_on_count": self.kettle.heater_on_count, "user_on_count": self.kettle.user_on_count, "sound_enabled": self.kettle.sound_enabled, From 73cf7a8c2cfb2b9944c72ced00c12bb887a91e7c Mon Sep 17 00:00:00 2001 From: Maksim Kovalyov Date: Mon, 9 Feb 2026 22:52:45 +0800 Subject: [PATCH 2/3] =?UTF-8?q?=D0=A3=D0=BB=D1=83=D1=87=D1=88=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D0=B5=20BLE-=D0=BF=D0=BE=D0=B4=D0=BA=D0=BB=D1=8E=D1=87?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D1=8F=20=D1=87=D0=B5=D1=80=D0=B5=D0=B7=20ble?= =?UTF-8?q?ak-retry-connector?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Явно указываем connectable=True при получении устройства для активного подключения (по рекомендациям Home Assistant) - Добавлена проверка на None: при отсутствии устройства выбрасывается понятная ошибка вместо передачи None в establish_connection - Добавлен ble_device_callback для обновления устройства при повторных подключениях (актуально при смене пути на Linux) Co-authored-by: Cursor --- custom_components/skykettle/kettle_connection.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/custom_components/skykettle/kettle_connection.py b/custom_components/skykettle/kettle_connection.py index df35c7d..fbb8c7a 100644 --- a/custom_components/skykettle/kettle_connection.py +++ b/custom_components/skykettle/kettle_connection.py @@ -93,13 +93,20 @@ async def _connect(self): if self._disposed: raise DisposedError() if self._client and self._client.is_connected: return - self._device = bluetooth.async_ble_device_from_address(self.hass, self._mac) + self._device = bluetooth.async_ble_device_from_address( + self.hass, self._mac, connectable=True + ) + if not self._device: + raise IOError("Device not found") _LOGGER.debug("Connecting to the Kettle...") self._client = await establish_connection( BleakClientWithServiceCache, self._device, self._device.name or "Unknown Device", - max_attempts=3 + max_attempts=3, + ble_device_callback=lambda: bluetooth.async_ble_device_from_address( + self.hass, self._mac, connectable=True + ), ) _LOGGER.debug("Connected to the Kettle") await self._client.start_notify(KettleConnection.UUID_RX, self._rx_callback) From bf1da15515c18436fdcc921efb9a965273fb60c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9A=D0=BE=D0=B2=D0=B0=D0=BB=D1=91=D0=B2=20=D0=9C=D0=B0?= =?UTF-8?q?=D0=BA=D1=81=D0=B8=D0=BC?= <4065357+kai-zer-ru@users.noreply.github.com> Date: Wed, 18 Feb 2026 19:38:40 +0800 Subject: [PATCH 3/3] Update custom_components/skykettle/sensor.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- custom_components/skykettle/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/skykettle/sensor.py b/custom_components/skykettle/sensor.py index adb67a5..7f9b0cd 100644 --- a/custom_components/skykettle/sensor.py +++ b/custom_components/skykettle/sensor.py @@ -186,7 +186,7 @@ def native_value(self): return round(e / 1000.0, 2) if e is not None else None if self.sensor_type == SENSOR_TYPE_POWER: pw = self.kettle.power_w - return 0 if pw is None else pw + return pw if self.sensor_type == SENSOR_TYPE_ONTIME: return self.kettle.ontime.total_seconds() if self.sensor_type == SENSOR_TYPE_HEATER_ON_COUNT: