Skip to content

Commit

Permalink
Battery fixes (#911)
Browse files Browse the repository at this point in the history
* Rename battery low entity id

* Tidy rounding

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* Docs

* Lint fixes
  • Loading branch information
andrew-codechimp authored Feb 7, 2024
1 parent 3f56ca1 commit ed95c88
Show file tree
Hide file tree
Showing 18 changed files with 277 additions and 213 deletions.
206 changes: 42 additions & 164 deletions custom_components/battery_notes/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,13 @@
BinarySensorEntityDescription,
BinarySensorDeviceClass,
)

from homeassistant.helpers.update_coordinator import (
CoordinatorEntity,
)
from homeassistant.helpers.entity import DeviceInfo, EntityCategory
from homeassistant.helpers.event import (
EventStateChangedData,
async_track_state_change_event,
async_track_entity_registry_updated_event,
)
from homeassistant.helpers.typing import EventType
from homeassistant.helpers.reload import async_setup_reload_service

from homeassistant.const import (
Expand All @@ -42,27 +41,12 @@

from .const import (
DOMAIN,
DOMAIN_CONFIG,
DATA,
CONF_ENABLE_REPLACED,
CONF_ROUND_BATTERY,
CONF_BATTERY_INCREASE_THRESHOLD,
EVENT_BATTERY_THRESHOLD,
EVENT_BATTERY_INCREASED,
DEFAULT_BATTERY_INCREASE_THRESHOLD,
ATTR_DEVICE_ID,
ATTR_BATTERY_QUANTITY,
ATTR_BATTERY_TYPE,
ATTR_BATTERY_TYPE_AND_QUANTITY,
ATTR_BATTERY_LOW,
ATTR_BATTERY_LOW_THRESHOLD,
ATTR_DEVICE_NAME,
ATTR_BATTERY_LEVEL,
ATTR_PREVIOUS_BATTERY_LEVEL,
)

from .common import isfloat
from .device import BatteryNotesDevice

from .coordinator import BatteryNotesCoordinator

from .entity import (
Expand Down Expand Up @@ -147,21 +131,12 @@ async def async_registry_updated(event: Event) -> None:

device_id = async_add_to_device(hass, config_entry)

enable_replaced = True
round_battery = False

if DOMAIN_CONFIG in hass.data[DOMAIN]:
domain_config: dict = hass.data[DOMAIN][DOMAIN_CONFIG]
enable_replaced = domain_config.get(CONF_ENABLE_REPLACED, True)
round_battery = domain_config.get(CONF_ROUND_BATTERY, False)

description = BatteryNotesBinarySensorEntityDescription(
unique_id_suffix="_battery_low",
key="battery_low",
key="_battery_plus_low",
translation_key="battery_low",
icon="mdi:battery-alert",
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=enable_replaced,
device_class=BinarySensorDeviceClass.BATTERY,
)

Expand All @@ -175,8 +150,6 @@ async def async_registry_updated(event: Event) -> None:
coordinator,
description,
f"{config_entry.entry_id}{description.unique_id_suffix}",
device,
round_battery,
)
]
)
Expand All @@ -190,35 +163,28 @@ async def async_setup_platform(
await async_setup_reload_service(hass, DOMAIN, PLATFORMS)


class BatteryNotesBatteryLowSensor(BinarySensorEntity):
class BatteryNotesBatteryLowSensor(BinarySensorEntity, CoordinatorEntity[BatteryNotesCoordinator]):
"""Represents a low battery threshold binary sensor."""

_attr_should_poll = False
_battery_entity_id = None
device_name = None
_previous_battery_low = None
_previous_battery_level = None
_previous_state_last_changed = None

entity_description: BatteryNotesBinarySensorEntityDescription

def __init__(
self,
hass: HomeAssistant,
coordinator: BatteryNotesCoordinator,
description: BatteryNotesBinarySensorEntityDescription,
unique_id: str,
device: BatteryNotesDevice,
round_battery: bool,
) -> None:
"""Create a low battery binary sensor."""

device_registry = dr.async_get(hass)

self.coordinator = coordinator
self.entity_description = description
self._attr_unique_id = unique_id
self._attr_has_entity_name = True
self.round_battery = round_battery

super().__init__(coordinator=coordinator)

if coordinator.device_id and (
device_entry := device_registry.async_get(coordinator.device_id)
Expand All @@ -228,141 +194,53 @@ def __init__(
identifiers=device_entry.identifiers,
)

self.entity_id = f"binary_sensor.{device.name.lower()}_{description.key}"
self.device_name = device.name

self._battery_entity_id = (
device.wrapped_battery.entity_id if device.wrapped_battery else None
)

@callback
async def async_state_changed_listener(
self, event: EventType[EventStateChangedData] | None = None
) -> None:
# pylint: disable=unused-argument
"""Handle child updates."""

if not self._battery_entity_id:
return

if (
wrapped_battery_state := self.hass.states.get(self._battery_entity_id)
) is None or wrapped_battery_state.state in [STATE_UNAVAILABLE, STATE_UNKNOWN]:
self._attr_is_on = False
self._attr_available = True
return

battery_low = bool(
float(wrapped_battery_state.state) < self.coordinator.battery_low_threshold
)

self.coordinator.set_battery_low(battery_low)

self._attr_is_on = self.coordinator.battery_low

self._attr_available = True

self.async_write_ha_state()

_LOGGER.debug(
"%s battery_low changed: %s", self._battery_entity_id, battery_low
)

await self.coordinator.async_request_refresh()

if isfloat(wrapped_battery_state.state):
if self.round_battery:
battery_level = round(float(wrapped_battery_state.state), 0)
else:
battery_level = round(float(wrapped_battery_state.state), 1)
else:
battery_level = wrapped_battery_state.state

if self._previous_state_last_changed:
# Battery low event
if battery_low != self._previous_battery_low:
self.hass.bus.fire(
EVENT_BATTERY_THRESHOLD,
{
ATTR_DEVICE_ID: self.coordinator.device_id,
ATTR_DEVICE_NAME: self.device_name,
ATTR_BATTERY_LOW: battery_low,
ATTR_BATTERY_TYPE_AND_QUANTITY: self.coordinator.battery_type_and_quantity,
ATTR_BATTERY_TYPE: self.coordinator.battery_type,
ATTR_BATTERY_QUANTITY: self.coordinator.battery_quantity,
ATTR_BATTERY_LEVEL: battery_level,
ATTR_PREVIOUS_BATTERY_LEVEL: self._previous_battery_level,
},
)

_LOGGER.debug("battery_threshold event fired Low: %s", battery_low)

# Battery increased event
increase_threshold = DEFAULT_BATTERY_INCREASE_THRESHOLD
if DOMAIN_CONFIG in self.hass.data[DOMAIN]:
domain_config: dict = self.hass.data[DOMAIN][DOMAIN_CONFIG]
increase_threshold = domain_config.get(
CONF_BATTERY_INCREASE_THRESHOLD, DEFAULT_BATTERY_INCREASE_THRESHOLD
)

if wrapped_battery_state.state not in [STATE_UNAVAILABLE, STATE_UNKNOWN]:
if (
wrapped_battery_state.state
and self._previous_battery_level
and float(wrapped_battery_state.state)
>= (self._previous_battery_level + increase_threshold)
):
self.hass.bus.fire(
EVENT_BATTERY_INCREASED,
{
ATTR_DEVICE_ID: self.coordinator.device_id,
ATTR_DEVICE_NAME: self.device_name,
ATTR_BATTERY_LOW: battery_low,
ATTR_BATTERY_TYPE_AND_QUANTITY: self.coordinator.battery_type_and_quantity,
ATTR_BATTERY_TYPE: self.coordinator.battery_type,
ATTR_BATTERY_QUANTITY: self.coordinator.battery_quantity,
ATTR_BATTERY_LEVEL: battery_level,
ATTR_PREVIOUS_BATTERY_LEVEL: self._previous_battery_level,
},
)

_LOGGER.debug("battery_increased event fired")

self._previous_battery_level = battery_level
self._previous_state_last_changed = wrapped_battery_state.last_changed
self._previous_battery_low = battery_low
self.entity_id = f"binary_sensor.{coordinator.device_name.lower()}_{description.key}"

async def async_added_to_hass(self) -> None:
"""Handle added to Hass."""

@callback
async def _async_state_changed_listener(
event: EventType[EventStateChangedData] | None = None,
) -> None:
"""Handle child updates."""
await self.async_state_changed_listener(event)

if self._battery_entity_id:
self.async_on_remove(
async_track_state_change_event(
self.hass, [self._battery_entity_id], _async_state_changed_listener
)
)

# Call once on adding
await _async_state_changed_listener()
await super().async_added_to_hass()

# Update entity options
registry = er.async_get(self.hass)
if registry.async_get(self.entity_id) is not None and self._battery_entity_id:
if registry.async_get(self.entity_id) is not None and self.coordinator.wrapped_battery.entity_id:
registry.async_update_entity_options(
self.entity_id,
DOMAIN,
{"entity_id": self._battery_entity_id},
{"entity_id": self.coordinator.wrapped_battery.entity_id},
)

await self.coordinator.async_config_entry_first_refresh()

@callback
def _handle_coordinator_update(self) -> None:
"""Handle updated data from the coordinator."""

if (
(
wrapped_battery_state := self.hass.states.get(
self.coordinator.wrapped_battery.entity_id
)
)
is None
or wrapped_battery_state.state
in [
STATE_UNAVAILABLE,
STATE_UNKNOWN,
]
or not isfloat(wrapped_battery_state.state)
):
self._attr_is_on = None
self._attr_available = False
self.async_write_ha_state()
return

self._attr_is_on = self.coordinator.battery_low

self.async_write_ha_state()

_LOGGER.debug("%s binary sensor battery_low set to: %s", self.coordinator.wrapped_battery.entity_id, self.coordinator.battery_low)

@property
def extra_state_attributes(self) -> dict[str, str] | None:
"""Return the state attributes of battery low."""
Expand Down
5 changes: 4 additions & 1 deletion custom_components/battery_notes/button.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,9 @@ def __init__(
device_id: str,
) -> None:
"""Create a battery replaced button."""

super().__init__()

device_registry = dr.async_get(hass)

self.coordinator = coordinator
Expand All @@ -188,7 +191,7 @@ def __init__(
identifiers=device.identifiers,
)

self.entity_id = f"button.{device.name.lower()}_{description.key}"
self.entity_id = f"button.{coordinator.device_name.lower()}_{description.key}"

async def async_added_to_hass(self) -> None:
"""Handle added to Hass."""
Expand Down
12 changes: 7 additions & 5 deletions custom_components/battery_notes/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@

def isfloat(num):
"""Is the value a float."""
try:
float(num)
return True
except ValueError:
return False
if num:
try:
float(num)
return True
except ValueError:
return False
return False
2 changes: 2 additions & 0 deletions custom_components/battery_notes/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@
ATTR_BATTERY_LOW_THRESHOLD = "battery_low_threshold"
ATTR_DEVICE_NAME = "device_name"
ATTR_BATTERY_LEVEL = "battery_level"
ATTR_BATTERY_LAST_REPORTED = "battery_last_reported"
ATTR_BATTERY_LAST_REPORTED_LEVEL = "battery_last_reported_level"
ATTR_PREVIOUS_BATTERY_LEVEL = "previous_battery_level"

SERVICE_BATTERY_REPLACED_SCHEMA = vol.Schema(
Expand Down
Loading

0 comments on commit ed95c88

Please sign in to comment.