Merge pull request #24465 from home-assistant/rc

0.94.2
This commit is contained in:
Paulus Schoutsen 2019-06-11 08:54:29 -07:00 committed by GitHub
commit c2218e8a64
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 375 additions and 139 deletions

View File

@ -14,6 +14,8 @@ from .const import CONF_MODEL, DOMAIN
from .device import get_device from .device import get_device
from .errors import AlreadyConfigured, AuthenticationRequired, CannotConnect from .errors import AlreadyConfigured, AuthenticationRequired, CannotConnect
AXIS_OUI = {'00408C', 'ACCC8E', 'B8A44F'}
CONFIG_FILE = 'axis.conf' CONFIG_FILE = 'axis.conf'
EVENT_TYPES = ['motion', 'vmd3', 'pir', 'sound', EVENT_TYPES = ['motion', 'vmd3', 'pir', 'sound',
@ -151,10 +153,14 @@ class AxisFlowHandler(config_entries.ConfigFlow):
This flow is triggered by the discovery component. This flow is triggered by the discovery component.
""" """
serialnumber = discovery_info['properties']['macaddress']
if serialnumber[:6] not in AXIS_OUI:
return self.async_abort(reason='not_axis_device')
if discovery_info[CONF_HOST].startswith('169.254'): if discovery_info[CONF_HOST].startswith('169.254'):
return self.async_abort(reason='link_local_address') return self.async_abort(reason='link_local_address')
serialnumber = discovery_info['properties']['macaddress']
# pylint: disable=unsupported-assignment-operation # pylint: disable=unsupported-assignment-operation
self.context['macaddress'] = serialnumber self.context['macaddress'] = serialnumber

View File

@ -21,7 +21,8 @@
"abort": { "abort": {
"already_configured": "Device is already configured", "already_configured": "Device is already configured",
"bad_config_file": "Bad data from config file", "bad_config_file": "Bad data from config file",
"link_local_address": "Link local addresses are not supported" "link_local_address": "Link local addresses are not supported",
"not_axis_device": "Discovered device not an Axis device"
} }
} }
} }

View File

