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鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add config panel to recorder #62604

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
88 changes: 45 additions & 43 deletions homeassistant/components/recorder/__init__.py
Expand Up @@ -22,6 +22,7 @@
import voluptuous as vol

from homeassistant.components import persistent_notification
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
ATTR_ENTITY_ID,
CONF_EXCLUDE,
Expand Down Expand Up @@ -55,7 +56,13 @@

from . import history, migration, purge, statistics, websocket_api
from .const import (
CONF_AUTO_PURGE,
CONF_COMMIT_INTERVAL,
CONF_DB_INTEGRITY_CHECK,
CONF_DB_MAX_RETRIES,
CONF_DB_RETRY_WAIT,
CONF_DB_URL,
CONF_PURGE_KEEP_DAYS,
DATA_INSTANCE,
DOMAIN,
MAX_QUEUE_BACKLOG,
Expand Down Expand Up @@ -129,14 +136,8 @@
DB_LOCK_TIMEOUT = 30
DB_LOCK_QUEUE_CHECK_TIMEOUT = 1

CONF_AUTO_PURGE = "auto_purge"
CONF_DB_URL = "db_url"
CONF_DB_MAX_RETRIES = "db_max_retries"
CONF_DB_RETRY_WAIT = "db_retry_wait"
CONF_PURGE_KEEP_DAYS = "purge_keep_days"
CONF_PURGE_INTERVAL = "purge_interval"
CONF_EVENT_TYPES = "event_types"
CONF_COMMIT_INTERVAL = "commit_interval"

INVALIDATED_ERR = "Database connection invalidated"
CONNECTIVITY_ERR = "Error in database connectivity during commit"
Expand Down Expand Up @@ -234,35 +235,32 @@ def run_information_with_session(session, point_in_time: datetime | None = None)


async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the recorder."""
entries = hass.config_entries.async_entries(DOMAIN)
if not entries:
hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN, context={"source": "system"}, data=(config[DOMAIN])
)
)
return True


async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
) -> bool:
"""Set up the recorder."""
hass.data[DOMAIN] = {}
conf = config[DOMAIN]
entity_filter = convert_include_exclude_filter(conf)
auto_purge = conf[CONF_AUTO_PURGE]
keep_days = conf[CONF_PURGE_KEEP_DAYS]
commit_interval = conf[CONF_COMMIT_INTERVAL]
db_max_retries = conf[CONF_DB_MAX_RETRIES]
db_retry_wait = conf[CONF_DB_RETRY_WAIT]
db_url = conf.get(CONF_DB_URL) or DEFAULT_URL.format(
hass_config_path=hass.config.path(DEFAULT_DB_FILE)
)
exclude = conf[CONF_EXCLUDE]
exclude = config_entry.data[CONF_EXCLUDE]
exclude_t = exclude.get(CONF_EVENT_TYPES, [])
if EVENT_STATE_CHANGED in exclude_t:
_LOGGER.warning(
"State change events are excluded, recorder will not record state changes."
"This will become an error in Home Assistant Core 2022.2"
)
instance = hass.data[DATA_INSTANCE] = Recorder(
hass=hass,
auto_purge=auto_purge,
keep_days=keep_days,
commit_interval=commit_interval,
uri=db_url,
db_max_retries=db_max_retries,
db_retry_wait=db_retry_wait,
entity_filter=entity_filter,
exclude_t=exclude_t,
hass=hass, config_entry=config_entry, exclude_t=exclude_t
)
instance.async_initialize()
instance.start()
Expand Down Expand Up @@ -484,36 +482,32 @@ class Recorder(threading.Thread):
stop_requested: bool

def __init__(
self,
hass: HomeAssistant,
auto_purge: bool,
keep_days: int,
commit_interval: int,
uri: str,
db_max_retries: int,
db_retry_wait: int,
entity_filter: Callable[[str], bool],
exclude_t: list[str],
self, hass: HomeAssistant, config_entry: ConfigEntry, exclude_t: list[str]
) -> None:
"""Initialize the recorder."""
threading.Thread.__init__(self, name="Recorder")

self.hass = hass
self.auto_purge = auto_purge
self.keep_days = keep_days
self.commit_interval = commit_interval
self.config_entry = config_entry

conf = dict(config_entry.data)
self.auto_purge = conf[CONF_AUTO_PURGE]
self.keep_days = conf[CONF_PURGE_KEEP_DAYS]
self.commit_interval = conf[CONF_COMMIT_INTERVAL]
self.queue: queue.SimpleQueue[RecorderTask] = queue.SimpleQueue()
self.recording_start = dt_util.utcnow()
self.db_url = uri
self.db_max_retries = db_max_retries
self.db_retry_wait = db_retry_wait
self.db_url = conf.get(CONF_DB_URL) or DEFAULT_URL.format(
hass_config_path=hass.config.path(DEFAULT_DB_FILE)
)
self.db_max_retries = conf[CONF_DB_MAX_RETRIES]
self.db_retry_wait = conf[CONF_DB_RETRY_WAIT]
self.async_db_ready: asyncio.Future = asyncio.Future()
self.async_recorder_ready = asyncio.Event()
self._queue_watch = threading.Event()
self.engine: Any = None
self.run_info: Any = None

self.entity_filter = entity_filter
self.entity_filter = convert_include_exclude_filter(conf)
self.exclude_t = exclude_t

self._timechanges_seen = 0
Expand All @@ -533,6 +527,14 @@ def __init__(

self.enabled = True

def update_config(self, config_entry: ConfigEntry):
"""Update config in runtime."""
conf = dict(config_entry.data)
self.auto_purge = conf[CONF_AUTO_PURGE]
self.commit_interval = conf[CONF_COMMIT_INTERVAL]
self.entity_filter = convert_include_exclude_filter(conf)
self.keep_days = conf[CONF_PURGE_KEEP_DAYS]

def set_enable(self, enable):
"""Enable or disable recording events and states."""
self.enabled = enable
Expand Down
31 changes: 31 additions & 0 deletions homeassistant/components/recorder/config_flow.py
@@ -0,0 +1,31 @@
"""Config flow to connect with Home Assistant."""
from __future__ import annotations

import logging
from typing import Any

from homeassistant import config_entries
from homeassistant.data_entry_flow import FlowResult

from .const import DOMAIN


@config_entries.HANDLERS.register(DOMAIN)
class RecorderFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
"""Recorder configuration flow."""

@property
def logger(self) -> logging.Logger:
"""Return logger."""
return logging.getLogger(__name__)

async def async_step_system(self, user_input: dict[str, Any]) -> FlowResult:
"""Import data."""
# Only allow 1 instance.
if self._async_current_entries():
return self.async_abort(reason="single_instance_allowed")

return self.async_create_entry(
title="Recorder",
data=user_input,
)
6 changes: 6 additions & 0 deletions homeassistant/components/recorder/const.py
Expand Up @@ -4,7 +4,13 @@
SQLITE_URL_PREFIX = "sqlite://"
DOMAIN = "recorder"

CONF_AUTO_PURGE = "auto_purge"
CONF_COMMIT_INTERVAL = "commit_interval"
CONF_DB_URL = "db_url"
CONF_DB_MAX_RETRIES = "db_max_retries"
CONF_DB_RETRY_WAIT = "db_retry_wait"
CONF_DB_INTEGRITY_CHECK = "db_integrity_check"
CONF_PURGE_KEEP_DAYS = "purge_keep_days"

MAX_QUEUE_BACKLOG = 30000

Expand Down
1 change: 1 addition & 0 deletions homeassistant/components/recorder/manifest.json
@@ -1,6 +1,7 @@
{
"domain": "recorder",
"name": "Recorder",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/recorder",
"requirements": ["sqlalchemy==1.4.27"],
"codeowners": ["@home-assistant/core"],
Expand Down
72 changes: 69 additions & 3 deletions homeassistant/components/recorder/websocket_api.py
@@ -1,4 +1,4 @@
"""The Energy websocket API."""
"""The Recorder websocket API."""
from __future__ import annotations

import logging
Expand All @@ -7,9 +7,21 @@
import voluptuous as vol

from homeassistant.components import websocket_api
from homeassistant.const import CONF_EXCLUDE, CONF_INCLUDE
from homeassistant.core import HomeAssistant, callback

from .const import DATA_INSTANCE, MAX_QUEUE_BACKLOG
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entityfilter import INCLUDE_EXCLUDE_FILTER_SCHEMA_INNER

from .const import (
CONF_AUTO_PURGE,
CONF_COMMIT_INTERVAL,
CONF_DB_MAX_RETRIES,
CONF_DB_RETRY_WAIT,
CONF_DB_URL,
CONF_PURGE_KEEP_DAYS,
DATA_INSTANCE,
MAX_QUEUE_BACKLOG,
)
from .statistics import validate_statistics
from .util import async_migration_in_progress

Expand All @@ -26,10 +38,64 @@ def async_setup(hass: HomeAssistant) -> None:
websocket_api.async_register_command(hass, ws_clear_statistics)
websocket_api.async_register_command(hass, ws_update_statistics_metadata)
websocket_api.async_register_command(hass, ws_info)
websocket_api.async_register_command(hass, ws_config)
websocket_api.async_register_command(hass, ws_update_config)
websocket_api.async_register_command(hass, ws_backup_start)
websocket_api.async_register_command(hass, ws_backup_end)


@websocket_api.websocket_command(
{
vol.Required("type"): "recorder/config",
}
)
@callback
def ws_config(
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict
) -> None:
"""Return config of the recorder."""
instance: Recorder = hass.data[DATA_INSTANCE]
connection.send_result(msg["id"], dict(instance.config_entry.data))


@websocket_api.websocket_command(
{
vol.Required("type"): "recorder/update_config",
vol.Required("config"): vol.Schema(
{
vol.Optional(CONF_AUTO_PURGE): cv.boolean,
vol.Optional(CONF_COMMIT_INTERVAL): cv.positive_int,
vol.Optional(CONF_DB_URL): cv.string,
vol.Optional(CONF_DB_MAX_RETRIES): cv.positive_int,
vol.Optional(CONF_DB_RETRY_WAIT): cv.positive_int,
vol.Optional(CONF_EXCLUDE): INCLUDE_EXCLUDE_FILTER_SCHEMA_INNER,
vol.Optional(CONF_INCLUDE): INCLUDE_EXCLUDE_FILTER_SCHEMA_INNER,
vol.Optional(CONF_PURGE_KEEP_DAYS): vol.All(
vol.Coerce(int), vol.Range(min=1)
),
}
),
}
)
@callback
def ws_update_config(
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict
) -> None:
"""Update the recoder config."""
instance: Recorder = hass.data[DATA_INSTANCE]
config_entry = instance.config_entry
data_to_save = {**config_entry.data, **msg["config"]}
_LOGGER.warning(
"Updating Recorder configuration from %s to %s",
config_entry.data,
data_to_save,
)

hass.config_entries.async_update_entry(config_entry, data=data_to_save)
instance.update_config(config_entry)
connection.send_result(msg["id"])


@websocket_api.websocket_command(
{
vol.Required("type"): "recorder/validate_statistics",
Expand Down
1 change: 1 addition & 0 deletions homeassistant/generated/config_flows.py
Expand Up @@ -249,6 +249,7 @@
"rainmachine",
"rdw",
"recollect_waste",
"recorder",
"renault",
"rfxtrx",
"ridwell",
Expand Down
11 changes: 4 additions & 7 deletions tests/components/recorder/test_init.py
Expand Up @@ -55,6 +55,7 @@
from .conftest import SetupRecorderInstanceT

from tests.common import (
MockConfigEntry,
async_fire_time_changed,
async_init_recorder_component,
fire_time_changed,
Expand All @@ -64,15 +65,11 @@

def _default_recorder(hass):
"""Return a recorder with reasonable defaults."""
conf = CONFIG_SCHEMA({DOMAIN: {"db_url": "sqlite://"}})
config_entry = MockConfigEntry(data=conf[DOMAIN])
return Recorder(
hass,
auto_purge=True,
keep_days=7,
commit_interval=1,
uri="sqlite://",
db_max_retries=10,
db_retry_wait=3,
entity_filter=CONFIG_SCHEMA({DOMAIN: {}}),
config_entry,
exclude_t=[],
)

Expand Down
7 changes: 5 additions & 2 deletions tests/components/recorder/test_websocket_api.py
Expand Up @@ -9,6 +9,7 @@

from homeassistant.components import recorder
from homeassistant.components.recorder.const import DATA_INSTANCE
from homeassistant.config_entries import ConfigEntryState
from homeassistant.setup import async_setup_component
import homeassistant.util.dt as dt_util
from homeassistant.util.unit_system import METRIC_SYSTEM
Expand Down Expand Up @@ -287,10 +288,12 @@ async def test_recorder_info_bad_recorder_config(hass, hass_ws_client):
client = await hass_ws_client()

with patch("homeassistant.components.recorder.migration.migrate_schema"):
assert not await async_setup_component(
assert await async_setup_component(
hass, recorder.DOMAIN, {recorder.DOMAIN: config}
)
assert recorder.DOMAIN not in hass.config.components
assert recorder.DOMAIN in hass.config.components
entries = hass.config_entries.async_entries(recorder.DOMAIN)
assert ConfigEntryState.SETUP_ERROR == entries[0].state
await hass.async_block_till_done()

# Wait for recorder to shut down
Expand Down