Skip to content

Commit 770a4e7

Browse files
Auto Library Update (#120)
* WIP * WIP * Change library update to proper values * Version bump * Lint fixes * Check library version * Check version in library * Update readme * Update faq's
1 parent 8de4d80 commit 770a4e7

File tree

10 files changed

+228
-4
lines changed

10 files changed

+228
-4
lines changed

README.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,21 @@ In the HA UI go to "Configuration" -> "Integrations" click "+" and search for "B
6262
On the "Configuration" -> "Integrations" -> "Battery Notes" screen add a new device, pick your device with a battery and add the battery type.
6363
The battery type will then be displayed as a diagnostic sensor on the device.
6464

65+
## FAQ's
66+
67+
* When is the library updated?
68+
It updates when Home Assistant is restarted and approximately every 24 hours after that.
69+
It will pull the latest devices that have been merged into the main branch, if you have recently submitted a pull request for a new device it will not appear until it has been manually reviewed and merged.
70+
71+
* How do I remove a battery note on a device?
72+
Go into the Settings -> Integrations -> Battery Notes, use the menu on the right of a device and select Delete, this will only delete the battery note, not the whole device.
73+
74+
* Why does the device icon change?
75+
Unfortunately where there are multiple integrations associated with a device Home Assistant seems to choose an icon at random, I have no control over this.
76+
77+
* Can I edit a battery note?
78+
Go into Settings -> Integrations -> Battery Notes and click Configure on the device you want to edit.
79+
6580
## Automatic discovery
6681

6782
Battery Notes will automatically discover devices (as long as you have followed the installation instructions above) that it has in its library and create a notification to add a battery note.

custom_components/battery_notes/__init__.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,23 @@
1414
from homeassistant.config_entries import ConfigEntry
1515
from homeassistant.core import HomeAssistant, callback
1616
from homeassistant.const import __version__ as HA_VERSION # noqa: N812
17+
from homeassistant.helpers.aiohttp_client import async_get_clientsession
1718

1819
from homeassistant.helpers.typing import ConfigType
1920

2021
from .discovery import DiscoveryManager
22+
from .library_coordinator import BatteryNotesLibraryUpdateCoordinator
23+
from .library_updater import (
24+
LibraryUpdaterClient,
25+
)
2126

2227
from .const import (
2328
DOMAIN,
2429
DOMAIN_CONFIG,
2530
PLATFORMS,
2631
CONF_ENABLE_AUTODISCOVERY,
2732
CONF_LIBRARY,
33+
DATA_UPDATE_COORDINATOR,
2834
)
2935

3036
MIN_HA_VERSION = "2023.7"
@@ -66,6 +72,15 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
6672
DOMAIN_CONFIG: domain_config,
6773
}
6874

75+
coordinator = BatteryNotesLibraryUpdateCoordinator(
76+
hass=hass,
77+
client=LibraryUpdaterClient(session=async_get_clientsession(hass)),
78+
)
79+
80+
hass.data[DOMAIN][DATA_UPDATE_COORDINATOR] = coordinator
81+
82+
await coordinator.async_refresh()
83+
6984
if domain_config.get(CONF_ENABLE_AUTODISCOVERY):
7085
discovery_manager = DiscoveryManager(hass, config)
7186
await discovery_manager.start_discovery()

custom_components/battery_notes/config_flow.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
CONF_DEVICE_NAME,
3030
CONF_MANUFACTURER,
3131
CONF_MODEL,
32+
DATA_UPDATE_COORDINATOR,
3233
)
3334