@ -56,7 +56,7 @@ async def websocket_update_config(hass, connection, msg):
data.pop('type') data.pop('type')
try: try:
await hass.config.update(**data) await hass.config.async_update(**data)
connection.send_result(msg['id']) connection.send_result(msg['id'])
except ValueError as err: except ValueError as err:
connection.send_error( connection.send_error(

View File

@ -9,7 +9,6 @@ from pydeconz.utils import (
async_discovery, async_get_api_key, async_get_bridgeid) async_discovery, async_get_api_key, async_get_bridgeid)
from homeassistant import config_entries from homeassistant import config_entries
from homeassistant.components.ssdp import ATTR_MANUFACTURERURL, ATTR_SERIAL
from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.helpers import aiohttp_client from homeassistant.helpers import aiohttp_client
@ -154,6 +153,9 @@ class DeconzFlowHandler(config_entries.ConfigFlow):
async def async_step_ssdp(self, discovery_info): async def async_step_ssdp(self, discovery_info):
"""Handle a discovered deCONZ bridge.""" """Handle a discovered deCONZ bridge."""
from homeassistant.components.ssdp import (
ATTR_MANUFACTURERURL, ATTR_SERIAL)
if discovery_info[ATTR_MANUFACTURERURL] != DECONZ_MANUFACTURERURL: if discovery_info[ATTR_MANUFACTURERURL] != DECONZ_MANUFACTURERURL:
return self.async_abort(reason='not_deconz_bridge') return self.async_abort(reason='not_deconz_bridge')

View File

@ -4,7 +4,7 @@
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/components/deconz", "documentation": "https://www.home-assistant.io/components/deconz",
"requirements": [ "requirements": [
"pydeconz==59" "pydeconz==60"
], ],
"ssdp": { "ssdp": {
"manufacturer": [ "manufacturer": [

View File

@ -11,19 +11,21 @@ from homeassistant.const import HTTP_UNPROCESSABLE_ENTITY, \
from homeassistant.helpers import config_entry_flow from homeassistant.helpers import config_entry_flow
from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER
from .const import DOMAIN from .const import (
DOMAIN,
ATTR_ALTITUDE,
ATTR_ACCURACY,
ATTR_ACTIVITY,
ATTR_DEVICE,
ATTR_DIRECTION,
ATTR_PROVIDER,
ATTR_SPEED,
)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
TRACKER_UPDATE = '{}_tracker_update'.format(DOMAIN) TRACKER_UPDATE = '{}_tracker_update'.format(DOMAIN)
ATTR_ALTITUDE = 'altitude'
ATTR_ACCURACY = 'accuracy'
ATTR_ACTIVITY = 'activity'
ATTR_DEVICE = 'device'
ATTR_DIRECTION = 'direction'
ATTR_PROVIDER = 'provider'
ATTR_SPEED = 'speed'
DEFAULT_ACCURACY = 200 DEFAULT_ACCURACY = 200
DEFAULT_BATTERY = -1 DEFAULT_BATTERY = -1

View File

@ -1,3 +1,11 @@
"""Const for GPSLogger.""" """Const for GPSLogger."""
DOMAIN = 'gpslogger' DOMAIN = 'gpslogger'
ATTR_ALTITUDE = 'altitude'
ATTR_ACCURACY = 'accuracy'
ATTR_ACTIVITY = 'activity'
ATTR_DEVICE = 'device'
ATTR_DIRECTION = 'direction'
ATTR_PROVIDER = 'provider'
ATTR_SPEED = 'speed'

View File

@ -2,14 +2,29 @@
import logging import logging
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.const import (
ATTR_BATTERY_LEVEL,
ATTR_GPS_ACCURACY,
ATTR_LATITUDE,
ATTR_LONGITUDE,
)
from homeassistant.components.device_tracker import SOURCE_TYPE_GPS from homeassistant.components.device_tracker import SOURCE_TYPE_GPS
from homeassistant.components.device_tracker.config_entry import ( from homeassistant.components.device_tracker.config_entry import (
DeviceTrackerEntity DeviceTrackerEntity
) )
from homeassistant.helpers import device_registry
from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.restore_state import RestoreEntity
from homeassistant.helpers.typing import HomeAssistantType from homeassistant.helpers.typing import HomeAssistantType
from . import DOMAIN as GPL_DOMAIN, TRACKER_UPDATE from . import DOMAIN as GPL_DOMAIN, TRACKER_UPDATE
from .const import (
ATTR_ACTIVITY,
ATTR_ALTITUDE,
ATTR_DIRECTION,
ATTR_PROVIDER,
ATTR_SPEED,
)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -32,8 +47,27 @@ async def async_setup_entry(hass: HomeAssistantType, entry,
hass.data[GPL_DOMAIN]['unsub_device_tracker'][entry.entry_id] = \ hass.data[GPL_DOMAIN]['unsub_device_tracker'][entry.entry_id] = \
async_dispatcher_connect(hass, TRACKER_UPDATE, _receive_data) async_dispatcher_connect(hass, TRACKER_UPDATE, _receive_data)
# Restore previously loaded devices
dev_reg = await device_registry.async_get_registry(hass)
dev_ids = {
identifier[1]
for device in dev_reg.devices.values()
for identifier in device.identifiers
if identifier[0] == GPL_DOMAIN
}
if not dev_ids:
return
class GPSLoggerEntity(DeviceTrackerEntity): entities = []
for dev_id in dev_ids:
hass.data[GPL_DOMAIN]['devices'].add(dev_id)
entity = GPSLoggerEntity(dev_id, None, None, None, None)
entities.append(entity)
async_add_entities(entities)
class GPSLoggerEntity(DeviceTrackerEntity, RestoreEntity):
"""Represent a tracked device.""" """Represent a tracked device."""
def __init__( def __init__(
@ -102,11 +136,46 @@ class GPSLoggerEntity(DeviceTrackerEntity):
async def async_added_to_hass(self): async def async_added_to_hass(self):
"""Register state update callback.""" """Register state update callback."""
await super().async_added_to_hass()
self._unsub_dispatcher = async_dispatcher_connect( self._unsub_dispatcher = async_dispatcher_connect(
self.hass, TRACKER_UPDATE, self._async_receive_data) self.hass, TRACKER_UPDATE, self._async_receive_data)
# don't restore if we got created with data
if self._location is not None:
return
state = await self.async_get_last_state()
if state is None:
self._location = (None, None)
self._accuracy = None
self._attributes = {
ATTR_ALTITUDE: None,
ATTR_ACTIVITY: None,
ATTR_DIRECTION: None,
ATTR_PROVIDER: None,
ATTR_SPEED: None,
}
self._battery = None
return
attr = state.attributes
self._location = (
attr.get(ATTR_LATITUDE),
attr.get(ATTR_LONGITUDE),
)
self._accuracy = attr.get(ATTR_GPS_ACCURACY)
self._attributes = {
ATTR_ALTITUDE: attr.get(ATTR_ALTITUDE),
ATTR_ACTIVITY: attr.get(ATTR_ACTIVITY),
ATTR_DIRECTION: attr.get(ATTR_DIRECTION),
ATTR_PROVIDER: attr.get(ATTR_PROVIDER),
ATTR_SPEED: attr.get(ATTR_SPEED),
}
self._battery = attr.get(ATTR_BATTERY_LEVEL)
async def async_will_remove_from_hass(self): async def async_will_remove_from_hass(self):
"""Clean up after entity before removal.""" """Clean up after entity before removal."""
await super().async_will_remove_from_hass()
self._unsub_dispatcher() self._unsub_dispatcher()
@callback @callback

View File

@ -9,7 +9,8 @@ from homeassistant.auth.const import GROUP_ID_ADMIN
from homeassistant.components.homeassistant import SERVICE_CHECK_CONFIG from homeassistant.components.homeassistant import SERVICE_CHECK_CONFIG
import homeassistant.config as conf_util import homeassistant.config as conf_util
from homeassistant.const import ( from homeassistant.const import (
ATTR_NAME, SERVICE_HOMEASSISTANT_RESTART, SERVICE_HOMEASSISTANT_STOP) ATTR_NAME, SERVICE_HOMEASSISTANT_RESTART, SERVICE_HOMEASSISTANT_STOP,
EVENT_CORE_CONFIG_UPDATE)
from homeassistant.core import DOMAIN as HASS_DOMAIN, callback from homeassistant.core import DOMAIN as HASS_DOMAIN, callback
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
@ -194,8 +195,13 @@ async def async_setup(hass, config):
await hassio.update_hass_api(config.get('http', {}), refresh_token.token) await hassio.update_hass_api(config.get('http', {}), refresh_token.token)
if 'homeassistant' in config: async def push_config(_):
await hassio.update_hass_timezone(config['homeassistant']) """Push core config to Hass.io."""
await hassio.update_hass_timezone(str(hass.config.time_zone))
hass.bus.async_listen(EVENT_CORE_CONFIG_UPDATE, push_config)
await push_config(None)
async def async_service_handler(service): async def async_service_handler(service):
"""Handle service calls for Hass.io.""" """Handle service calls for Hass.io."""

View File

@ -11,7 +11,7 @@ from homeassistant.components.http import (
CONF_SERVER_PORT, CONF_SERVER_PORT,
CONF_SSL_CERTIFICATE, CONF_SSL_CERTIFICATE,
) )
from homeassistant.const import CONF_TIME_ZONE, SERVER_PORT from homeassistant.const import SERVER_PORT
from .const import X_HASSIO from .const import X_HASSIO
@ -140,13 +140,13 @@ class HassIO:
payload=options) payload=options)
@_api_bool @_api_bool
def update_hass_timezone(self, core_config): def update_hass_timezone(self, timezone):
"""Update Home-Assistant timezone data on Hass.io. """Update Home-Assistant timezone data on Hass.io.
This method return a coroutine. This method return a coroutine.
""" """
return self.send_command("/supervisor/options", payload={ return self.send_command("/supervisor/options", payload={
'timezone': core_config.get(CONF_TIME_ZONE) 'timezone': timezone
}) })
async def send_command(self, command, method="post", payload=None, async def send_command(self, command, method="post", payload=None,

View File

@ -8,7 +8,6 @@ import async_timeout
import voluptuous as vol import voluptuous as vol
from homeassistant import config_entries from homeassistant import config_entries
from homeassistant.components.ssdp import ATTR_MANUFACTURERURL
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.helpers import aiohttp_client from homeassistant.helpers import aiohttp_client
@ -146,6 +145,8 @@ class HueFlowHandler(config_entries.ConfigFlow):
This flow is triggered by the SSDP component. It will check if the This flow is triggered by the SSDP component. It will check if the
host is already configured and delegate to the import step if not. host is already configured and delegate to the import step if not.
""" """
from homeassistant.components.ssdp import ATTR_MANUFACTURERURL
if discovery_info[ATTR_MANUFACTURERURL] != HUE_MANUFACTURERURL: if discovery_info[ATTR_MANUFACTURERURL] != HUE_MANUFACTURERURL:
return self.async_abort(reason='not_hue_bridge') return self.async_abort(reason='not_hue_bridge')

View File

@ -3,10 +3,12 @@ import logging
import voluptuous as vol import voluptuous as vol
from homeassistant.core import callback
from homeassistant.loader import bind_hass from homeassistant.loader import bind_hass
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.const import ( from homeassistant.const import (
CONF_NAME, CONF_LATITUDE, CONF_LONGITUDE, CONF_ICON, CONF_RADIUS) CONF_NAME, CONF_LATITUDE, CONF_LONGITUDE, CONF_ICON, CONF_RADIUS,
EVENT_CORE_CONFIG_UPDATE)
from homeassistant.helpers import config_per_platform from homeassistant.helpers import config_per_platform
from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.entity import async_generate_entity_id
from homeassistant.util import slugify from homeassistant.util import slugify
@ -90,13 +92,25 @@ async def async_setup(hass, config):
hass.async_create_task(zone.async_update_ha_state()) hass.async_create_task(zone.async_update_ha_state())
entities.add(zone.entity_id) entities.add(zone.entity_id)
if ENTITY_ID_HOME not in entities and HOME_ZONE not in zone_entries: if ENTITY_ID_HOME in entities or HOME_ZONE in zone_entries:
return True
zone = Zone(hass, hass.config.location_name, zone = Zone(hass, hass.config.location_name,
hass.config.latitude, hass.config.longitude, hass.config.latitude, hass.config.longitude,
DEFAULT_RADIUS, ICON_HOME, False) DEFAULT_RADIUS, ICON_HOME, False)
zone.entity_id = ENTITY_ID_HOME zone.entity_id = ENTITY_ID_HOME
hass.async_create_task(zone.async_update_ha_state()) hass.async_create_task(zone.async_update_ha_state())
@callback
def core_config_updated(_):
"""Handle core config updated."""
zone.name = hass.config.location_name
zone.latitude = hass.config.latitude
zone.longitude = hass.config.longitude
zone.async_write_ha_state()
hass.bus.async_listen(EVENT_CORE_CONFIG_UPDATE, core_config_updated)
return True return True

View File

@ -23,21 +23,18 @@ def in_zone(zone, latitude, longitude, radius=0) -> bool:
class Zone(Entity): class Zone(Entity):
"""Representation of a Zone.""" """Representation of a Zone."""
name = None
def __init__(self, hass, name, latitude, longitude, radius, icon, passive): def __init__(self, hass, name, latitude, longitude, radius, icon, passive):
"""Initialize the zone.""" """Initialize the zone."""
self.hass = hass self.hass = hass
self._name = name self.name = name
self._latitude = latitude self.latitude = latitude
self._longitude = longitude self.longitude = longitude
self._radius = radius self._radius = radius
self._icon = icon self._icon = icon
self._passive = passive self._passive = passive
@property
def name(self):
"""Return the name of the zone."""
return self._name
@property @property
def state(self): def state(self):
"""Return the state property really does nothing for a zone.""" """Return the state property really does nothing for a zone."""
@ -53,8 +50,8 @@ class Zone(Entity):
"""Return the state attributes of the zone.""" """Return the state attributes of the zone."""
data = { data = {
ATTR_HIDDEN: True, ATTR_HIDDEN: True,
ATTR_LATITUDE: self._latitude, ATTR_LATITUDE: self.latitude,
ATTR_LONGITUDE: self._longitude, ATTR_LONGITUDE: self.longitude,
ATTR_RADIUS: self._radius, ATTR_RADIUS: self._radius,
} }
if self._passive: if self._passive:

View File

@ -2,7 +2,7 @@
"""Constants used by Home Assistant components.""" """Constants used by Home Assistant components."""
MAJOR_VERSION = 0 MAJOR_VERSION = 0
MINOR_VERSION = 94 MINOR_VERSION = 94
PATCH_VERSION = '1' PATCH_VERSION = '2'
__short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION)
__version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION)
REQUIRED_PYTHON_VER = (3, 5, 3) REQUIRED_PYTHON_VER = (3, 5, 3)

View File

@ -1288,10 +1288,7 @@ class Config:
unit_system: Optional[str] = None, unit_system: Optional[str] = None,
location_name: Optional[str] = None, location_name: Optional[str] = None,
time_zone: Optional[str] = None) -> None: time_zone: Optional[str] = None) -> None:
"""Update the configuration from a dictionary. """Update the configuration from a dictionary."""
Async friendly.
"""
self.config_source = source self.config_source = source
if latitude is not None: if latitude is not None:
self.latitude = latitude self.latitude = latitude
@ -1309,11 +1306,8 @@ class Config:
if time_zone is not None: if time_zone is not None:
self.set_time_zone(time_zone) self.set_time_zone(time_zone)
async def update(self, **kwargs: Any) -> None: async def async_update(self, **kwargs: Any) -> None:
"""Update the configuration from a dictionary. """Update the configuration from a dictionary."""
Async friendly.
"""
self._update(source=SOURCE_STORAGE, **kwargs) self._update(source=SOURCE_STORAGE, **kwargs)
await self.async_store() await self.async_store()
self.hass.bus.async_fire( self.hass.bus.async_fire(

View File

@ -1,15 +1,18 @@
"""Helpers for listening to events.""" """Helpers for listening to events."""
from datetime import timedelta from datetime import timedelta
import functools as ft import functools as ft
from typing import Callable
import attr
from homeassistant.loader import bind_hass from homeassistant.loader import bind_hass
from homeassistant.helpers.sun import get_astral_event_next from homeassistant.helpers.sun import get_astral_event_next
from ..core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from ..const import ( from homeassistant.const import (
ATTR_NOW, EVENT_STATE_CHANGED, EVENT_TIME_CHANGED, MATCH_ALL, ATTR_NOW, EVENT_STATE_CHANGED, EVENT_TIME_CHANGED, MATCH_ALL,
SUN_EVENT_SUNRISE, SUN_EVENT_SUNSET) SUN_EVENT_SUNRISE, SUN_EVENT_SUNSET, EVENT_CORE_CONFIG_UPDATE)
from ..util import dt as dt_util from homeassistant.util import dt as dt_util
from ..util.async_ import run_callback_threadsafe from homeassistant.util.async_ import run_callback_threadsafe
# PyLint does not like the use of threaded_listener_factory # PyLint does not like the use of threaded_listener_factory
# pylint: disable=invalid-name # pylint: disable=invalid-name
@ -263,30 +266,71 @@ def async_track_time_interval(hass, action, interval):
track_time_interval = threaded_listener_factory(async_track_time_interval) track_time_interval = threaded_listener_factory(async_track_time_interval)
@attr.s
class SunListener:
"""Helper class to help listen to sun events."""
hass = attr.ib(type=HomeAssistant)
action = attr.ib(type=Callable)
event = attr.ib(type=str)
offset = attr.ib(type=timedelta)
_unsub_sun = attr.ib(default=None)
_unsub_config = attr.ib(default=None)
@callback
def async_attach(self):
"""Attach a sun listener."""
assert self._unsub_config is None
self._unsub_config = self.hass.bus.async_listen(
EVENT_CORE_CONFIG_UPDATE, self._handle_config_event)
self._listen_next_sun_event()
@callback
def async_detach(self):
"""Detach the sun listener."""
assert self._unsub_sun is not None
assert self._unsub_config is not None
self._unsub_sun()
self._unsub_sun = None
self._unsub_config()
self._unsub_config = None
@callback
def _listen_next_sun_event(self):
"""Set up the sun event listener."""
assert self._unsub_sun is None
self._unsub_sun = async_track_point_in_utc_time(
self.hass, self._handle_sun_event,
get_astral_event_next(self.hass, self.event, offset=self.offset)
)
@callback
def _handle_sun_event(self, _now):
"""Handle solar event."""
self._unsub_sun = None
self._listen_next_sun_event()
self.hass.async_run_job(self.action)
@callback
def _handle_config_event(self, _event):
"""Handle core config update."""
assert self._unsub_sun is not None
self._unsub_sun()
self._unsub_sun = None
self._listen_next_sun_event()
@callback @callback
@bind_hass @bind_hass
def async_track_sunrise(hass, action, offset=None): def async_track_sunrise(hass, action, offset=None):
"""Add a listener that will fire a specified offset from sunrise daily.""" """Add a listener that will fire a specified offset from sunrise daily."""
remove = None listener = SunListener(hass, action, SUN_EVENT_SUNRISE, offset)
listener.async_attach()
@callback return listener.async_detach
def sunrise_automation_listener(now):
"""Handle points in time to execute actions."""
nonlocal remove
remove = async_track_point_in_utc_time(
hass, sunrise_automation_listener, get_astral_event_next(
hass, SUN_EVENT_SUNRISE, offset=offset))
hass.async_run_job(action)
remove = async_track_point_in_utc_time(
hass, sunrise_automation_listener, get_astral_event_next(
hass, SUN_EVENT_SUNRISE, offset=offset))
def remove_listener():
"""Remove sunset listener."""
remove()
return remove_listener
track_sunrise = threaded_listener_factory(async_track_sunrise) track_sunrise = threaded_listener_factory(async_track_sunrise)
@ -296,26 +340,9 @@ track_sunrise = threaded_listener_factory(async_track_sunrise)
@bind_hass @bind_hass
def async_track_sunset(hass, action, offset=None): def async_track_sunset(hass, action, offset=None):
"""Add a listener that will fire a specified offset from sunset daily.""" """Add a listener that will fire a specified offset from sunset daily."""
remove = None listener = SunListener(hass, action, SUN_EVENT_SUNSET, offset)
listener.async_attach()
@callback return listener.async_detach
def sunset_automation_listener(now):
"""Handle points in time to execute actions."""
nonlocal remove
remove = async_track_point_in_utc_time(
hass, sunset_automation_listener, get_astral_event_next(
hass, SUN_EVENT_SUNSET, offset=offset))
hass.async_run_job(action)
remove = async_track_point_in_utc_time(
hass, sunset_automation_listener, get_astral_event_next(
hass, SUN_EVENT_SUNSET, offset=offset))
def remove_listener():
"""Remove sunset listener."""
remove()
return remove_listener
track_sunset = threaded_listener_factory(async_track_sunset) track_sunset = threaded_listener_factory(async_track_sunset)

View File

@ -1045,7 +1045,7 @@ pydaikin==1.4.6
pydanfossair==0.1.0 pydanfossair==0.1.0
# homeassistant.components.deconz # homeassistant.components.deconz
pydeconz==59 pydeconz==60
# homeassistant.components.zwave # homeassistant.components.zwave
pydispatcher==2.0.5 pydispatcher==2.0.5

View File

@ -230,7 +230,7 @@ pyHS100==0.3.5
pyblackbird==0.5 pyblackbird==0.5
# homeassistant.components.deconz # homeassistant.components.deconz
pydeconz==59 pydeconz==60
# homeassistant.components.zwave # homeassistant.components.zwave
pydispatcher==2.0.5 pydispatcher==2.0.5

View File

@ -169,7 +169,7 @@ async def test_zeroconf_flow(hass):
data={ data={
config_flow.CONF_HOST: '1.2.3.4', config_flow.CONF_HOST: '1.2.3.4',
config_flow.CONF_PORT: 80, config_flow.CONF_PORT: 80,
'properties': {'macaddress': '1234'} 'properties': {'macaddress': '00408C12345'}
}, },
context={'source': 'zeroconf'} context={'source': 'zeroconf'}
) )
@ -184,7 +184,7 @@ async def test_zeroconf_flow_known_device(hass):
This is legacy support from devices registered with configurator. This is legacy support from devices registered with configurator.
""" """
with patch('homeassistant.components.axis.config_flow.load_json', with patch('homeassistant.components.axis.config_flow.load_json',
return_value={'1234ABCD': { return_value={'00408C12345': {
config_flow.CONF_HOST: '2.3.4.5', config_flow.CONF_HOST: '2.3.4.5',
config_flow.CONF_USERNAME: 'user', config_flow.CONF_USERNAME: 'user',
config_flow.CONF_PASSWORD: 'pass', config_flow.CONF_PASSWORD: 'pass',
@ -208,7 +208,7 @@ async def test_zeroconf_flow_known_device(hass):
config_flow.CONF_HOST: '1.2.3.4', config_flow.CONF_HOST: '1.2.3.4',
config_flow.CONF_PORT: 80, config_flow.CONF_PORT: 80,
'hostname': 'name', 'hostname': 'name',
'properties': {'macaddress': '1234ABCD'} 'properties': {'macaddress': '00408C12345'}
}, },
context={'source': 'zeroconf'} context={'source': 'zeroconf'}
) )
@ -221,7 +221,7 @@ async def test_zeroconf_flow_already_configured(hass):
entry = MockConfigEntry( entry = MockConfigEntry(
domain=axis.DOMAIN, domain=axis.DOMAIN,
data={axis.CONF_DEVICE: {axis.config_flow.CONF_HOST: '1.2.3.4'}, data={axis.CONF_DEVICE: {axis.config_flow.CONF_HOST: '1.2.3.4'},
axis.config_flow.CONF_MAC: '1234ABCD'} axis.config_flow.CONF_MAC: '00408C12345'}
) )
entry.add_to_hass(hass) entry.add_to_hass(hass)
@ -233,7 +233,7 @@ async def test_zeroconf_flow_already_configured(hass):
config_flow.CONF_PASSWORD: 'pass', config_flow.CONF_PASSWORD: 'pass',
config_flow.CONF_PORT: 80, config_flow.CONF_PORT: 80,
'hostname': 'name', 'hostname': 'name',
'properties': {'macaddress': '1234ABCD'} 'properties': {'macaddress': '00408C12345'}
}, },
context={'source': 'zeroconf'} context={'source': 'zeroconf'}
) )
@ -242,11 +242,29 @@ async def test_zeroconf_flow_already_configured(hass):
assert result['reason'] == 'already_configured' assert result['reason'] == 'already_configured'
async def test_zeroconf_flow_ignore_non_axis_device(hass):
"""Test that zeroconf doesn't setup devices with link local addresses."""
result = await hass.config_entries.flow.async_init(
config_flow.DOMAIN,
data={
config_flow.CONF_HOST: '169.254.3.4',
'properties': {'macaddress': '01234567890'}
},
context={'source': 'zeroconf'}
)
assert result['type'] == 'abort'
assert result['reason'] == 'not_axis_device'
async def test_zeroconf_flow_ignore_link_local_address(hass): async def test_zeroconf_flow_ignore_link_local_address(hass):
"""Test that zeroconf doesn't setup devices with link local addresses.""" """Test that zeroconf doesn't setup devices with link local addresses."""
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
config_flow.DOMAIN, config_flow.DOMAIN,
data={config_flow.CONF_HOST: '169.254.3.4'}, data={
config_flow.CONF_HOST: '169.254.3.4',
'properties': {'macaddress': '00408C12345'}
},
context={'source': 'zeroconf'} context={'source': 'zeroconf'}
) )
@ -257,7 +275,7 @@ async def test_zeroconf_flow_ignore_link_local_address(hass):
async def test_zeroconf_flow_bad_config_file(hass): async def test_zeroconf_flow_bad_config_file(hass):
"""Test that zeroconf discovery with bad config files abort.""" """Test that zeroconf discovery with bad config files abort."""
with patch('homeassistant.components.axis.config_flow.load_json', with patch('homeassistant.components.axis.config_flow.load_json',
return_value={'1234ABCD': { return_value={'00408C12345': {
config_flow.CONF_HOST: '2.3.4.5', config_flow.CONF_HOST: '2.3.4.5',
config_flow.CONF_USERNAME: 'user', config_flow.CONF_USERNAME: 'user',
config_flow.CONF_PASSWORD: 'pass', config_flow.CONF_PASSWORD: 'pass',
@ -268,7 +286,7 @@ async def test_zeroconf_flow_bad_config_file(hass):
config_flow.DOMAIN, config_flow.DOMAIN,
data={ data={
config_flow.CONF_HOST: '1.2.3.4', config_flow.CONF_HOST: '1.2.3.4',
'properties': {'macaddress': '1234ABCD'} 'properties': {'macaddress': '00408C12345'}
}, },
context={'source': 'zeroconf'} context={'source': 'zeroconf'}
) )

View File

@ -4,6 +4,7 @@ from unittest.mock import Mock, patch
import asyncio import asyncio
from homeassistant.components.deconz import config_flow from homeassistant.components.deconz import config_flow
from homeassistant.components.ssdp import ATTR_MANUFACTURERURL, ATTR_SERIAL
from tests.common import MockConfigEntry from tests.common import MockConfigEntry
import pydeconz import pydeconz
@ -175,8 +176,8 @@ async def test_bridge_ssdp_discovery(hass):
data={ data={
config_flow.CONF_HOST: '1.2.3.4', config_flow.CONF_HOST: '1.2.3.4',
config_flow.CONF_PORT: 80, config_flow.CONF_PORT: 80,
config_flow.ATTR_SERIAL: 'id', ATTR_SERIAL: 'id',
config_flow.ATTR_MANUFACTURERURL: ATTR_MANUFACTURERURL:
config_flow.DECONZ_MANUFACTURERURL, config_flow.DECONZ_MANUFACTURERURL,
config_flow.ATTR_UUID: 'uuid:1234' config_flow.ATTR_UUID: 'uuid:1234'
}, },
@ -192,7 +193,7 @@ async def test_bridge_ssdp_discovery_not_deconz_bridge(hass):
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
config_flow.DOMAIN, config_flow.DOMAIN,
data={ data={
config_flow.ATTR_MANUFACTURERURL: 'not deconz bridge' ATTR_MANUFACTURERURL: 'not deconz bridge'
}, },
context={'source': 'ssdp'} context={'source': 'ssdp'}
) )
@ -217,8 +218,8 @@ async def test_bridge_discovery_update_existing_entry(hass):
config_flow.DOMAIN, config_flow.DOMAIN,
data={ data={
config_flow.CONF_HOST: 'mock-deconz', config_flow.CONF_HOST: 'mock-deconz',
config_flow.ATTR_SERIAL: 'id', ATTR_SERIAL: 'id',
config_flow.ATTR_MANUFACTURERURL: ATTR_MANUFACTURERURL:
config_flow.DECONZ_MANUFACTURERURL, config_flow.DECONZ_MANUFACTURERURL,
config_flow.ATTR_UUID: 'uuid:1234' config_flow.ATTR_UUID: 'uuid:1234'
}, },

View File

@ -29,11 +29,16 @@ def hassio_env():
@pytest.fixture @pytest.fixture
def hassio_stubs(hassio_env, hass, hass_client, aioclient_mock): def hassio_stubs(hassio_env, hass, hass_client, aioclient_mock):
"""Create mock hassio http client.""" """Create mock hassio http client."""
with patch('homeassistant.components.hassio.HassIO.update_hass_api', with patch(
Mock(return_value=mock_coro({"result": "ok"}))), \ 'homeassistant.components.hassio.HassIO.update_hass_api',
patch('homeassistant.components.hassio.HassIO.' return_value=mock_coro({"result": "ok"})
'get_homeassistant_info', ), patch(
Mock(side_effect=HassioAPIError())): 'homeassistant.components.hassio.HassIO.update_hass_timezone',
return_value=mock_coro({"result": "ok"})
), patch(
'homeassistant.components.hassio.HassIO.get_homeassistant_info',
side_effect=HassioAPIError()
):
hass.state = CoreState.starting hass.state = CoreState.starting
hass.loop.run_until_complete(async_setup_component(hass, 'hassio', { hass.loop.run_until_complete(async_setup_component(hass, 'hassio', {
'http': { 'http': {

View File

@ -56,7 +56,7 @@ async def test_hassio_addon_panel_startup(hass, aioclient_mock, hassio_env):
}) })
await hass.async_block_till_done() await hass.async_block_till_done()
assert aioclient_mock.call_count == 2 assert aioclient_mock.call_count == 3
assert mock_panel.called assert mock_panel.called
mock_panel.assert_called_with( mock_panel.assert_called_with(
hass, 'test1', { hass, 'test1', {
@ -98,7 +98,7 @@ async def test_hassio_addon_panel_api(hass, aioclient_mock, hassio_env,
}) })
await hass.async_block_till_done() await hass.async_block_till_done()
assert aioclient_mock.call_count == 2 assert aioclient_mock.call_count == 3
assert mock_panel.called assert mock_panel.called
mock_panel.assert_called_with( mock_panel.assert_called_with(
hass, 'test1', { hass, 'test1', {

View File

@ -43,7 +43,7 @@ def test_setup_api_ping(hass, aioclient_mock):
result = yield from async_setup_component(hass, 'hassio', {}) result = yield from async_setup_component(hass, 'hassio', {})
assert result assert result
assert aioclient_mock.call_count == 4 assert aioclient_mock.call_count == 5
assert hass.components.hassio.get_homeassistant_version() == "10.0" assert hass.components.hassio.get_homeassistant_version() == "10.0"
assert hass.components.hassio.is_hassio() assert hass.components.hassio.is_hassio()
@ -82,7 +82,7 @@ def test_setup_api_push_api_data(hass, aioclient_mock):
}) })
assert result assert result
assert aioclient_mock.call_count == 4 assert aioclient_mock.call_count == 5
assert not aioclient_mock.mock_calls[1][2]['ssl'] assert not aioclient_mock.mock_calls[1][2]['ssl']
assert aioclient_mock.mock_calls[1][2]['port'] == 9999 assert aioclient_mock.mock_calls[1][2]['port'] == 9999
assert aioclient_mock.mock_calls[1][2]['watchdog'] assert aioclient_mock.mock_calls[1][2]['watchdog']
@ -101,7 +101,7 @@ def test_setup_api_push_api_data_server_host(hass, aioclient_mock):
}) })
assert result assert result
assert aioclient_mock.call_count == 4 assert aioclient_mock.call_count == 5
assert not aioclient_mock.mock_calls[1][2]['ssl'] assert not aioclient_mock.mock_calls[1][2]['ssl']
assert aioclient_mock.mock_calls[1][2]['port'] == 9999 assert aioclient_mock.mock_calls[1][2]['port'] == 9999
assert not aioclient_mock.mock_calls[1][2]['watchdog'] assert not aioclient_mock.mock_calls[1][2]['watchdog']
@ -117,7 +117,7 @@ async def test_setup_api_push_api_data_default(hass, aioclient_mock,
}) })
assert result assert result
assert aioclient_mock.call_count == 4 assert aioclient_mock.call_count == 5
assert not aioclient_mock.mock_calls[1][2]['ssl'] assert not aioclient_mock.mock_calls[1][2]['ssl']
assert aioclient_mock.mock_calls[1][2]['port'] == 8123 assert aioclient_mock.mock_calls[1][2]['port'] == 8123
refresh_token = aioclient_mock.mock_calls[1][2]['refresh_token'] refresh_token = aioclient_mock.mock_calls[1][2]['refresh_token']
@ -177,27 +177,29 @@ async def test_setup_api_existing_hassio_user(hass, aioclient_mock,
}) })
assert result assert result
assert aioclient_mock.call_count == 4 assert aioclient_mock.call_count == 5
assert not aioclient_mock.mock_calls[1][2]['ssl'] assert not aioclient_mock.mock_calls[1][2]['ssl']
assert aioclient_mock.mock_calls[1][2]['port'] == 8123 assert aioclient_mock.mock_calls[1][2]['port'] == 8123
assert aioclient_mock.mock_calls[1][2]['refresh_token'] == token.token assert aioclient_mock.mock_calls[1][2]['refresh_token'] == token.token
@asyncio.coroutine async def test_setup_core_push_timezone(hass, aioclient_mock):
def test_setup_core_push_timezone(hass, aioclient_mock):
"""Test setup with API push default data.""" """Test setup with API push default data."""
hass.config.time_zone = 'testzone'
with patch.dict(os.environ, MOCK_ENVIRON): with patch.dict(os.environ, MOCK_ENVIRON):
result = yield from async_setup_component(hass, 'hassio', { result = await async_setup_component(hass, 'hassio', {
'hassio': {}, 'hassio': {},
'homeassistant': {
'time_zone': 'testzone',
},
}) })
assert result assert result
assert aioclient_mock.call_count == 5 assert aioclient_mock.call_count == 5
assert aioclient_mock.mock_calls[2][2]['timezone'] == "testzone" assert aioclient_mock.mock_calls[2][2]['timezone'] == "testzone"
await hass.config.async_update(time_zone='America/New_York')
await hass.async_block_till_done()
assert aioclient_mock.mock_calls[-1][2]['timezone'] == "America/New_York"
@asyncio.coroutine @asyncio.coroutine
def test_setup_hassio_no_additional_data(hass, aioclient_mock): def test_setup_hassio_no_additional_data(hass, aioclient_mock):
@ -209,7 +211,7 @@ def test_setup_hassio_no_additional_data(hass, aioclient_mock):
}) })
assert result assert result
assert aioclient_mock.call_count == 4 assert aioclient_mock.call_count == 5
assert aioclient_mock.mock_calls[-1][3]['X-Hassio-Key'] == "123456" assert aioclient_mock.mock_calls[-1][3]['X-Hassio-Key'] == "123456"
@ -288,14 +290,14 @@ def test_service_calls(hassio_env, hass, aioclient_mock):
'hassio', 'addon_stdin', {'addon': 'test', 'input': 'test'}) 'hassio', 'addon_stdin', {'addon': 'test', 'input': 'test'})
yield from hass.async_block_till_done() yield from hass.async_block_till_done()
assert aioclient_mock.call_count == 6 assert aioclient_mock.call_count == 7
assert aioclient_mock.mock_calls[-1][2] == 'test' assert aioclient_mock.mock_calls[-1][2] == 'test'
yield from hass.services.async_call('hassio', 'host_shutdown', {}) yield from hass.services.async_call('hassio', 'host_shutdown', {})
yield from hass.services.async_call('hassio', 'host_reboot', {}) yield from hass.services.async_call('hassio', 'host_reboot', {})
yield from hass.async_block_till_done() yield from hass.async_block_till_done()
assert aioclient_mock.call_count == 8 assert aioclient_mock.call_count == 9
yield from hass.services.async_call('hassio', 'snapshot_full', {}) yield from hass.services.async_call('hassio', 'snapshot_full', {})
yield from hass.services.async_call('hassio', 'snapshot_partial', { yield from hass.services.async_call('hassio', 'snapshot_partial', {
@ -305,7 +307,7 @@ def test_service_calls(hassio_env, hass, aioclient_mock):
}) })
yield from hass.async_block_till_done() yield from hass.async_block_till_done()
assert aioclient_mock.call_count == 10 assert aioclient_mock.call_count == 11
assert aioclient_mock.mock_calls[-1][2] == { assert aioclient_mock.mock_calls[-1][2] == {
'addons': ['test'], 'folders': ['ssl'], 'password': "123456"} 'addons': ['test'], 'folders': ['ssl'], 'password': "123456"}
@ -321,7 +323,7 @@ def test_service_calls(hassio_env, hass, aioclient_mock):
}) })
yield from hass.async_block_till_done() yield from hass.async_block_till_done()
assert aioclient_mock.call_count == 12 assert aioclient_mock.call_count == 13
assert aioclient_mock.mock_calls[-1][2] == { assert aioclient_mock.mock_calls[-1][2] == {
'addons': ['test'], 'folders': ['ssl'], 'homeassistant': False, 'addons': ['test'], 'folders': ['ssl'], 'homeassistant': False,
'password': "123456" 'password': "123456"
@ -341,12 +343,12 @@ def test_service_calls_core(hassio_env, hass, aioclient_mock):
yield from hass.services.async_call('homeassistant', 'stop') yield from hass.services.async_call('homeassistant', 'stop')
yield from hass.async_block_till_done() yield from hass.async_block_till_done()
assert aioclient_mock.call_count == 3 assert aioclient_mock.call_count == 4
yield from hass.services.async_call('homeassistant', 'check_config') yield from hass.services.async_call('homeassistant', 'check_config')
yield from hass.async_block_till_done() yield from hass.async_block_till_done()
assert aioclient_mock.call_count == 3 assert aioclient_mock.call_count == 4
with patch( with patch(
'homeassistant.config.async_check_ha_config_file', 'homeassistant.config.async_check_ha_config_file',
@ -356,4 +358,4 @@ def test_service_calls_core(hassio_env, hass, aioclient_mock):
yield from hass.async_block_till_done() yield from hass.async_block_till_done()
assert mock_check_config.called assert mock_check_config.called
assert aioclient_mock.call_count == 4 assert aioclient_mock.call_count == 5

View File

@ -221,3 +221,24 @@ class TestComponentZone(unittest.TestCase):
assert zone.zone.in_zone(self.hass.states.get('zone.passive_zone'), assert zone.zone.in_zone(self.hass.states.get('zone.passive_zone'),
latitude, longitude) latitude, longitude)
async def test_core_config_update(hass):
"""Test updating core config will update home zone."""
assert await setup.async_setup_component(hass, 'zone', {})
home = hass.states.get('zone.home')
await hass.config.async_update(
location_name='Updated Name',
latitude=10,
longitude=20,
)
await hass.async_block_till_done()
home_updated = hass.states.get('zone.home')
assert home is not home_updated
assert home_updated.name == 'Updated Name'
assert home_updated.attributes['latitude'] == 10
assert home_updated.attributes['longitude'] == 20

View File

@ -436,6 +436,68 @@ async def test_track_sunrise(hass):
assert len(offset_runs) == 1 assert len(offset_runs) == 1
async def test_track_sunrise_update_location(hass):
"""Test track the sunrise."""
# Setup sun component
hass.config.latitude = 32.87336
hass.config.longitude = 117.22743
assert await async_setup_component(hass, sun.DOMAIN, {
sun.DOMAIN: {sun.CONF_ELEVATION: 0}})
# Get next sunrise
astral = Astral()
utc_now = datetime(2014, 5, 24, 12, 0, 0, tzinfo=dt_util.UTC)
utc_today = utc_now.date()
mod = -1
while True:
next_rising = (astral.sunrise_utc(
utc_today + timedelta(days=mod),
hass.config.latitude, hass.config.longitude))
if next_rising > utc_now:
break
mod += 1
# Track sunrise
runs = []
with patch('homeassistant.util.dt.utcnow', return_value=utc_now):
async_track_sunrise(hass, lambda: runs.append(1))
# Mimick sunrise
_send_time_changed(hass, next_rising)
await hass.async_block_till_done()
assert len(runs) == 1
# Move!
with patch('homeassistant.util.dt.utcnow', return_value=utc_now):
await hass.config.async_update(
latitude=40.755931,
longitude=-73.984606,
)
await hass.async_block_till_done()
# Mimick sunrise
_send_time_changed(hass, next_rising)
await hass.async_block_till_done()
# Did not increase
assert len(runs) == 1
# Get next sunrise
mod = -1
while True:
next_rising = (astral.sunrise_utc(
utc_today + timedelta(days=mod),
hass.config.latitude, hass.config.longitude))
if next_rising > utc_now:
break
mod += 1
# Mimick sunrise at new location
_send_time_changed(hass, next_rising)
await hass.async_block_till_done()
assert len(runs) == 2
async def test_track_sunset(hass): async def test_track_sunset(hass):
"""Test track the sunset.""" """Test track the sunset."""
latitude = 32.87336 latitude = 32.87336

View File

@ -444,7 +444,7 @@ async def test_updating_configuration(hass, hass_storage):
hass_storage["core.config"] = dict(core_data) hass_storage["core.config"] = dict(core_data)
await config_util.async_process_ha_core_config( await config_util.async_process_ha_core_config(
hass, {'whitelist_external_dirs': '/tmp'}) hass, {'whitelist_external_dirs': '/tmp'})
await hass.config.update(latitude=50) await hass.config.async_update(latitude=50)
new_core_data = copy.deepcopy(core_data) new_core_data = copy.deepcopy(core_data)
new_core_data['data']['latitude'] = 50 new_core_data['data']['latitude'] = 50

View File

@ -955,7 +955,7 @@ async def test_event_on_update(hass, hass_storage):
assert hass.config.latitude != 12 assert hass.config.latitude != 12
await hass.config.update(latitude=12) await hass.config.async_update(latitude=12)
await hass.async_block_till_done() await hass.async_block_till_done()
assert hass.config.latitude == 12 assert hass.config.latitude == 12
@ -963,10 +963,10 @@ async def test_event_on_update(hass, hass_storage):
assert events[0].data == {'latitude': 12} assert events[0].data == {'latitude': 12}
def test_bad_timezone_raises_value_error(hass): async def test_bad_timezone_raises_value_error(hass):
"""Test bad timezone raises ValueError.""" """Test bad timezone raises ValueError."""
with pytest.raises(ValueError): with pytest.raises(ValueError):
hass.config.set_time_zone('not_a_timezone') await hass.config.async_update(time_zone='not_a_timezone')
@patch('homeassistant.core.monotonic') @patch('homeassistant.core.monotonic')