mirror of
https://github.com/home-assistant/core.git
synced 2025-06-25 01:21:51 +02:00
integration with Remember The Milk. (#9803)
* MVP integration with Remember The Milk. This version offers a service allowing you to create new issues in Remember The Milk. * fixed pylint issue with import path * - added files to .coveragerc as the server inerface is hard to test - added tests for config file handling * fixed lint error * added missing docstrings * removed stray edit * fixed minor issues reported by @fabaff * changed naming of the service, so that serveral accounts can be used * added disclaimer * moved service description to services.yaml * fixed blank lines * fixed structure of configuration * added comment about httplib2 * renamed internal config file * improved logging statements * moved entry in services.yaml into separate folder. Had to move the component itself as well. * fixed static analysis findings * mocked first test case * fixed bug in config handling, fixed unit tests * mocked second test case * fixed line length * fixed static analysis findings and failing test case * also renamed file in .coveragerc * control flow changes as requested by @balloob
This commit is contained in:
committed by
Paulus Schoutsen
parent
8c266f9266
commit
80a9539f97
@ -457,6 +457,7 @@ omit =
|
||||
homeassistant/components/notify/yessssms.py
|
||||
homeassistant/components/nuimo_controller.py
|
||||
homeassistant/components/prometheus.py
|
||||
homeassistant/components/remember_the_milk/__init__.py
|
||||
homeassistant/components/remote/harmony.py
|
||||
homeassistant/components/remote/itach.py
|
||||
homeassistant/components/scene/hunterdouglas_powerview.py
|
||||
|
251
homeassistant/components/remember_the_milk/__init__.py
Normal file
251
homeassistant/components/remember_the_milk/__init__.py
Normal file
@ -0,0 +1,251 @@
|
||||
"""Component to interact with Remember The Milk.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/remember_the_milk/
|
||||
|
||||
Minimum viable product, it currently only support creating new tasks in your
|
||||
Remember The Milk (https://www.rememberthemilk.com/) account.
|
||||
|
||||
This product uses the Remember The Milk API but is not endorsed or certified
|
||||
by Remember The Milk.
|
||||
"""
|
||||
import logging
|
||||
import os
|
||||
import json
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
from homeassistant.const import (CONF_API_KEY, STATE_OK, CONF_TOKEN, CONF_NAME)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
|
||||
# httplib2 is a transitive dependency from RtmAPI. If this dependency is not
|
||||
# set explicitly, the library does not work.
|
||||
REQUIREMENTS = ['RtmAPI==0.7.0', 'httplib2==0.10.3']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = 'remember_the_milk'
|
||||
DEFAULT_NAME = DOMAIN
|
||||
GROUP_NAME_RTM = 'remember the milk accounts'
|
||||
|
||||
CONF_SHARED_SECRET = 'shared_secret'
|
||||
|
||||
RTM_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_NAME): cv.string,
|
||||
vol.Required(CONF_API_KEY): cv.string,
|
||||
vol.Required(CONF_SHARED_SECRET): cv.string,
|
||||
})
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.All(cv.ensure_list, [RTM_SCHEMA])
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
CONFIG_FILE_NAME = '.remember_the_milk.conf'
|
||||
SERVICE_CREATE_TASK = 'create_task'
|
||||
|
||||
SERVICE_SCHEMA_CREATE_TASK = vol.Schema({
|
||||
vol.Required(CONF_NAME): cv.string,
|
||||
})
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Set up the remember_the_milk component."""
|
||||
component = EntityComponent(_LOGGER, DOMAIN, hass,
|
||||
group_name=GROUP_NAME_RTM)
|
||||
|
||||
descriptions = load_yaml_config_file(
|
||||
os.path.join(os.path.dirname(__file__), 'services.yaml'))
|
||||
|
||||
stored_rtm_config = RememberTheMilkConfiguration(hass)
|
||||
for rtm_config in config[DOMAIN]:
|
||||
account_name = rtm_config[CONF_NAME]
|
||||
_LOGGER.info("Adding Remember the milk account %s", account_name)
|
||||
api_key = rtm_config[CONF_API_KEY]
|
||||
shared_secret = rtm_config[CONF_SHARED_SECRET]
|
||||
token = stored_rtm_config.get_token(account_name)
|
||||
if token:
|
||||
_LOGGER.debug("found token for account %s", account_name)
|
||||
_create_instance(
|
||||
hass, account_name, api_key, shared_secret, token,
|
||||
stored_rtm_config, component, descriptions)
|
||||
else:
|
||||
_register_new_account(
|
||||
hass, account_name, api_key, shared_secret,
|
||||
stored_rtm_config, component, descriptions)
|
||||
|
||||
_LOGGER.debug("Finished adding all Remember the milk accounts")
|
||||
return True
|
||||
|
||||
|
||||
def _create_instance(hass, account_name, api_key, shared_secret,
|
||||
token, stored_rtm_config, component, descriptions):
|
||||
entity = RememberTheMilk(account_name, api_key, shared_secret,
|
||||
token, stored_rtm_config)
|
||||
component.add_entity(entity)
|
||||
hass.services.async_register(
|
||||
DOMAIN, '{}_create_task'.format(account_name), entity.create_task,
|
||||
description=descriptions.get(SERVICE_CREATE_TASK),
|
||||
schema=SERVICE_SCHEMA_CREATE_TASK)
|
||||
|
||||
|
||||
def _register_new_account(hass, account_name, api_key, shared_secret,
|
||||
stored_rtm_config, component, descriptions):
|
||||
from rtmapi import Rtm
|
||||
|
||||
request_id = None
|
||||
configurator = hass.components.configurator
|
||||
api = Rtm(api_key, shared_secret, "write", None)
|
||||
url, frob = api.authenticate_desktop()
|
||||
_LOGGER.debug('sent authentication request to server')
|
||||
|
||||
def register_account_callback(_):
|
||||
"""Callback for configurator."""
|
||||
api.retrieve_token(frob)
|
||||
token = api.token
|
||||
if api.token is None:
|
||||
_LOGGER.error('Failed to register, please try again.')
|
||||
configurator.notify_errors(
|
||||
request_id,
|
||||
'Failed to register, please try again.')
|
||||
return
|
||||
|
||||
stored_rtm_config.set_token(account_name, token)
|
||||
_LOGGER.debug('retrieved new token from server')
|
||||
|
||||
_create_instance(
|
||||
hass, account_name, api_key, shared_secret, token,
|
||||
stored_rtm_config, component, descriptions)
|
||||
|
||||
configurator.request_done(request_id)
|
||||
|
||||
request_id = configurator.async_request_config(
|
||||
'{} - {}'.format(DOMAIN, account_name),
|
||||
callback=register_account_callback,
|
||||
description='You need to log in to Remember The Milk to' +
|
||||
'connect your account. \n\n' +
|
||||
'Step 1: Click on the link "Remember The Milk login"\n\n' +
|
||||
'Step 2: Click on "login completed"',
|
||||
link_name='Remember The Milk login',
|
||||
link_url=url,
|
||||
submit_caption="login completed",
|
||||
)
|
||||
|
||||
|
||||
class RememberTheMilkConfiguration(object):
|
||||
"""Internal configuration data for RememberTheMilk class.
|
||||
|
||||
This class stores the authentication token it get from the backend.
|
||||
"""
|
||||
|
||||
def __init__(self, hass):
|
||||
"""Create new instance of configuration."""
|
||||
self._config_file_path = hass.config.path(CONFIG_FILE_NAME)
|
||||
if not os.path.isfile(self._config_file_path):
|
||||
self._config = dict()
|
||||
return
|
||||
try:
|
||||
_LOGGER.debug('loading configuration from file: %s',
|
||||
self._config_file_path)
|
||||
with open(self._config_file_path, 'r') as config_file:
|
||||
self._config = json.load(config_file)
|
||||
except ValueError:
|
||||
_LOGGER.error('failed to load configuration file, creating a '
|
||||
'new one: %s', self._config_file_path)
|
||||
self._config = dict()
|
||||
|
||||
def save_config(self):
|
||||
"""Write the configuration to a file."""
|
||||
with open(self._config_file_path, 'w') as config_file:
|
||||
json.dump(self._config, config_file)
|
||||
|
||||
def get_token(self, profile_name):
|
||||
"""Get the server token for a profile."""
|
||||
if profile_name in self._config:
|
||||
return self._config[profile_name][CONF_TOKEN]
|
||||
return None
|
||||
|
||||
def set_token(self, profile_name, token):
|
||||
"""Store a new server token for a profile."""
|
||||
if profile_name not in self._config:
|
||||
self._config[profile_name] = dict()
|
||||
self._config[profile_name][CONF_TOKEN] = token
|
||||
self.save_config()
|
||||
|
||||
def delete_token(self, profile_name):
|
||||
"""Delete a token for a profile.
|
||||
|
||||
Usually called when the token has expired.
|
||||
"""
|
||||
self._config.pop(profile_name, None)
|
||||
self.save_config()
|
||||
|
||||
|
||||
class RememberTheMilk(Entity):
|
||||
"""MVP implementation of an interface to Remember The Milk."""
|
||||
|
||||
def __init__(self, name, api_key, shared_secret, token, rtm_config):
|
||||
"""Create new instance of Remember The Milk component."""
|
||||
import rtmapi
|
||||
|
||||
self._name = name
|
||||
self._api_key = api_key
|
||||
self._shared_secret = shared_secret
|
||||
self._token = token
|
||||
self._rtm_config = rtm_config
|
||||
self._rtm_api = rtmapi.Rtm(api_key, shared_secret, "delete", token)
|
||||
self._token_valid = None
|
||||
self._check_token()
|
||||
_LOGGER.debug("instance created for account %s", self._name)
|
||||
|
||||
def _check_token(self):
|
||||
"""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:
|
||||
self._token_valid = True
|
||||
return self._token_valid
|
||||
|
||||
def create_task(self, call):
|
||||
"""Create a new task on Remember The Milk.
|
||||
|
||||
You can use the smart syntax to define the attribues of a new task,
|
||||
e.g. "my task #some_tag ^today" will add tag "some_tag" and set the
|
||||
due date to today.
|
||||
"""
|
||||
import rtmapi
|
||||
|
||||
try:
|
||||
task_name = call.data.get('name')
|
||||
result = self._rtm_api.rtm.timelines.create()
|
||||
timeline = result.timeline.value
|
||||
self._rtm_api.rtm.tasks.add(
|
||||
timeline=timeline, name=task_name, parse='1')
|
||||
_LOGGER.debug('created new task "%s" in account %s',
|
||||
task_name, self.name)
|
||||
except rtmapi.RtmRequestFailedException as rtm_exception:
|
||||
_LOGGER.error('Error creating new Remember The Milk task for '
|
||||
'account %s: %s', self._name, rtm_exception)
|
||||
return False
|
||||
return True
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the device."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the device."""
|
||||
if not self._token_valid:
|
||||
return 'API token invalid'
|
||||
return STATE_OK
|
9
homeassistant/components/remember_the_milk/services.yaml
Normal file
9
homeassistant/components/remember_the_milk/services.yaml
Normal file
@ -0,0 +1,9 @@
|
||||
# Describes the format for available Remember The Milk services
|
||||
|
||||
create_task:
|
||||
description: Create a new task in your Remember The Milk account
|
||||
|
||||
fields:
|
||||
name:
|
||||
description: name of the new task, you can use the smart syntax here
|
||||
example: 'do this ^today #from_hass'
|
@ -42,6 +42,9 @@ PyXiaomiGateway==0.6.0
|
||||
# homeassistant.components.rpi_gpio
|
||||
# RPi.GPIO==0.6.1
|
||||
|
||||
# homeassistant.components.remember_the_milk
|
||||
RtmAPI==0.7.0
|
||||
|
||||
# homeassistant.components.media_player.sonos
|
||||
SoCo==0.12
|
||||
|
||||
@ -332,6 +335,9 @@ home-assistant-frontend==20171030.0
|
||||
# homeassistant.components.camera.onvif
|
||||
http://github.com/tgaugry/suds-passworddigest-py3/archive/86fc50e39b4d2b8997481967d6a7fe1c57118999.zip#suds-passworddigest-py3==0.1.2a
|
||||
|
||||
# homeassistant.components.remember_the_milk
|
||||
httplib2==0.10.3
|
||||
|
||||
# homeassistant.components.sensor.dht
|
||||
# https://github.com/adafruit/Adafruit_Python_DHT/archive/da8cddf7fb629c1ef4f046ca44f42523c9cf2d11.zip#Adafruit_DHT==1.3.2
|
||||
|
||||
|
49
tests/components/test_remember_the_milk.py
Normal file
49
tests/components/test_remember_the_milk.py
Normal file
@ -0,0 +1,49 @@
|
||||
"""Tests for the Remember The Milk component."""
|
||||
|
||||
import logging
|
||||
import unittest
|
||||
from unittest.mock import patch, mock_open, Mock
|
||||
|
||||
import homeassistant.components.remember_the_milk as rtm
|
||||
|
||||
from tests.common import get_test_home_assistant
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TestConfiguration(unittest.TestCase):
|
||||
"""Basic tests for the class RememberTheMilkConfiguration."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test home assistant main loop."""
|
||||
self.hass = get_test_home_assistant()
|
||||
self.profile = "myprofile"
|
||||
self.token = "mytoken"
|
||||
self.json_string = '{"myprofile": {"token": "mytoken"}}'
|
||||
|
||||
def tearDown(self):
|
||||
"""Exit home assistant."""
|
||||
self.hass.stop()
|
||||
|
||||
def test_create_new(self):
|
||||
"""Test creating a new config file."""
|
||||
with patch("builtins.open", mock_open()), \
|
||||
patch("os.path.isfile", Mock(return_value=False)):
|
||||
config = rtm.RememberTheMilkConfiguration(self.hass)
|
||||
config.set_token(self.profile, self.token)
|
||||
self.assertEqual(config.get_token(self.profile), self.token)
|
||||
|
||||
def test_load_config(self):
|
||||
"""Test loading an existing token from the file."""
|
||||
with patch("builtins.open", mock_open(read_data=self.json_string)), \
|
||||
patch("os.path.isfile", Mock(return_value=True)):
|
||||
config = rtm.RememberTheMilkConfiguration(self.hass)
|
||||
self.assertEqual(config.get_token(self.profile), self.token)
|
||||
|
||||
def test_invalid_data(self):
|
||||
"""Test starts with invalid data and should not raise an exception."""
|
||||
with patch("builtins.open",
|
||||
mock_open(read_data='random charachters')),\
|
||||
patch("os.path.isfile", Mock(return_value=True)):
|
||||
config = rtm.RememberTheMilkConfiguration(self.hass)
|
||||
self.assertIsNotNone(config)
|
Reference in New Issue
Block a user