3435
_LOGGER = logging.getLogger(__name__)
@@ -84,6 +85,10 @@ async def async_step_user(
8485

8586
device_id = user_input[CONF_DEVICE_ID]
8687

88+
coordinator = self.hass.data[DOMAIN][DATA_UPDATE_COORDINATOR]
89+
90+
await coordinator.async_refresh()
91+
8792
device_registry = dr.async_get(self.hass)
8893
device_entry = device_registry.async_get(device_id)
8994

custom_components/battery_notes/const.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,14 @@
2727
CONF_MODEL = "model"
2828
CONF_MANUFACTURER = "manufacturer"
2929
CONF_DEVICE_NAME = "device_name"
30+
CONF_LIBRARY_URL = "https://raw.githubusercontent.com/andrew-codechimp/HA-Battery-Notes/main/custom_components/battery_notes/data/library.json" # pylint: disable=line-too-long
3031

3132
DATA_CONFIGURED_ENTITIES = "configured_entities"
3233
DATA_DISCOVERED_ENTITIES = "discovered_entities"
3334
DATA_DOMAIN_ENTITIES = "domain_entities"
3435
DATA_LIBRARY = "library"
36+
DATA_UPDATE_COORDINATOR = "update_coordinator"
37+
DATA_LIBRARY_LAST_UPDATE = "library_last_update"
3538

3639
PLATFORMS: Final = [
3740
Platform.SENSOR,

custom_components/battery_notes/diagnostics.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from homeassistant.config_entries import ConfigEntry
44

5+
56
async def async_get_config_entry_diagnostics(
67
entry: ConfigEntry,
78
) -> dict:

custom_components/battery_notes/entity.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
from homeassistant.helpers.entity import EntityDescription
77

8+
89
@dataclass
910
class BatteryNotesRequiredKeysMixin:
1011
"""Mixin for required keys."""
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
"""DataUpdateCoordinator for battery notes library."""
2+
from __future__ import annotations
3+
4+
from datetime import datetime, timedelta
5+
import logging
6+
import json
7+
import os
8+
9+
from homeassistant.core import HomeAssistant
10+
from homeassistant.exceptions import ConfigEntryNotReady
11+
from homeassistant.helpers.update_coordinator import (
12+
DataUpdateCoordinator,
13+
UpdateFailed,
14+
)
15+
16+
from .library_updater import (
17+
LibraryUpdaterClient,
18+
LibraryUpdaterClientError,
19+
)
20+
21+
from .const import (
22+
DOMAIN,
23+
LOGGER,
24+
DATA_LIBRARY_LAST_UPDATE,
25+
)
26+
27+
_LOGGER = logging.getLogger(__name__)
28+
29+
BUILT_IN_DATA_DIRECTORY = os.path.join(os.path.dirname(__file__), "data")
30+
31+
32+
class BatteryNotesLibraryUpdateCoordinator(DataUpdateCoordinator):
33+
"""Class to manage fetching the library from GitHub."""
34+
35+
def __init__(
36+
self,
37+
hass: HomeAssistant,
38+
client: LibraryUpdaterClient,
39+
) -> None:
40+
"""Initialize."""
41+
self.client = client
42+
super().__init__(
43+
hass=hass,
44+
logger=LOGGER,
45+
name=DOMAIN,
46+
update_interval=timedelta(hours=24),
47+
)
48+
49+
async def _async_update_data(self):
50+
"""Update data via library."""
51+
52+
if await self.time_to_update_library() is False:
53+
return
54+
55+
try:
56+
_LOGGER.debug("Getting library updates")
57+
58+
content = await self.client.async_get_data()
59+
60+
if validate_json(content):
61+
json_path = os.path.join(
62+
BUILT_IN_DATA_DIRECTORY,
63+
"library.json",
64+
)
65+
66+
f = open(json_path, mode="w", encoding="utf-8")
67+
f.write(content)
68+
69+
self.hass.data[DOMAIN][DATA_LIBRARY_LAST_UPDATE] = datetime.now()
70+
71+
_LOGGER.debug("Updated library")
72+
else:
73+
_LOGGER.error("Library file is invalid, not updated")
74+
75+
except LibraryUpdaterClientError as exception:
76+
raise UpdateFailed(exception) from exception
77+
78+
async def time_to_update_library(self) -> bool:
79+
"""Check when last updated and if OK to do a new library update."""
80+
try:
81+
if DATA_LIBRARY_LAST_UPDATE in self.hass.data[DOMAIN]:
82+
time_since_last_update = (
83+
datetime.now() - self.hass.data[DOMAIN][DATA_LIBRARY_LAST_UPDATE]
84+
)
85+
86+
time_difference_in_hours = time_since_last_update / timedelta(hours=1)
87+
88+
if time_difference_in_hours < 23:
89+
_LOGGER.debug("Skipping library updates")
90+
return False
91+
return True
92+
except ConfigEntryNotReady:
93+
# Ignore as we are initial load
94+
return True
95+
96+
97+
def validate_json(content: str) -> bool:
98+
"""Check if content is valid json."""
99+
try:
100+
library = json.loads(content)
101+
102+
if "version" not in library:
103+
return False
104+
105+
if library["version"] > 1:
106+
return False
107+
except ValueError:
108+
return False
109+
return True
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
"""Sample API Client."""
2+
from __future__ import annotations
3+
4+
import asyncio
5+
import socket
6+
7+
import aiohttp
8+
import async_timeout
9+
10+
from .const import CONF_LIBRARY_URL
11+
12+
13+
class LibraryUpdaterClientError(Exception):
14+
"""Exception to indicate a general API error."""
15+
16+
17+
class LibraryUpdaterClientCommunicationError(LibraryUpdaterClientError):
18+
"""Exception to indicate a communication error."""
19+
20+
21+
class LibraryUpdaterClient:
22+
"""Library downloader."""
23+
24+
def __init__(
25+
self,
26+
session: aiohttp.ClientSession,
27+
) -> None:
28+
"""Client to get latest library file from GitHub."""
29+
self._session = session
30+
31+
async def async_get_data(self) -> any:
32+
"""Get data from the API."""
33+
return await self._api_wrapper(method="get", url=CONF_LIBRARY_URL)
34+
35+
async def _api_wrapper(
36+
self,
37+
method: str,
38+
url: str,
39+
) -> any:
40+
"""Get information from the API."""
41+
try:
42+
async with async_timeout.timeout(10):
43+
response = await self._session.request(
44+
method=method,
45+
url=url,
46+
allow_redirects=True,
47+
)
48+
# response.raise_for_status()
49+
return await response.text()
50+
51+
except asyncio.TimeoutError as exception:
52+
raise LibraryUpdaterClientCommunicationError(
53+
"Timeout error fetching information",
54+
) from exception
55+
except (aiohttp.ClientError, socket.gaierror) as exception:
56+
raise LibraryUpdaterClientCommunicationError(
57+
"Error fetching information",
58+
) from exception
59+
except Exception as exception: # pylint: disable=broad-except
60+
raise LibraryUpdaterClientError(
61+
"Something really wrong happened!"
62+
) from exception

custom_components/battery_notes/manifest.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,5 @@
99
"integration_type": "device",
1010
"iot_class": "calculated",
1111
"issue_tracker": "https://github.com/andrew-codechimp/ha-battery-notes/issues",
12-
"version": "1.1.9"
13-
}
12+
"version": "1.2.0"
13+
}

