Updater component is always available and shows on/off depending on whether an update is available or not (#25418)

* Updater Component is always available and shows on/off wether an update is available

* Use == instead of is to compare strings

* Edit log message  when local version is newer

* One more commit to trigger CI

* Add binary sensor

* Remove ATTR

* Use dispatcher

* Use callback instead of async

* Make flake happy

* Fix callback

* discover binary sensor

* flake

* Fix discovery

* prepared tests, TODO

* Fix tests

* Test release notes

* Add one more test

* Add another test

* Add docstring

* Revert "Add another test"

This reverts commit 3f896a4e3b.

* Remove unused file

* Update docstrings

* mock time

* Test renaming entity

* Add test_rename_entity

* Improve test_rename_entity
This commit is contained in:
Santobert 2019-08-08 00:28:22 +02:00 committed by Paulus Schoutsen
parent 1739f50b59
commit c3455efc11
3 changed files with 230 additions and 38 deletions

View File

@ -12,22 +12,25 @@ import aiohttp
import async_timeout
import voluptuous as vol
from homeassistant.const import ATTR_FRIENDLY_NAME, __version__ as current_version
from homeassistant.const import __version__ as current_version
from homeassistant.helpers import event
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers import discovery
from homeassistant.helpers.dispatcher import async_dispatcher_send
import homeassistant.helpers.config_validation as cv
import homeassistant.util.dt as dt_util
_LOGGER = logging.getLogger(__name__)
ATTR_RELEASE_NOTES = "release_notes"
ATTR_NEWEST_VERSION = "newest_version"
CONF_REPORTING = "reporting"
CONF_COMPONENT_REPORTING = "include_used_components"
DOMAIN = "updater"
ENTITY_ID = "updater.updater"
DISPATCHER_REMOTE_UPDATE = "updater_remote_update"
UPDATER_URL = "https://updater.home-assistant.io/"
UPDATER_UUID_FILE = ".uuid"
@ -47,6 +50,16 @@ RESPONSE_SCHEMA = vol.Schema(
)
class Updater:
"""Updater class for data exchange."""
def __init__(self, update_available: bool, newest_version: str, release_notes: str):
"""Initialize attributes."""
self.update_available = update_available
self.release_notes = release_notes
self.newest_version = newest_version
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:
@ -73,6 +86,10 @@ async def async_setup(hass, config):
# This component only makes sense in release versions
_LOGGER.info("Running on 'dev', only analytics will be submitted")
hass.async_create_task(
discovery.async_load_platform(hass, "binary_sensor", DOMAIN, {}, config)
)
config = config.get(DOMAIN, {})
if config.get(CONF_REPORTING):
huuid = await hass.async_add_job(_load_uuid, hass)
@ -88,7 +105,7 @@ async def async_setup(hass, config):
if result is None:
return
newest, releasenotes = result
newest, release_notes = result
# Skip on dev
if newest is None or "dev" in current_version:
@ -99,18 +116,17 @@ async def async_setup(hass, config):
newest = hass.components.hassio.get_homeassistant_version()
# Validate version
update_available = False
if StrictVersion(newest) > StrictVersion(current_version):
_LOGGER.info("The latest available version is %s", newest)
hass.states.async_set(
ENTITY_ID,
newest,
{
ATTR_FRIENDLY_NAME: "Update Available",
ATTR_RELEASE_NOTES: releasenotes,
},
)
_LOGGER.info("The latest available version of Home Assistant is %s", newest)
update_available = True
elif StrictVersion(newest) == StrictVersion(current_version):
_LOGGER.info("You are on the latest version (%s) of Home Assistant", newest)
elif StrictVersion(newest) < StrictVersion(current_version):
_LOGGER.debug("Local version is newer than the latest version (%s)", newest)
updater = Updater(update_available, newest, release_notes)
async_dispatcher_send(hass, DISPATCHER_REMOTE_UPDATE, updater)
# Update daily, start 1 hour after startup
_dt = dt_util.utcnow() + timedelta(hours=1)
@ -151,7 +167,7 @@ async def get_newest_version(hass, huuid, include_components):
info_object,
)
except (asyncio.TimeoutError, aiohttp.ClientError):
_LOGGER.error("Could not contact Home Assistant Update to check " "for updates")
_LOGGER.error("Could not contact Home Assistant Update to check for updates")
return None
try:

