From c70722dbaef64cb00dbf6793d84b5530856a35cb Mon Sep 17 00:00:00 2001 From: Johann Kellerman <kellerza@gmail.com> Date: Thu, 20 Oct 2016 21:30:44 +0200 Subject: [PATCH] Updater component with basic system reporting (#3781) --- homeassistant/components/updater.py | 106 +++++++++++++++++++++------- requirements_all.txt | 3 + tests/components/test_updater.py | 85 +++++++++++++++------- 3 files changed, 142 insertions(+), 52 deletions(-) diff --git a/homeassistant/components/updater.py b/homeassistant/components/updater.py index ec91149a87d..76dd9d8c09e 100644 --- a/homeassistant/components/updater.py +++ b/homeassistant/components/updater.py @@ -4,62 +4,118 @@ Support to check for available updates. For more details about this component, please refer to the documentation at https://home-assistant.io/components/updater/ """ +from datetime import datetime, timedelta import logging +import json +import platform +import uuid +# pylint: disable=no-name-in-module,import-error +from distutils.version import StrictVersion import requests import voluptuous as vol from homeassistant.const import __version__ as CURRENT_VERSION from homeassistant.const import ATTR_FRIENDLY_NAME +import homeassistant.util.dt as dt_util from homeassistant.helpers import event +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) - +UPDATER_URL = 'https://updater.home-assistant.io/' DOMAIN = 'updater' - ENTITY_ID = 'updater.updater' +ATTR_RELEASE_NOTES = 'release_notes' +UPDATER_UUID_FILE = '.uuid' +CONF_OPT_OUT = 'opt_out' + +REQUIREMENTS = ['distro==1.0.0'] + +CONFIG_SCHEMA = vol.Schema({DOMAIN: { + vol.Optional(CONF_OPT_OUT, default=False): cv.boolean +}}, extra=vol.ALLOW_EXTRA) -PYPI_URL = 'https://pypi.python.org/pypi/homeassistant/json' -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({}), -}, 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): """Setup the updater component.""" 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 - def check_newest_version(_=None): - """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'}) + huuid = None if config.get(CONF_OPT_OUT) else _load_uuid(hass) + # Update daily, start 1 hour after startup + _dt = datetime.now() + timedelta(hours=1) event.track_time_change( - hass, check_newest_version, hour=[0, 12], minute=0, second=0) - - check_newest_version() + hass, lambda _: check_newest_version(hass, huuid), + hour=_dt.hour, minute=_dt.minute, second=_dt.second) return True -def get_newest_version(): - """Get the newest Home Assistant version from PyPI.""" - try: - req = requests.get(PYPI_URL) +def check_newest_version(hass, huuid): + """Check if a new version is available and report if one is.""" + newest, releasenotes = get_newest_version(huuid) - 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: - _LOGGER.exception("Could not contact PyPI to check for updates") + _LOGGER.exception('Could not contact HASS Update to check for updates') return None except ValueError: - _LOGGER.exception("Received invalid response from PyPI") + _LOGGER.exception('Received invalid response from HASS Update') return None except KeyError: - _LOGGER.exception("Response from PyPI did not include version") + _LOGGER.exception('Response from HASS Update did not include version') return None diff --git a/requirements_all.txt b/requirements_all.txt index 03f35a03a37..4d05b474fe7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -79,6 +79,9 @@ concord232==0.14 # homeassistant.components.media_player.directv directpy==0.1 +# homeassistant.components.updater +distro==1.0.0 + # homeassistant.components.notify.xmpp dnspython3==1.15.0 diff --git a/tests/components/test_updater.py b/tests/components/test_updater.py index ec958a0d264..74fc7fc8cd4 100644 --- a/tests/components/test_updater.py +++ b/tests/components/test_updater.py @@ -1,13 +1,16 @@ """The tests for the Updater component.""" +from datetime import datetime, timedelta import unittest from unittest.mock import patch +import os import requests from homeassistant.bootstrap import setup_component 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' @@ -18,65 +21,93 @@ MOCK_CURRENT_VERSION = '10.0' class TestUpdater(unittest.TestCase): """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.""" self.hass = get_test_home_assistant() - def tearDown(self): # pylint: disable=invalid-name + def teardown_method(self, _): """Stop everything that was started.""" self.hass.stop() @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.""" - mock_get_newest_version.return_value = NEW_VERSION + mock_get_newest_version.return_value = (NEW_VERSION, '') updater.CURRENT_VERSION = MOCK_CURRENT_VERSION - self.assertTrue(setup_component(self.hass, updater.DOMAIN, { - 'updater': {} - })) + with assert_setup_component(1) as config: + setup_component(self.hass, updater.DOMAIN, {updater.DOMAIN: {}}) + _dt = datetime.now() + timedelta(hours=1) + assert config['updater'] == {'opt_out': False} + + 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.get_newest_version') - def test_no_entity_on_same_version(self, mock_get_newest_version): + 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 + mock_get_newest_version.return_value = (MOCK_CURRENT_VERSION, '') updater.CURRENT_VERSION = MOCK_CURRENT_VERSION - self.assertTrue(setup_component(self.hass, updater.DOMAIN, { - 'updater': {} - })) + 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 - - fire_time_changed( - self.hass, dt_util.utcnow().replace(hour=0, minute=0, second=0)) + mock_get_newest_version.return_value = (NEW_VERSION, '') - self.hass.block_till_done() + 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.get') - def test_errors_while_fetching_new_version(self, mock_get): + @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.""" 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 - self.assertIsNone(updater.get_newest_version()) + self.assertIsNone(updater.get_newest_version(uuid)) 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): """Test if the updater component is disabled on dev.""" updater.CURRENT_VERSION = MOCK_CURRENT_VERSION + 'dev' - self.assertFalse(setup_component(self.hass, updater.DOMAIN, { - 'updater': {} - })) + with assert_setup_component(1) as config: + 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) -- GitLab