custom_components/battery_notes/sensor.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@
2424
async_track_state_change_event,
2525
async_track_entity_registry_updated_event,
2626
)
27+
from homeassistant.helpers.update_coordinator import (
28+
CoordinatorEntity,
29+
)
2730

2831
from homeassistant.helpers.reload import async_setup_reload_service
2932

@@ -36,8 +39,11 @@
3639
DOMAIN,
3740
PLATFORMS,
3841
CONF_BATTERY_TYPE,
42+
DATA_UPDATE_COORDINATOR,
3943
)
4044

45+
from .library_coordinator import BatteryNotesLibraryUpdateCoordinator
46+
4147
from .entity import (
4248
BatteryNotesEntityDescription,
4349
)
@@ -129,9 +135,12 @@ async def async_registry_updated(event: Event) -> None:
129135

130136
device_id = async_add_to_device(hass, config_entry)
131137

138+
coordinator = hass.data[DOMAIN][DATA_UPDATE_COORDINATOR]
139+
132140
entities = [
133141
BatteryNotesTypeSensor(
134142
hass,
143+
coordinator,
135144
typeSensorEntityDescription,
136145
device_id,
137146
f"{config_entry.entry_id}{typeSensorEntityDescription.unique_id_suffix}",
@@ -149,7 +158,7 @@ async def async_setup_platform(
149158
await async_setup_reload_service(hass, DOMAIN, PLATFORMS)
150159

151160

152-
class BatteryNotesSensor(RestoreSensor, SensorEntity):
161+
class BatteryNotesSensor(RestoreSensor, SensorEntity, CoordinatorEntity):
153162
"""Represents a battery note sensor."""
154163

155164
_attr_should_poll = False
@@ -158,11 +167,14 @@ class BatteryNotesSensor(RestoreSensor, SensorEntity):
158167
def __init__(
159168
self,
160169
hass,
170+
coordinator: BatteryNotesLibraryUpdateCoordinator,
161171
description: BatteryNotesSensorEntityDescription,
162172
device_id: str,
163173
unique_id: str,
164174
) -> None:
165175
"""Initialize the sensor."""
176+
super().__init__(coordinator)
177+
166178
device_registry = dr.async_get(hass)
167179

168180
self.entity_description = description
@@ -219,13 +231,14 @@ class BatteryNotesTypeSensor(BatteryNotesSensor):
219231
def __init__(
220232
self,
221233
hass,
234+
coordinator: BatteryNotesLibraryUpdateCoordinator,
222235
description: BatteryNotesSensorEntityDescription,
223236
device_id: str,
224237
unique_id: str,
225238
battery_type: str | None = None,
226239
) -> None:
227240
"""Initialize the sensor."""
228-
super().__init__(hass, description, device_id, unique_id)
241+
super().__init__(hass, coordinator, description, device_id, unique_id)
229242

230243
self._battery_type = battery_type
231244

0 commit comments

Comments
 (0)