Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Battery fixes #911

Merged
merged 21 commits into from
Feb 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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