mirror of
https://github.com/home-assistant/core.git
synced 2025-04-23 16:57:53 +00:00
Use builtin store for remember the milk config
This commit is contained in:
parent
d2a660714f
commit
cbc1899990
@ -5,20 +5,18 @@ import voluptuous as vol
|
||||
|
||||
from homeassistant.components import configurator
|
||||
from homeassistant.const import CONF_API_KEY, CONF_ID, CONF_NAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import LOGGER
|
||||
from .const import DOMAIN, LOGGER
|
||||
from .entity import RememberTheMilkEntity
|
||||
from .storage import RememberTheMilkConfiguration
|
||||
|
||||
# httplib2 is a transitive dependency from RtmAPI. If this dependency is not
|
||||
# set explicitly, the library does not work.
|
||||
|
||||
DOMAIN = "remember_the_milk"
|
||||
|
||||
CONF_SHARED_SECRET = "shared_secret"
|
||||
|
||||
RTM_SCHEMA = vol.Schema(
|
||||
@ -43,11 +41,12 @@ SERVICE_SCHEMA_CREATE_TASK = vol.Schema(
|
||||
SERVICE_SCHEMA_COMPLETE_TASK = vol.Schema({vol.Required(CONF_ID): cv.string})
|
||||
|
||||
|
||||
def setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the Remember the milk component."""
|
||||
component = EntityComponent[RememberTheMilkEntity](LOGGER, DOMAIN, hass)
|
||||
|
||||
stored_rtm_config = RememberTheMilkConfiguration(hass)
|
||||
await stored_rtm_config.setup()
|
||||
for rtm_config in config[DOMAIN]:
|
||||
account_name = rtm_config[CONF_NAME]
|
||||
LOGGER.debug("Adding Remember the milk account %s", account_name)
|
||||
@ -56,7 +55,7 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
token = stored_rtm_config.get_token(account_name)
|
||||
if token:
|
||||
LOGGER.debug("found token for account %s", account_name)
|
||||
_create_instance(
|
||||
await _create_instance(
|
||||
hass,
|
||||
account_name,
|
||||
api_key,
|
||||
@ -66,7 +65,7 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
component,
|
||||
)
|
||||
else:
|
||||
_register_new_account(
|
||||
await _register_new_account(
|
||||
hass, account_name, api_key, shared_secret, stored_rtm_config, component
|
||||
)
|
||||
|
||||
@ -74,20 +73,28 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
def _create_instance(
|
||||
hass, account_name, api_key, shared_secret, token, stored_rtm_config, component
|
||||
):
|
||||
async def _create_instance(
|
||||
hass: HomeAssistant,
|
||||
account_name: str,
|
||||
api_key: str,
|
||||
shared_secret: str,
|
||||
token: str,
|
||||
stored_rtm_config: RememberTheMilkConfiguration,
|
||||
component: EntityComponent[RememberTheMilkEntity],
|
||||
) -> None:
|
||||
entity = RememberTheMilkEntity(
|
||||
account_name, api_key, shared_secret, token, stored_rtm_config
|
||||
)
|
||||
component.add_entities([entity])
|
||||
hass.services.register(
|
||||
LOGGER.debug("Instance created for account %s", entity.name)
|
||||
await entity.check_token(hass)
|
||||
await component.async_add_entities([entity])
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
f"{account_name}_create_task",
|
||||
entity.create_task,
|
||||
schema=SERVICE_SCHEMA_CREATE_TASK,
|
||||
)
|
||||
hass.services.register(
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
f"{account_name}_complete_task",
|
||||
entity.complete_task,
|
||||
@ -95,21 +102,32 @@ def _create_instance(
|
||||
)
|
||||
|
||||
|
||||
def _register_new_account(
|
||||
hass, account_name, api_key, shared_secret, stored_rtm_config, component
|
||||
):
|
||||
request_id = None
|
||||
async def _register_new_account(
|
||||
hass: HomeAssistant,
|
||||
account_name: str,
|
||||
api_key: str,
|
||||
shared_secret: str,
|
||||
stored_rtm_config: RememberTheMilkConfiguration,
|
||||
component: EntityComponent[RememberTheMilkEntity],
|
||||
) -> None:
|
||||
"""Register a new account."""
|
||||
api = Rtm(api_key, shared_secret, "write", None)
|
||||
url, frob = api.authenticate_desktop()
|
||||
url, frob = await hass.async_add_executor_job(api.authenticate_desktop)
|
||||
LOGGER.debug("Sent authentication request to server")
|
||||
|
||||
@callback
|
||||
def register_account_callback(fields: list[dict[str, str]]) -> None:
|
||||
"""Call for register the configurator."""
|
||||
api.retrieve_token(frob)
|
||||
token = api.token
|
||||
if api.token is None:
|
||||
hass.async_create_task(handle_token(api, frob))
|
||||
|
||||
async def handle_token(api: Rtm, frob: str) -> None:
|
||||
"""Handle token."""
|
||||
await hass.async_add_executor_job(api.retrieve_token, frob)
|
||||
|
||||
token: str | None = api.token
|
||||
if token is None:
|
||||
LOGGER.error("Failed to register, please try again")
|
||||
configurator.notify_errors(
|
||||
configurator.async_notify_errors(
|
||||
hass, request_id, "Failed to register, please try again."
|
||||
)
|
||||
return
|
||||
@ -117,7 +135,7 @@ def _register_new_account(
|
||||
stored_rtm_config.set_token(account_name, token)
|
||||
LOGGER.debug("Retrieved new token from server")
|
||||
|
||||
_create_instance(
|
||||
await _create_instance(
|
||||
hass,
|
||||
account_name,
|
||||
api_key,
|
||||
@ -127,9 +145,9 @@ def _register_new_account(
|
||||
component,
|
||||
)
|
||||
|
||||
configurator.request_done(hass, request_id)
|
||||
configurator.async_request_done(hass, request_id)
|
||||
|
||||
request_id = configurator.request_config(
|
||||
request_id = configurator.async_request_config(
|
||||
hass,
|
||||
f"{DOMAIN} - {account_name}",
|
||||
callback=register_account_callback,
|
||||
|
@ -2,4 +2,5 @@
|
||||
|
||||
import logging
|
||||
|
||||
DOMAIN = "remember_the_milk"
|
||||
LOGGER = logging.getLogger(__package__)
|
||||
|
@ -1,18 +1,30 @@
|
||||
"""Support to interact with Remember The Milk."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from rtmapi import Rtm, RtmRequestFailedException
|
||||
|
||||
from homeassistant.const import CONF_ID, CONF_NAME, STATE_OK
|
||||
from homeassistant.core import ServiceCall
|
||||
from homeassistant.core import HomeAssistant, ServiceCall
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
from .const import LOGGER
|
||||
from .storage import RememberTheMilkConfiguration
|
||||
|
||||
|
||||
class RememberTheMilkEntity(Entity):
|
||||
"""Representation of an interface to Remember The Milk."""
|
||||
|
||||
def __init__(self, name, api_key, shared_secret, token, rtm_config):
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
api_key: str,
|
||||
shared_secret: str,
|
||||
token: str,
|
||||
rtm_config: RememberTheMilkConfiguration,
|
||||
) -> None:
|
||||
"""Create new instance of Remember The Milk component."""
|
||||
self._name = name
|
||||
self._api_key = api_key
|
||||
@ -20,29 +32,27 @@ class RememberTheMilkEntity(Entity):
|
||||
self._token = token
|
||||
self._rtm_config = rtm_config
|
||||
self._rtm_api = Rtm(api_key, shared_secret, "delete", token)
|
||||
self._token_valid = None
|
||||
self._check_token()
|
||||
LOGGER.debug("Instance created for account %s", self._name)
|
||||
self._token_valid = False
|
||||
|
||||
def _check_token(self):
|
||||
async def check_token(self, hass: HomeAssistant) -> None:
|
||||
"""Check if the API token is still valid.
|
||||
|
||||
If it is not valid any more, delete it from the configuration. This
|
||||
will trigger a new authentication process.
|
||||
"""
|
||||
valid = self._rtm_api.token_valid()
|
||||
if not valid:
|
||||
LOGGER.error(
|
||||
"Token for account %s is invalid. You need to register again!",
|
||||
self.name,
|
||||
)
|
||||
self._rtm_config.delete_token(self._name)
|
||||
self._token_valid = False
|
||||
else:
|
||||
valid = await hass.async_add_executor_job(self._rtm_api.token_valid)
|
||||
if valid:
|
||||
self._token_valid = True
|
||||
return self._token_valid
|
||||
return
|
||||
|
||||
def create_task(self, call: ServiceCall) -> None:
|
||||
LOGGER.error(
|
||||
"Token for account %s is invalid. You need to register again!",
|
||||
self.name,
|
||||
)
|
||||
self._rtm_config.delete_token(self._name)
|
||||
self._token_valid = False
|
||||
|
||||
async def create_task(self, call: ServiceCall) -> None:
|
||||
"""Create a new task on Remember The Milk.
|
||||
|
||||
You can use the smart syntax to define the attributes of a new task,
|
||||
@ -55,12 +65,11 @@ class RememberTheMilkEntity(Entity):
|
||||
rtm_id = None
|
||||
if hass_id is not None:
|
||||
rtm_id = self._rtm_config.get_rtm_id(self._name, hass_id)
|
||||
result = self._rtm_api.rtm.timelines.create()
|
||||
timeline = result.timeline.value
|
||||
|
||||
if rtm_id is None:
|
||||
result = self._rtm_api.rtm.tasks.add(
|
||||
timeline=timeline, name=task_name, parse="1"
|
||||
result = await self.hass.async_add_executor_job(
|
||||
self._add_task,
|
||||
task_name,
|
||||
)
|
||||
LOGGER.debug(
|
||||
"Created new task '%s' in account %s", task_name, self.name
|
||||
@ -74,12 +83,10 @@ class RememberTheMilkEntity(Entity):
|
||||
result.list.taskseries.task.id,
|
||||
)
|
||||
else:
|
||||
self._rtm_api.rtm.tasks.setName(
|
||||
name=task_name,
|
||||
list_id=rtm_id[0],
|
||||
taskseries_id=rtm_id[1],
|
||||
task_id=rtm_id[2],
|
||||
timeline=timeline,
|
||||
await self.hass.async_add_executor_job(
|
||||
self._rename_task,
|
||||
rtm_id,
|
||||
task_name,
|
||||
)
|
||||
LOGGER.debug(
|
||||
"Updated task with id '%s' in account %s to name %s",
|
||||
@ -94,7 +101,29 @@ class RememberTheMilkEntity(Entity):
|
||||
rtm_exception,
|
||||
)
|
||||
|
||||
def complete_task(self, call: ServiceCall) -> None:
|
||||
def _add_task(self, task_name: str) -> Any:
|
||||
"""Add a task."""
|
||||
result = self._rtm_api.rtm.timelines.create()
|
||||
timeline = result.timeline.value
|
||||
return self._rtm_api.rtm.tasks.add(
|
||||
timeline=timeline,
|
||||
name=task_name,
|
||||
parse="1",
|
||||
)
|
||||
|
||||
def _rename_task(self, rtm_id: tuple[str, str, str], task_name: str) -> None:
|
||||
"""Rename a task."""
|
||||
result = self._rtm_api.rtm.timelines.create()
|
||||
timeline = result.timeline.value
|
||||
self._rtm_api.rtm.tasks.setName(
|
||||
name=task_name,
|
||||
list_id=rtm_id[0],
|
||||
taskseries_id=rtm_id[1],
|
||||
task_id=rtm_id[2],
|
||||
timeline=timeline,
|
||||
)
|
||||
|
||||
async def complete_task(self, call: ServiceCall) -> None:
|
||||
"""Complete a task that was previously created by this component."""
|
||||
hass_id = call.data[CONF_ID]
|
||||
rtm_id = self._rtm_config.get_rtm_id(self._name, hass_id)
|
||||
@ -109,30 +138,36 @@ class RememberTheMilkEntity(Entity):
|
||||
)
|
||||
return
|
||||
try:
|
||||
result = self._rtm_api.rtm.timelines.create()
|
||||
timeline = result.timeline.value
|
||||
self._rtm_api.rtm.tasks.complete(
|
||||
list_id=rtm_id[0],
|
||||
taskseries_id=rtm_id[1],
|
||||
task_id=rtm_id[2],
|
||||
timeline=timeline,
|
||||
)
|
||||
self._rtm_config.delete_rtm_id(self._name, hass_id)
|
||||
LOGGER.debug("Completed task with id %s in account %s", hass_id, self._name)
|
||||
await self.hass.async_add_executor_job(self._complete_task, rtm_id)
|
||||
except RtmRequestFailedException as rtm_exception:
|
||||
LOGGER.error(
|
||||
"Error creating new Remember The Milk task for account %s: %s",
|
||||
self._name,
|
||||
rtm_exception,
|
||||
)
|
||||
return
|
||||
|
||||
self._rtm_config.delete_rtm_id(self._name, hass_id)
|
||||
LOGGER.debug("Completed task with id %s in account %s", hass_id, self._name)
|
||||
|
||||
def _complete_task(self, rtm_id: tuple[str, str, str]) -> None:
|
||||
"""Complete a task."""
|
||||
result = self._rtm_api.rtm.timelines.create()
|
||||
timeline = result.timeline.value
|
||||
self._rtm_api.rtm.tasks.complete(
|
||||
list_id=rtm_id[0],
|
||||
taskseries_id=rtm_id[1],
|
||||
task_id=rtm_id[2],
|
||||
timeline=timeline,
|
||||
)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
def name(self) -> str:
|
||||
"""Return the name of the device."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
def state(self) -> str:
|
||||
"""Return the state of the device."""
|
||||
if not self._token_valid:
|
||||
return "API token invalid"
|
||||
|
@ -4,17 +4,31 @@ from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import TypedDict
|
||||
|
||||
from homeassistant.const import CONF_TOKEN
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.storage import Store
|
||||
|
||||
from .const import LOGGER
|
||||
from .const import DOMAIN, LOGGER
|
||||
|
||||
CONFIG_FILE_NAME = ".remember_the_milk.conf"
|
||||
CONF_ID_MAP = "id_map"
|
||||
CONF_LIST_ID = "list_id"
|
||||
CONF_TASK_ID = "task_id"
|
||||
CONF_TIMESERIES_ID = "timeseries_id"
|
||||
LEGACY_CONFIG_FILE_NAME = ".remember_the_milk.conf"
|
||||
STORE_DELAY_SAVE = 30
|
||||
|
||||
|
||||
class StoredUserConfig(TypedDict, total=False):
|
||||
"""Represent the stored config for a username."""
|
||||
|
||||
id_map: dict[str, TaskIds]
|
||||
token: str
|
||||
|
||||
|
||||
class TaskIds(TypedDict):
|
||||
"""Represent the stored ids of a task."""
|
||||
|
||||
list_id: str
|
||||
timeseries_id: str
|
||||
task_id: str
|
||||
|
||||
|
||||
class RememberTheMilkConfiguration:
|
||||
@ -22,31 +36,63 @@ class RememberTheMilkConfiguration:
|
||||
|
||||
def __init__(self, hass: HomeAssistant) -> None:
|
||||
"""Create new instance of configuration."""
|
||||
self._config_file_path = hass.config.path(CONFIG_FILE_NAME)
|
||||
self._config = {}
|
||||
LOGGER.debug("Loading configuration from file: %s", self._config_file_path)
|
||||
self._legacy_config_path = hass.config.path(LEGACY_CONFIG_FILE_NAME)
|
||||
self._config: dict[str, StoredUserConfig] = {}
|
||||
self._hass = hass
|
||||
self._store = Store[dict[str, StoredUserConfig]](hass, 1, DOMAIN)
|
||||
|
||||
async def setup(self) -> None:
|
||||
"""Set up the configuration."""
|
||||
if not (config := await self._hass.async_add_executor_job(self._load_legacy)):
|
||||
config = await self._load()
|
||||
|
||||
self._config = config or {}
|
||||
|
||||
def _load_legacy(self) -> dict[str, StoredUserConfig] | None:
|
||||
"""Load configuration from legacy storage."""
|
||||
# Do not load from legacy if the new store exists.
|
||||
if Path(self._store.path).exists():
|
||||
return None
|
||||
|
||||
LOGGER.debug(
|
||||
"Loading legacy configuration from file: %s", self._legacy_config_path
|
||||
)
|
||||
config: dict[str, StoredUserConfig] | None = None
|
||||
|
||||
try:
|
||||
self._config = json.loads(
|
||||
Path(self._config_file_path).read_text(encoding="utf8")
|
||||
config = json.loads(
|
||||
Path(self._legacy_config_path).read_text(encoding="utf8")
|
||||
)
|
||||
except FileNotFoundError:
|
||||
LOGGER.debug("Missing configuration file: %s", self._config_file_path)
|
||||
LOGGER.debug(
|
||||
"Missing legacy configuration file: %s", self._legacy_config_path
|
||||
)
|
||||
except OSError:
|
||||
LOGGER.debug(
|
||||
"Failed to read from configuration file, %s, using empty configuration",
|
||||
self._config_file_path,
|
||||
"Failed to read from legacy configuration file, %s, using empty configuration",
|
||||
self._legacy_config_path,
|
||||
)
|
||||
except ValueError:
|
||||
LOGGER.error(
|
||||
"Failed to parse configuration file, %s, using empty configuration",
|
||||
self._config_file_path,
|
||||
"Failed to parse legacy configuration file, %s, using empty configuration",
|
||||
self._legacy_config_path,
|
||||
)
|
||||
|
||||
return config
|
||||
|
||||
async def _load(self) -> dict[str, StoredUserConfig] | None:
|
||||
"""Load the store."""
|
||||
return await self._store.async_load()
|
||||
|
||||
@callback
|
||||
def _save_config(self) -> None:
|
||||
"""Write the configuration to a file."""
|
||||
Path(self._config_file_path).write_text(
|
||||
json.dumps(self._config), encoding="utf8"
|
||||
)
|
||||
"""Save config."""
|
||||
self._store.async_delay_save(self._data_to_save, STORE_DELAY_SAVE)
|
||||
|
||||
@callback
|
||||
def _data_to_save(self) -> dict[str, StoredUserConfig]:
|
||||
"""Return data to save."""
|
||||
return self._config
|
||||
|
||||
def get_token(self, profile_name: str) -> str | None:
|
||||
"""Get the server token for a profile."""
|
||||
@ -54,12 +100,14 @@ class RememberTheMilkConfiguration:
|
||||
return self._config[profile_name][CONF_TOKEN]
|
||||
return None
|
||||
|
||||
@callback
|
||||
def set_token(self, profile_name: str, token: str) -> None:
|
||||
"""Store a new server token for a profile."""
|
||||
self._initialize_profile(profile_name)
|
||||
self._config[profile_name][CONF_TOKEN] = token
|
||||
self._save_config()
|
||||
|
||||
@callback
|
||||
def delete_token(self, profile_name: str) -> None:
|
||||
"""Delete a token for a profile.
|
||||
|
||||
@ -72,8 +120,8 @@ class RememberTheMilkConfiguration:
|
||||
"""Initialize the data structures for a profile."""
|
||||
if profile_name not in self._config:
|
||||
self._config[profile_name] = {}
|
||||
if CONF_ID_MAP not in self._config[profile_name]:
|
||||
self._config[profile_name][CONF_ID_MAP] = {}
|
||||
if "id_map" not in self._config[profile_name]:
|
||||
self._config[profile_name]["id_map"] = {}
|
||||
|
||||
def get_rtm_id(
|
||||
self, profile_name: str, hass_id: str
|
||||
@ -84,11 +132,12 @@ class RememberTheMilkConfiguration:
|
||||
list id, timeseries id and the task id.
|
||||
"""
|
||||
self._initialize_profile(profile_name)
|
||||
ids = self._config[profile_name][CONF_ID_MAP].get(hass_id)
|
||||
if ids is None:
|
||||
task_ids = self._config[profile_name]["id_map"].get(hass_id)
|
||||
if task_ids is None:
|
||||
return None
|
||||
return ids[CONF_LIST_ID], ids[CONF_TIMESERIES_ID], ids[CONF_TASK_ID]
|
||||
return task_ids["list_id"], task_ids["timeseries_id"], task_ids["task_id"]
|
||||
|
||||
@callback
|
||||
def set_rtm_id(
|
||||
self,
|
||||
profile_name: str,
|
||||
@ -97,19 +146,20 @@ class RememberTheMilkConfiguration:
|
||||
time_series_id: str,
|
||||
rtm_task_id: str,
|
||||
) -> None:
|
||||
"""Add/Update the RTM task ID for a Home Assistant task IS."""
|
||||
"""Add/Update the RTM task ID for a Home Assistant task ID."""
|
||||
self._initialize_profile(profile_name)
|
||||
id_tuple = {
|
||||
CONF_LIST_ID: list_id,
|
||||
CONF_TIMESERIES_ID: time_series_id,
|
||||
CONF_TASK_ID: rtm_task_id,
|
||||
}
|
||||
self._config[profile_name][CONF_ID_MAP][hass_id] = id_tuple
|
||||
ids = TaskIds(
|
||||
list_id=list_id,
|
||||
timeseries_id=time_series_id,
|
||||
task_id=rtm_task_id,
|
||||
)
|
||||
self._config[profile_name]["id_map"][hass_id] = ids
|
||||
self._save_config()
|
||||
|
||||
@callback
|
||||
def delete_rtm_id(self, profile_name: str, hass_id: str) -> None:
|
||||
"""Delete a key mapping."""
|
||||
self._initialize_profile(profile_name)
|
||||
if hass_id in self._config[profile_name][CONF_ID_MAP]:
|
||||
del self._config[profile_name][CONF_ID_MAP][hass_id]
|
||||
if hass_id in self._config[profile_name]["id_map"]:
|
||||
del self._config[profile_name]["id_map"][hass_id]
|
||||
self._save_config()
|
||||
|
@ -32,7 +32,8 @@ def client_fixture() -> Generator[MagicMock]:
|
||||
async def storage(hass: HomeAssistant, client) -> AsyncGenerator[MagicMock]:
|
||||
"""Mock the config storage."""
|
||||
with patch(
|
||||
"homeassistant.components.remember_the_milk.RememberTheMilkConfiguration"
|
||||
"homeassistant.components.remember_the_milk.RememberTheMilkConfiguration",
|
||||
autospec=True,
|
||||
) as storage_class:
|
||||
storage = storage_class.return_value
|
||||
storage.get_token.return_value = TOKEN
|
||||
|
@ -4,11 +4,10 @@ import json
|
||||
|
||||
PROFILE = "myprofile"
|
||||
TOKEN = "mytoken"
|
||||
JSON_STRING = json.dumps(
|
||||
{
|
||||
"myprofile": {
|
||||
"token": "mytoken",
|
||||
"id_map": {"123": {"list_id": "1", "timeseries_id": "2", "task_id": "3"}},
|
||||
}
|
||||
STORED_DATA = {
|
||||
"myprofile": {
|
||||
"token": "mytoken",
|
||||
"id_map": {"123": {"list_id": "1", "timeseries_id": "2", "task_id": "3"}},
|
||||
}
|
||||
)
|
||||
}
|
||||
JSON_STRING = json.dumps(STORED_DATA)
|
||||
|
@ -1,54 +1,88 @@
|
||||
"""Tests for the Remember The Milk integration."""
|
||||
|
||||
import json
|
||||
from collections.abc import Generator
|
||||
from typing import Any
|
||||
from unittest.mock import mock_open, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components import remember_the_milk as rtm
|
||||
from homeassistant.components.remember_the_milk import (
|
||||
DOMAIN,
|
||||
RememberTheMilkConfiguration,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .const import JSON_STRING, PROFILE, TOKEN
|
||||
from .const import JSON_STRING, PROFILE, STORED_DATA, TOKEN
|
||||
|
||||
from tests.common import async_fire_time_changed
|
||||
|
||||
pytestmark = pytest.mark.parametrize("new_storage_exists", [True, False])
|
||||
|
||||
|
||||
def test_set_get_delete_token(hass: HomeAssistant) -> None:
|
||||
@pytest.fixture(autouse=True)
|
||||
def mock_path_exists(new_storage_exists: bool) -> Generator[None]:
|
||||
"""Mock path exists."""
|
||||
with patch(
|
||||
"homeassistant.components.remember_the_milk.storage.Path.exists",
|
||||
return_value=new_storage_exists,
|
||||
):
|
||||
yield
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def mock_delay_save() -> Generator[None]:
|
||||
"""Mock delay save."""
|
||||
with patch(
|
||||
"homeassistant.components.remember_the_milk.storage.STORE_DELAY_SAVE", 0
|
||||
):
|
||||
yield
|
||||
|
||||
|
||||
async def test_set_get_delete_token(
|
||||
hass: HomeAssistant,
|
||||
hass_storage: dict[str, Any],
|
||||
) -> None:
|
||||
"""Test set, get and delete token."""
|
||||
open_mock = mock_open()
|
||||
config = RememberTheMilkConfiguration(hass)
|
||||
with patch(
|
||||
"homeassistant.components.remember_the_milk.storage.Path.open", open_mock
|
||||
):
|
||||
config = rtm.RememberTheMilkConfiguration(hass)
|
||||
assert open_mock.return_value.write.call_count == 0
|
||||
await config.setup()
|
||||
assert config.get_token(PROFILE) is None
|
||||
assert open_mock.return_value.write.call_count == 0
|
||||
config.set_token(PROFILE, TOKEN)
|
||||
assert open_mock.return_value.write.call_count == 1
|
||||
assert open_mock.return_value.write.call_args[0][0] == json.dumps(
|
||||
{
|
||||
"myprofile": {
|
||||
"id_map": {},
|
||||
"token": "mytoken",
|
||||
}
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
assert hass_storage[DOMAIN]["data"] == {
|
||||
"myprofile": {
|
||||
"id_map": {},
|
||||
"token": "mytoken",
|
||||
}
|
||||
)
|
||||
}
|
||||
assert config.get_token(PROFILE) == TOKEN
|
||||
assert open_mock.return_value.write.call_count == 1
|
||||
config.delete_token(PROFILE)
|
||||
assert open_mock.return_value.write.call_count == 2
|
||||
assert open_mock.return_value.write.call_args[0][0] == json.dumps({})
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
assert hass_storage[DOMAIN]["data"] == {}
|
||||
assert config.get_token(PROFILE) is None
|
||||
assert open_mock.return_value.write.call_count == 2
|
||||
|
||||
|
||||
def test_config_load(hass: HomeAssistant) -> None:
|
||||
async def test_config_load(hass: HomeAssistant, hass_storage: dict[str, Any]) -> None:
|
||||
"""Test loading from the file."""
|
||||
hass_storage[DOMAIN] = {
|
||||
"data": STORED_DATA,
|
||||
"key": DOMAIN,
|
||||
"version": 1,
|
||||
"minor_version": 1,
|
||||
}
|
||||
config = RememberTheMilkConfiguration(hass)
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.remember_the_milk.storage.Path.open",
|
||||
mock_open(read_data=JSON_STRING),
|
||||
),
|
||||
):
|
||||
config = rtm.RememberTheMilkConfiguration(hass)
|
||||
await config.setup()
|
||||
|
||||
rtm_id = config.get_rtm_id(PROFILE, "123")
|
||||
assert rtm_id is not None
|
||||
@ -58,16 +92,18 @@ def test_config_load(hass: HomeAssistant) -> None:
|
||||
@pytest.mark.parametrize(
|
||||
"side_effect", [FileNotFoundError("Missing file"), OSError("IO error")]
|
||||
)
|
||||
def test_config_load_file_error(hass: HomeAssistant, side_effect: Exception) -> None:
|
||||
async def test_config_load_file_error(
|
||||
hass: HomeAssistant, side_effect: Exception
|
||||
) -> None:
|
||||
"""Test loading with file error."""
|
||||
config = rtm.RememberTheMilkConfiguration(hass)
|
||||
config = RememberTheMilkConfiguration(hass)
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.remember_the_milk.storage.Path.open",
|
||||
side_effect=side_effect,
|
||||
),
|
||||
):
|
||||
config = rtm.RememberTheMilkConfiguration(hass)
|
||||
await config.setup()
|
||||
|
||||
# The config should be empty and we should not have any errors
|
||||
# when trying to access it.
|
||||
@ -75,16 +111,16 @@ def test_config_load_file_error(hass: HomeAssistant, side_effect: Exception) ->
|
||||
assert rtm_id is None
|
||||
|
||||
|
||||
def test_config_load_invalid_data(hass: HomeAssistant) -> None:
|
||||
async def test_config_load_invalid_data(hass: HomeAssistant) -> None:
|
||||
"""Test loading invalid data."""
|
||||
config = rtm.RememberTheMilkConfiguration(hass)
|
||||
config = RememberTheMilkConfiguration(hass)
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.remember_the_milk.storage.Path.open",
|
||||
mock_open(read_data="random characters"),
|
||||
),
|
||||
):
|
||||
config = rtm.RememberTheMilkConfiguration(hass)
|
||||
await config.setup()
|
||||
|
||||
# The config should be empty and we should not have any errors
|
||||
# when trying to access it.
|
||||
@ -92,40 +128,39 @@ def test_config_load_invalid_data(hass: HomeAssistant) -> None:
|
||||
assert rtm_id is None
|
||||
|
||||
|
||||
def test_config_set_delete_id(hass: HomeAssistant) -> None:
|
||||
async def test_config_set_delete_id(
|
||||
hass: HomeAssistant,
|
||||
hass_storage: dict[str, Any],
|
||||
) -> None:
|
||||
"""Test setting and deleting an id from the config."""
|
||||
hass_id = "123"
|
||||
list_id = "1"
|
||||
timeseries_id = "2"
|
||||
rtm_id = "3"
|
||||
open_mock = mock_open()
|
||||
config = rtm.RememberTheMilkConfiguration(hass)
|
||||
config = RememberTheMilkConfiguration(hass)
|
||||
with patch(
|
||||
"homeassistant.components.remember_the_milk.storage.Path.open", open_mock
|
||||
):
|
||||
config = rtm.RememberTheMilkConfiguration(hass)
|
||||
assert open_mock.return_value.write.call_count == 0
|
||||
await config.setup()
|
||||
assert config.get_rtm_id(PROFILE, hass_id) is None
|
||||
assert open_mock.return_value.write.call_count == 0
|
||||
config.set_rtm_id(PROFILE, hass_id, list_id, timeseries_id, rtm_id)
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
assert (list_id, timeseries_id, rtm_id) == config.get_rtm_id(PROFILE, hass_id)
|
||||
assert open_mock.return_value.write.call_count == 1
|
||||
assert open_mock.return_value.write.call_args[0][0] == json.dumps(
|
||||
{
|
||||
"myprofile": {
|
||||
"id_map": {
|
||||
"123": {"list_id": "1", "timeseries_id": "2", "task_id": "3"}
|
||||
}
|
||||
assert hass_storage[DOMAIN]["data"] == {
|
||||
"myprofile": {
|
||||
"id_map": {
|
||||
"123": {"list_id": "1", "timeseries_id": "2", "task_id": "3"}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
config.delete_rtm_id(PROFILE, hass_id)
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
assert config.get_rtm_id(PROFILE, hass_id) is None
|
||||
assert open_mock.return_value.write.call_count == 2
|
||||
assert open_mock.return_value.write.call_args[0][0] == json.dumps(
|
||||
{
|
||||
"myprofile": {
|
||||
"id_map": {},
|
||||
}
|
||||
assert hass_storage[DOMAIN]["data"] == {
|
||||
"myprofile": {
|
||||
"id_map": {},
|
||||
}
|
||||
)
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user