diff --git a/custom_components/skykettle/kettle_connection.py b/custom_components/skykettle/kettle_connection.py index aff76f1..fbb8c7a 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: @@ -90,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) @@ -221,6 +231,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 +502,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..7f9b0cd 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 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,