Updater component with basic system reporting (#3781)

This commit is contained in:
Johann Kellerman 2016-10-20 21:30:44 +02:00 committed by GitHub
parent a05fb4cef8
commit c70722dbae
3 changed files with 142 additions and 52 deletions

View File

@ -4,62 +4,118 @@ Support to check for available updates.
For more details about this component, please refer to the documentation at For more details about this component, please refer to the documentation at
https://home-assistant.io/components/updater/ https://home-assistant.io/components/updater/
""" """
from datetime import datetime, timedelta
import logging import logging
import json
import platform
import uuid
# pylint: disable=no-name-in-module,import-error
from distutils.version import StrictVersion
import requests import requests
import voluptuous as vol import voluptuous as vol
from homeassistant.const import __version__ as CURRENT_VERSION from homeassistant.const import __version__ as CURRENT_VERSION
from homeassistant.const import ATTR_FRIENDLY_NAME from homeassistant.const import ATTR_FRIENDLY_NAME
import homeassistant.util.dt as dt_util
from homeassistant.helpers import event from homeassistant.helpers import event
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
UPDATER_URL = 'https://updater.home-assistant.io/'
DOMAIN = 'updater' DOMAIN = 'updater'
ENTITY_ID = 'updater.updater' ENTITY_ID = 'updater.updater'
ATTR_RELEASE_NOTES = 'release_notes'
UPDATER_UUID_FILE = '.uuid'
CONF_OPT_OUT = 'opt_out'
PYPI_URL = 'https://pypi.python.org/pypi/homeassistant/json' REQUIREMENTS = ['distro==1.0.0']
CONFIG_SCHEMA = vol.Schema({ CONFIG_SCHEMA = vol.Schema({DOMAIN: {
DOMAIN: vol.Schema({}), vol.Optional(CONF_OPT_OUT, default=False): cv.boolean
}, extra=vol.ALLOW_EXTRA) }}, extra=vol.ALLOW_EXTRA)
def _create_uuid(hass, filename=UPDATER_UUID_FILE):
"""Create UUID and save it in a file."""
with open(hass.config.path(filename), 'w') as fptr:
_uuid = uuid.uuid4().hex
fptr.write(json.dumps({"uuid": _uuid}))
return _uuid
def _load_uuid(hass, filename=UPDATER_UUID_FILE):
"""Load UUID from a file, or return None."""
try:
with open(hass.config.path(filename)) as fptr:
jsonf = json.loads(fptr.read())
return uuid.UUID(jsonf['uuid'], version=4).hex
except (ValueError, AttributeError):
return None
except FileNotFoundError:
return _create_uuid(hass, filename)
def setup(hass, config): def setup(hass, config):
"""Setup the updater component.""" """Setup the updater component."""
if 'dev' in CURRENT_VERSION: if 'dev' in CURRENT_VERSION:
_LOGGER.warning("Updater not supported in development version") # This component only makes sense in release versions
_LOGGER.warning('Updater not supported in development version')
return False return False
def check_newest_version(_=None): huuid = None if config.get(CONF_OPT_OUT) else _load_uuid(hass)
"""Check if a new version is available and report if one is."""
newest = get_newest_version()
if newest != CURRENT_VERSION and newest is not None:
hass.states.set(
ENTITY_ID, newest, {ATTR_FRIENDLY_NAME: 'Update available'})
# Update daily, start 1 hour after startup
_dt = datetime.now() + timedelta(hours=1)
event.track_time_change( event.track_time_change(
hass, check_newest_version, hour=[0, 12], minute=0, second=0) hass, lambda _: check_newest_version(hass, huuid),
hour=_dt.hour, minute=_dt.minute, second=_dt.second)
check_newest_version()
return True return True
def get_newest_version(): def check_newest_version(hass, huuid):
"""Get the newest Home Assistant version from PyPI.""" """Check if a new version is available and report if one is."""
try: newest, releasenotes = get_newest_version(huuid)
req = requests.get(PYPI_URL)
return req.json()['info']['version'] if newest is not None:
if StrictVersion(newest) > StrictVersion(CURRENT_VERSION):
hass.states.set(
ENTITY_ID, newest, {ATTR_FRIENDLY_NAME: 'Update Available',
ATTR_RELEASE_NOTES: releasenotes}
)
def get_newest_version(huuid):
"""Get the newest Home Assistant version."""
info_object = {'uuid': huuid, 'version': CURRENT_VERSION,
'timezone': dt_util.DEFAULT_TIME_ZONE.zone,
'os_name': platform.system(), "arch": platform.machine(),
'python_version': platform.python_version()}
if platform.system() == 'Windows':
info_object['os_version'] = platform.win32_ver()[0]
elif platform.system() == 'Darwin':
info_object['os_version'] = platform.mac_ver()[0]
elif platform.system() == 'Linux':
import distro
linux_dist = distro.linux_distribution(full_distribution_name=False)
info_object['distribution'] = linux_dist[0]
info_object['os_version'] = linux_dist[1]
if not huuid:
info_object = {}
try:
req = requests.post(UPDATER_URL, json=info_object)
res = req.json()
return (res['version'], res['release-notes'])
except requests.RequestException: except requests.RequestException:
_LOGGER.exception("Could not contact PyPI to check for updates") _LOGGER.exception('Could not contact HASS Update to check for updates')
return None return None
except ValueError: except ValueError:
_LOGGER.exception("Received invalid response from PyPI") _LOGGER.exception('Received invalid response from HASS Update')
return None return None
except KeyError: except KeyError:
_LOGGER.exception("Response from PyPI did not include version") _LOGGER.exception('Response from HASS Update did not include version')
return None return None

View File

@ -79,6 +79,9 @@ concord232==0.14
# homeassistant.components.media_player.directv # homeassistant.components.media_player.directv
directpy==0.1 directpy==0.1
# homeassistant.components.updater
distro==1.0.0
# homeassistant.components.notify.xmpp # homeassistant.components.notify.xmpp
dnspython3==1.15.0 dnspython3==1.15.0

View File

@ -1,13 +1,16 @@
"""The tests for the Updater component.""" """The tests for the Updater component."""
from datetime import datetime, timedelta
import unittest import unittest
from unittest.mock import patch from unittest.mock import patch
import os
import requests import requests
from homeassistant.bootstrap import setup_component from homeassistant.bootstrap import setup_component
from homeassistant.components import updater from homeassistant.components import updater
import homeassistant.util.dt as dt_util
from tests.common import fire_time_changed, get_test_home_assistant from tests.common import (
assert_setup_component, fire_time_changed, get_test_home_assistant)
NEW_VERSION = '10000.0' NEW_VERSION = '10000.0'
@ -18,65 +21,93 @@ MOCK_CURRENT_VERSION = '10.0'
class TestUpdater(unittest.TestCase): class TestUpdater(unittest.TestCase):
"""Test the Updater component.""" """Test the Updater component."""
def setUp(self): # pylint: disable=invalid-name hass = None
def setup_method(self, _):
"""Setup things to be run when tests are started.""" """Setup things to be run when tests are started."""
self.hass = get_test_home_assistant() self.hass = get_test_home_assistant()
def tearDown(self): # pylint: disable=invalid-name def teardown_method(self, _):
"""Stop everything that was started.""" """Stop everything that was started."""
self.hass.stop() self.hass.stop()
@patch('homeassistant.components.updater.get_newest_version') @patch('homeassistant.components.updater.get_newest_version')
def test_new_version_shows_entity_on_start(self, mock_get_newest_version): def test_new_version_shows_entity_on_start( # pylint: disable=invalid-name
self, mock_get_newest_version):
"""Test if new entity is created if new version is available.""" """Test if new entity is created if new version is available."""
mock_get_newest_version.return_value = NEW_VERSION mock_get_newest_version.return_value = (NEW_VERSION, '')
updater.CURRENT_VERSION = MOCK_CURRENT_VERSION updater.CURRENT_VERSION = MOCK_CURRENT_VERSION
self.assertTrue(setup_component(self.hass, updater.DOMAIN, { with assert_setup_component(1) as config:
'updater': {} setup_component(self.hass, updater.DOMAIN, {updater.DOMAIN: {}})
})) _dt = datetime.now() + timedelta(hours=1)
assert config['updater'] == {'opt_out': False}
self.assertTrue(self.hass.states.is_state(
updater.ENTITY_ID, NEW_VERSION))
@patch('homeassistant.components.updater.get_newest_version')
def test_no_entity_on_same_version(self, mock_get_newest_version):
"""Test if no entity is created if same version."""
mock_get_newest_version.return_value = MOCK_CURRENT_VERSION
updater.CURRENT_VERSION = MOCK_CURRENT_VERSION
self.assertTrue(setup_component(self.hass, updater.DOMAIN, {
'updater': {}
}))
self.assertIsNone(self.hass.states.get(updater.ENTITY_ID))
mock_get_newest_version.return_value = NEW_VERSION
fire_time_changed(
self.hass, dt_util.utcnow().replace(hour=0, minute=0, second=0))
for secs in [-1, 0, 1]:
fire_time_changed(self.hass, _dt + timedelta(seconds=secs))
self.hass.block_till_done() self.hass.block_till_done()
self.assertTrue(self.hass.states.is_state( self.assertTrue(self.hass.states.is_state(
updater.ENTITY_ID, NEW_VERSION)) updater.ENTITY_ID, NEW_VERSION))
@patch('homeassistant.components.updater.requests.get') @patch('homeassistant.components.updater.get_newest_version')
def test_errors_while_fetching_new_version(self, mock_get): def test_no_entity_on_same_version( # pylint: disable=invalid-name
self, mock_get_newest_version):
"""Test if no entity is created if same version."""
mock_get_newest_version.return_value = (MOCK_CURRENT_VERSION, '')
updater.CURRENT_VERSION = MOCK_CURRENT_VERSION
with assert_setup_component(1) as config:
assert setup_component(
self.hass, updater.DOMAIN, {updater.DOMAIN: {}})
_dt = datetime.now() + timedelta(hours=1)
assert config['updater'] == {'opt_out': False}
self.assertIsNone(self.hass.states.get(updater.ENTITY_ID))
mock_get_newest_version.return_value = (NEW_VERSION, '')
for secs in [-1, 0, 1]:
fire_time_changed(self.hass, _dt + timedelta(seconds=secs))
self.hass.block_till_done()
self.assertTrue(self.hass.states.is_state(
updater.ENTITY_ID, NEW_VERSION))
@patch('homeassistant.components.updater.requests.post')
def test_errors_while_fetching_new_version( # pylint: disable=invalid-name
self, mock_get):
"""Test for errors while fetching the new version.""" """Test for errors while fetching the new version."""
mock_get.side_effect = requests.RequestException mock_get.side_effect = requests.RequestException
self.assertIsNone(updater.get_newest_version()) uuid = '0000'
self.assertIsNone(updater.get_newest_version(uuid))
mock_get.side_effect = ValueError mock_get.side_effect = ValueError
self.assertIsNone(updater.get_newest_version()) self.assertIsNone(updater.get_newest_version(uuid))
mock_get.side_effect = KeyError mock_get.side_effect = KeyError
self.assertIsNone(updater.get_newest_version()) self.assertIsNone(updater.get_newest_version(uuid))
def test_updater_disabled_on_dev(self): def test_updater_disabled_on_dev(self):
"""Test if the updater component is disabled on dev.""" """Test if the updater component is disabled on dev."""
updater.CURRENT_VERSION = MOCK_CURRENT_VERSION + 'dev' updater.CURRENT_VERSION = MOCK_CURRENT_VERSION + 'dev'
self.assertFalse(setup_component(self.hass, updater.DOMAIN, { with assert_setup_component(1) as config:
'updater': {} assert not setup_component(
})) self.hass, updater.DOMAIN, {updater.DOMAIN: {}})
assert config['updater'] == {'opt_out': False}
def test_uuid_function(self):
"""Test if the uuid function works."""
path = self.hass.config.path(updater.UPDATER_UUID_FILE)
try:
# pylint: disable=protected-access
uuid = updater._load_uuid(self.hass)
assert os.path.isfile(path)
uuid2 = updater._load_uuid(self.hass)
assert uuid == uuid2
os.remove(path)
uuid2 = updater._load_uuid(self.hass)
assert uuid != uuid2
finally:
os.remove(path)