mirror of https://github.com/home-assistant/core
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:
parent
1739f50b59
commit
c3455efc11
|
@ -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:
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
)
|
||||
|
|
Loading…
Reference in New Issue