Skip to content
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 29 additions & 13 deletions homeassistant/components/updater/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,22 +12,25 @@
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"
Expand All @@ -47,6 +50,16 @@
)


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:
Expand All @@ -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)
Expand All @@ -88,7 +105,7 @@ async def check_new_version(now):
if result is None:
return

newest, releasenotes = result
newest, release_notes = result

# Skip on dev
if newest is None or "dev" in current_version:
Expand All @@ -99,18 +116,17 @@ async def check_new_version(now):
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)
Comment thread
Santobert marked this conversation as resolved.

# Update daily, start 1 hour after startup
_dt = dt_util.utcnow() + timedelta(hours=1)
Expand Down Expand Up @@ -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:
Expand Down
81 changes: 81 additions & 0 deletions homeassistant/components/updater/binary_sensor.py
Original file line number Diff line number Diff line change
@@ -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
120 changes: 98 additions & 22 deletions tests/components/updater/test_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
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)
Expand All @@ -44,56 +45,118 @@ 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, RELEASE_NOTES))

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 "newest_version" not in hass.states.get("binary_sensor.updater").attributes
assert "release_notes" not in hass.states.get("binary_sensor.updater").attributes

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.is_state("binary_sensor.updater", "unavailable") is False
Comment thread
Santobert marked this conversation as resolved.
Outdated

Comment thread
Santobert marked this conversation as resolved.

@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, ""))
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, 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, NEW_VERSION)
assert hass.states.is_state("binary_sensor.updater", "on")
assert (
hass.states.get("binary_sensor.updater").attributes["newest_version"]
== NEW_VERSION
)
Comment thread
Santobert marked this conversation as resolved.
assert (
hass.states.get("binary_sensor.updater").attributes["release_notes"]
== RELEASE_NOTES
)


@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_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
Expand All @@ -114,7 +177,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(
Expand All @@ -127,7 +190,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"})),
Expand All @@ -138,7 +201,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(
Expand All @@ -151,7 +214,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={
Expand All @@ -172,17 +235,30 @@ 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
)