Skip to content

Commit

Permalink
Library (#39)
Browse files Browse the repository at this point in the history
* Add initial library

* Start of a library

* WIP

* A working library lookup on manual add

* Add readme instructions for library contribution

* Updated library contribution details

* Update file location

* Lint issues

* Change quantity to a numeric

* Fix lint issue

* WIP

* Add Acknowledgements

* WIP

* WIP

* WIP

* WIP

* Move library directory

* WIP

* WIP

* Config

* WIP

* Case insensitive matching

* Update library with some more data

* WIP

* Test validator

* More tests

* Testing done

* Conditional json validate

* A working discovery detail

* Docs

* Version bump

* Remove dev library

* Lint fixes

* Error handling on library issues

* Fix Ruff issues

* Fix ruff issues
  • Loading branch information
andrew-codechimp authored Dec 12, 2023
1 parent 2998e5f commit 67e15e2
Show file tree
Hide file tree
Showing 12 changed files with 490 additions and 67 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,4 @@ coverage.xml
config/*
!config/configuration.yaml
.DS_Store
custom_components/battery_notes/data/library-dev.json
custom_components/battery_notes/data/library-dev.json
11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,16 @@ In the HA UI go to "Configuration" -> "Integrations" click "+" and search for "B
On the "Configuration" -> "Integrations" -> "Battery Notes" screen add a new device, pick your device with a battery and add the battery type.
The battery type will then be displayed as a diagnostic sensor on the device.

## Contributing to the Battery Library (COMING SOON!)
## Automatic discovery

By default Battery Notes will automatically discover devices that it has in its library and create a notification to add a battery note.
If you wish to disable this functionality then add the following to your ```configuration.yaml```
```
battery_notes:
enable_autodiscovery: false
```

## Contributing to the Battery Library

The battery library is a JSON document at ```custom_components/battery_notes/data/library.json```
To contribute, fork the repository, add your device details to the JSON document and submit a pull request.
Expand Down
4 changes: 4 additions & 0 deletions config/configuration.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,7 @@ logger:
default: info
logs:
custom_components.battery_notes: debug

battery_notes:
enable_autodiscovery: true
library: "library-dev.json"
50 changes: 49 additions & 1 deletion custom_components/battery_notes/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,55 @@
"""
from __future__ import annotations

import logging

from awesomeversion.awesomeversion import AwesomeVersion
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.const import __version__ as HA_VERSION # noqa: N812

from homeassistant.helpers.typing import ConfigType

from .discovery import DiscoveryManager

from .const import (
DOMAIN,
DOMAIN_CONFIG,
PLATFORMS,
CONF_ENABLE_AUTODISCOVERY,
)

MIN_HA_VERSION = "2023.7"

_LOGGER = logging.getLogger(__name__)


async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Integration setup."""

if AwesomeVersion(HA_VERSION) < AwesomeVersion(MIN_HA_VERSION): # pragma: no cover
msg = (
"This integration requires at least HomeAssistant version "
f" {MIN_HA_VERSION}, you are running version {HA_VERSION}."
" Please upgrade HomeAssistant to continue use this integration."
)
_LOGGER.critical(msg)
return False

domain_config: ConfigType = config.get(DOMAIN) or {
CONF_ENABLE_AUTODISCOVERY: True,
}

hass.data[DOMAIN] = {
DOMAIN_CONFIG: domain_config,
}

if domain_config.get(CONF_ENABLE_AUTODISCOVERY):
discovery_manager = DiscoveryManager(hass, config)
await discovery_manager.start_discovery()

return True

from .const import PLATFORMS

async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up a config entry."""
Expand All @@ -19,15 +64,18 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:

return True


@callback
async def async_update_options(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Update options."""
await hass.config_entries.async_reload(entry.entry_id)


async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)


async def config_entry_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Update listener, called when the config entry options are changed."""
await hass.config_entries.async_reload(entry.entry_id)
143 changes: 100 additions & 43 deletions custom_components/battery_notes/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
from __future__ import annotations

import copy
import logging

from typing import Any

import voluptuous as vol
Expand All @@ -10,82 +12,144 @@
from homeassistant.data_entry_flow import FlowResult
from homeassistant.config_entries import ConfigEntry, OptionsFlow
from homeassistant.helpers import selector
from homeassistant.helpers.typing import DiscoveryInfoType

import homeassistant.helpers.device_registry as dr

from homeassistant.const import CONF_NAME
from homeassistant.const import (
CONF_NAME,
CONF_DEVICE_ID,
)

from .library import Library

from .const import (
DOMAIN,
CONF_BATTERY_TYPE,
CONF_DEVICE_NAME,
CONF_MANUFACTURER,
CONF_MODEL,
)

_LOGGER = logging.getLogger(__name__)

DEVICE_SCHEMA = vol.Schema(
{
vol.Required(CONF_DEVICE_ID): selector.DeviceSelector(
# selector.DeviceSelectorConfig(model="otgw-nodo")
),
vol.Optional(CONF_NAME): selector.TextSelector(
selector.TextSelectorConfig(type=selector.TextSelectorType.TEXT),
),
}
)

from .const import DOMAIN, CONF_DEVICE_ID, CONF_BATTERY_TYPE

class BatteryNotesFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
"""Config flow for BatteryNotes."""

VERSION = 1

data: dict

@staticmethod
@callback
def async_get_options_flow(config_entry: ConfigEntry) -> OptionsFlow:
"""Get the options flow for this handler."""
return OptionsFlowHandler(config_entry)

async def async_step_integration_discovery(
self,
discovery_info: DiscoveryInfoType,
) -> FlowResult:
"""Handle integration discovery."""
_LOGGER.debug("Starting discovery flow: %s", discovery_info)

self.context["title_placeholders"] = {
"name": discovery_info[CONF_DEVICE_NAME],
"manufacturer": discovery_info[CONF_MANUFACTURER],
"model": discovery_info[CONF_MODEL],
}

return await self.async_step_user(discovery_info)

async def async_step_user(
self,
user_input: dict | None = None,
) -> config_entries.FlowResult:
"""Handle a flow initialized by the user."""
_errors = {}
if user_input is not None:
self.data = user_input

device_id = user_input[CONF_DEVICE_ID]

device_registry = dr.async_get(self.hass)
device_entry = (
device_registry.async_get(device_id)
device_entry = device_registry.async_get(device_id)

library = Library(self.hass)

device_battery_details = await library.get_device_battery_details(
device_entry.manufacturer, device_entry.model
)

if device_battery_details:
self.data[
CONF_BATTERY_TYPE
] = device_battery_details.battery_type_and_quantity

return await self.async_step_battery()

return self.async_show_form(
step_id="user",
data_schema=DEVICE_SCHEMA,
errors=_errors,
last_step=False,
)

async def async_step_battery(self, user_input: dict[str, Any] | None = None):
"""Second step in config flow to add the battery type."""
errors: dict[str, str] = {}
if user_input is not None:
self.data[CONF_BATTERY_TYPE] = user_input[CONF_BATTERY_TYPE]

device_id = self.data[CONF_DEVICE_ID]
unique_id = f"bn_{device_id}"

device_registry = dr.async_get(self.hass)
device_entry = device_registry.async_get(device_id)

await self.async_set_unique_id(unique_id)
self._abort_if_unique_id_configured()

if CONF_NAME in user_input:
title = user_input.get(CONF_NAME)
if CONF_NAME in self.data:
title = self.data.get(CONF_NAME)
else:
title = device_entry.name_by_user or device_entry.name

return self.async_create_entry(
title=title,
data=user_input,
data=self.data,
)

return self.async_show_form(
step_id="user",
step_id="battery",
data_schema=vol.Schema(
{
vol.Required(
CONF_DEVICE_ID,
default=(user_input or {}).get(CONF_DEVICE_ID)
): selector.DeviceSelector(
# selector.DeviceSelectorConfig(model="otgw-nodo")
),
vol.Optional(
CONF_NAME
): selector.TextSelector(
selector.TextSelectorConfig(
type=selector.TextSelectorType.TEXT
),
),
vol.Required(
CONF_BATTERY_TYPE,
default=(user_input or {}).get(CONF_BATTERY_TYPE),
default=self.data.get(CONF_BATTERY_TYPE),
): selector.TextSelector(
selector.TextSelectorConfig(
type=selector.TextSelectorType.TEXT
),
),
}
),
errors=_errors,
errors=errors,
)


class OptionsFlowHandler(OptionsFlow):
"""Handle an option flow for BatteryNotes."""

Expand Down Expand Up @@ -124,8 +188,8 @@ async def save_options(
) -> dict:
"""Save options, and return errors when validation fails."""
device_registry = dr.async_get(self.hass)
device_entry = (
device_registry.async_get(self.config_entry.data.get(CONF_DEVICE_ID))
device_entry = device_registry.async_get(
self.config_entry.data.get(CONF_DEVICE_ID)
)

if CONF_NAME in user_input:
Expand Down Expand Up @@ -157,30 +221,23 @@ def _process_user_input(

def build_options_schema(self) -> vol.Schema:
"""Build the options schema."""
data_schema=vol.Schema(
{
vol.Optional(
CONF_NAME
): selector.TextSelector(
selector.TextSelectorConfig(
type=selector.TextSelectorType.TEXT
),
),
vol.Required(
CONF_BATTERY_TYPE
): selector.TextSelector(
selector.TextSelectorConfig(
type=selector.TextSelectorType.TEXT
),
),
}
)
data_schema = vol.Schema(
{
vol.Optional(CONF_NAME): selector.TextSelector(
selector.TextSelectorConfig(type=selector.TextSelectorType.TEXT),
),
vol.Required(CONF_BATTERY_TYPE): selector.TextSelector(
selector.TextSelectorConfig(type=selector.TextSelectorType.TEXT),
),
}
)

return _fill_schema_defaults(
data_schema,
self.current_config,
)


def _fill_schema_defaults(
data_schema: vol.Schema,
options: dict[str, str],
Expand Down
8 changes: 7 additions & 1 deletion custom_components/battery_notes/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,17 @@

DOMAIN_CONFIG = "config"

CONF_DEVICE_ID = "device_id"
CONF_BATTERY_TYPE = "battery_type"
CONF_SENSORS = "sensors"
CONF_ENABLE_AUTODISCOVERY = "enable_autodiscovery"
CONF_LIBRARY = "library"
CONF_MODEL = "model"
CONF_MANUFACTURER = "manufacturer"
CONF_DEVICE_NAME = "device_name"

DATA_CONFIGURED_ENTITIES = "configured_entities"
DATA_DISCOVERED_ENTITIES = "discovered_entities"
DATA_DOMAIN_ENTITIES = "domain_entities"

PLATFORMS: Final = [
Platform.SENSOR,
Expand Down
Loading

0 comments on commit 67e15e2

Please sign in to comment.