View File

@ -0,0 +1,81 @@
"""Support for Home Assistant Updater binary sensors."""
from homeassistant.core import callback
from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from . import ATTR_NEWEST_VERSION, ATTR_RELEASE_NOTES, DISPATCHER_REMOTE_UPDATE, Updater
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up the updater binary sensors."""
async_add_entities([UpdaterBinary()])
class UpdaterBinary(BinarySensorDevice):
"""Representation of an updater binary sensor."""
def __init__(self):
"""Initialize the binary sensor."""
self._update_available = None
self._release_notes = None
self._newest_version = None
self._unsub_dispatcher = None
@property
def name(self) -> str:
"""Return the name of the binary sensor, if any."""
return "Updater"
@property
def unique_id(self) -> str:
"""Return a unique ID."""
return "updater"
@property
def is_on(self) -> bool:
"""Return true if the binary sensor is on."""
return self._update_available
@property
def available(self) -> bool:
"""Return True if entity is available."""
return self._update_available is not None
@property
def should_poll(self) -> bool:
"""Return True if entity has to be polled for state."""
return False
@property
def device_state_attributes(self) -> dict:
"""Return the optional state attributes."""
data = super().device_state_attributes
if data is None:
data = {}
if self._release_notes:
data[ATTR_RELEASE_NOTES] = self._release_notes
if self._newest_version:
data[ATTR_NEWEST_VERSION] = self._newest_version
return data
async def async_added_to_hass(self):
"""Register update dispatcher."""
@callback
def async_state_update(updater: Updater):
"""Update callback."""
self._newest_version = updater.newest_version
self._release_notes = updater.release_notes
self._update_available = updater.update_available
self.async_schedule_update_ha_state()
self._unsub_dispatcher = async_dispatcher_connect(
self.hass, DISPATCHER_REMOTE_UPDATE, async_state_update
)
async def async_will_remove_from_hass(self):
"""Register update dispatcher."""
if self._unsub_dispatcher is not None:
self._unsub_dispatcher()
self._unsub_dispatcher = None

View File

@ -21,6 +21,7 @@ MOCK_DEV_VERSION = "10.0.dev0"
MOCK_HUUID = "abcdefg"
MOCK_RESPONSE = {"version": "0.15", "release-notes": "https://home-assistant.io"}
MOCK_CONFIG = {updater.DOMAIN: {"reporting": True}}
RELEASE_NOTES = "test release notes"
@pytest.fixture(autouse=True)
@ -44,56 +45,138 @@ def mock_get_uuid():
yield mock
@pytest.fixture
def mock_utcnow():
"""Fixture to mock utcnow."""
with patch("homeassistant.components.updater.dt_util.utcnow") as mock:
yield mock
@asyncio.coroutine
def test_new_version_shows_entity_after_hour(
hass, mock_get_uuid, mock_get_newest_version
):
"""Test if new entity is created if new version is available."""
def test_new_version_shows_entity_startup(hass, mock_get_uuid, mock_get_newest_version):
"""Test if binary sensor is unavailable at first."""
mock_get_uuid.return_value = MOCK_HUUID
mock_get_newest_version.return_value = mock_coro((NEW_VERSION, ""))
mock_get_newest_version.return_value = mock_coro((NEW_VERSION, RELEASE_NOTES))
res = yield from async_setup_component(hass, updater.DOMAIN, {updater.DOMAIN: {}})
assert res, "Updater failed to set up"
with patch("homeassistant.components.updater.current_version", MOCK_VERSION):
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(hours=1))
yield from hass.async_block_till_done()
assert hass.states.is_state(updater.ENTITY_ID, NEW_VERSION)
yield from hass.async_block_till_done()
assert hass.states.is_state("binary_sensor.updater", "unavailable")
assert "newest_version" not in hass.states.get("binary_sensor.updater").attributes
assert "release_notes" not in hass.states.get("binary_sensor.updater").attributes
@asyncio.coroutine
def test_same_version_not_show_entity(hass, mock_get_uuid, mock_get_newest_version):
"""Test if new entity is created if new version is available."""
def test_rename_entity(hass, mock_get_uuid, mock_get_newest_version):
"""Test if renaming the binary sensor works correctly."""
mock_get_uuid.return_value = MOCK_HUUID
mock_get_newest_version.return_value = mock_coro((NEW_VERSION, RELEASE_NOTES))
now = dt_util.utcnow()
later = now + timedelta(hours=1)
mock_utcnow.return_value = now
res = yield from async_setup_component(hass, updater.DOMAIN, {updater.DOMAIN: {}})
assert res, "Updater failed to set up"
yield from hass.async_block_till_done()
assert hass.states.is_state("binary_sensor.updater", "unavailable")
assert hass.states.get("binary_sensor.new_entity_id") is None
entity_registry = yield from hass.helpers.entity_registry.async_get_registry()
entity_registry.async_update_entity(
"binary_sensor.updater", new_entity_id="binary_sensor.new_entity_id"
)
yield from hass.async_block_till_done()
assert hass.states.is_state("binary_sensor.new_entity_id", "unavailable")
assert hass.states.get("binary_sensor.updater") is None
with patch("homeassistant.components.updater.current_version", MOCK_VERSION):
async_fire_time_changed(hass, later)
yield from hass.async_block_till_done()
assert hass.states.is_state("binary_sensor.new_entity_id", "on")
assert hass.states.get("binary_sensor.updater") is None
@asyncio.coroutine
def test_new_version_shows_entity_true(hass, mock_get_uuid, mock_get_newest_version):
"""Test if sensor is true if new version is available."""
mock_get_uuid.return_value = MOCK_HUUID
mock_get_newest_version.return_value = mock_coro((NEW_VERSION, RELEASE_NOTES))
now = dt_util.utcnow()
later = now + timedelta(hours=1)
mock_utcnow.return_value = now
res = yield from async_setup_component(hass, updater.DOMAIN, {updater.DOMAIN: {}})
assert res, "Updater failed to set up"
yield from hass.async_block_till_done()
with patch("homeassistant.components.updater.current_version", MOCK_VERSION):
async_fire_time_changed(hass, later)
yield from hass.async_block_till_done()
assert hass.states.is_state("binary_sensor.updater", "on")
assert (
hass.states.get("binary_sensor.updater").attributes["newest_version"]
== NEW_VERSION
)
assert (
hass.states.get("binary_sensor.updater").attributes["release_notes"]
== RELEASE_NOTES
)
@asyncio.coroutine
def test_same_version_shows_entity_false(hass, mock_get_uuid, mock_get_newest_version):
"""Test if sensor is false if no new version is available."""
mock_get_uuid.return_value = MOCK_HUUID
mock_get_newest_version.return_value = mock_coro((MOCK_VERSION, ""))
now = dt_util.utcnow()
later = now + timedelta(hours=1)
mock_utcnow.return_value = now
res = yield from async_setup_component(hass, updater.DOMAIN, {updater.DOMAIN: {}})
assert res, "Updater failed to set up"
yield from hass.async_block_till_done()
with patch("homeassistant.components.updater.current_version", MOCK_VERSION):
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(hours=1))
async_fire_time_changed(hass, later)
yield from hass.async_block_till_done()
assert hass.states.get(updater.ENTITY_ID) is None
assert hass.states.is_state("binary_sensor.updater", "off")
assert (
hass.states.get("binary_sensor.updater").attributes["newest_version"]
== MOCK_VERSION
)
assert "release_notes" not in hass.states.get("binary_sensor.updater").attributes
@asyncio.coroutine
def test_disable_reporting(hass, mock_get_uuid, mock_get_newest_version):
"""Test if new entity is created if new version is available."""
"""Test we do not gather analytics when disable reporting is active."""
mock_get_uuid.return_value = MOCK_HUUID
mock_get_newest_version.return_value = mock_coro((MOCK_VERSION, ""))
now = dt_util.utcnow()
later = now + timedelta(hours=1)
mock_utcnow.return_value = now
res = yield from async_setup_component(
hass, updater.DOMAIN, {updater.DOMAIN: {"reporting": False}}
)
assert res, "Updater failed to set up"
yield from hass.async_block_till_done()
with patch("homeassistant.components.updater.current_version", MOCK_VERSION):
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(hours=1))
async_fire_time_changed(hass, later)
yield from hass.async_block_till_done()
assert hass.states.get(updater.ENTITY_ID) is None
assert hass.states.is_state("binary_sensor.updater", "off")
res = yield from updater.get_newest_version(hass, MOCK_HUUID, MOCK_CONFIG)
call = mock_get_newest_version.mock_calls[0][1]
assert call[0] is hass
@ -114,7 +197,7 @@ def test_get_newest_version_no_analytics_when_no_huuid(hass, aioclient_mock):
@asyncio.coroutine
def test_get_newest_version_analytics_when_huuid(hass, aioclient_mock):
"""Test we do not gather analytics when no huuid is passed in."""
"""Test we gather analytics when huuid is passed in."""
aioclient_mock.post(updater.UPDATER_URL, json=MOCK_RESPONSE)
with patch(
@ -127,7 +210,7 @@ def test_get_newest_version_analytics_when_huuid(hass, aioclient_mock):
@asyncio.coroutine
def test_error_fetching_new_version_timeout(hass):
"""Test we do not gather analytics when no huuid is passed in."""
"""Test we handle timeout error while fetching new version."""
with patch(
"homeassistant.helpers.system_info.async_get_system_info",
Mock(return_value=mock_coro({"fake": "bla"})),
@ -138,7 +221,7 @@ def test_error_fetching_new_version_timeout(hass):
@asyncio.coroutine
def test_error_fetching_new_version_bad_json(hass, aioclient_mock):
"""Test we do not gather analytics when no huuid is passed in."""
"""Test we handle json error while fetching new version."""
aioclient_mock.post(updater.UPDATER_URL, text="not json")
with patch(
@ -151,7 +234,7 @@ def test_error_fetching_new_version_bad_json(hass, aioclient_mock):
@asyncio.coroutine
def test_error_fetching_new_version_invalid_response(hass, aioclient_mock):
"""Test we do not gather analytics when no huuid is passed in."""
"""Test we handle response error while fetching new version."""
aioclient_mock.post(
updater.UPDATER_URL,
json={
@ -172,17 +255,29 @@ def test_error_fetching_new_version_invalid_response(hass, aioclient_mock):
def test_new_version_shows_entity_after_hour_hassio(
hass, mock_get_uuid, mock_get_newest_version
):
"""Test if new entity is created if new version is available / hass.io."""
"""Test if binary sensor gets updated if new version is available / hass.io."""
mock_get_uuid.return_value = MOCK_HUUID
mock_get_newest_version.return_value = mock_coro((NEW_VERSION, ""))
mock_get_newest_version.return_value = mock_coro((NEW_VERSION, RELEASE_NOTES))
mock_component(hass, "hassio")
hass.data["hassio_hass_version"] = "999.0"
now = dt_util.utcnow()
later = now + timedelta(hours=1)
mock_utcnow.return_value = now
res = yield from async_setup_component(hass, updater.DOMAIN, {updater.DOMAIN: {}})
assert res, "Updater failed to set up"
yield from hass.async_block_till_done()
with patch("homeassistant.components.updater.current_version", MOCK_VERSION):
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(hours=1))
async_fire_time_changed(hass, later)
yield from hass.async_block_till_done()
assert hass.states.is_state(updater.ENTITY_ID, "999.0")
assert hass.states.is_state("binary_sensor.updater", "on")
assert (
hass.states.get("binary_sensor.updater").attributes["newest_version"] == "999.0"
)
assert (
hass.states.get("binary_sensor.updater").attributes["release_notes"]
== RELEASE_NOTES
)