Merge pull request #23448 from home-assistant/rc

0.92.1
This commit is contained in:
Pascal Vizeli 2019-04-26 22:41:20 +02:00 committed by GitHub
commit ee107755f8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 172 additions and 104 deletions

View File

@ -355,7 +355,7 @@ async def _async_set_up_integrations(
if stage_1_domains: if stage_1_domains:
await asyncio.gather(*[ await asyncio.gather(*[
async_setup_component(hass, domain, config) async_setup_component(hass, domain, config)
for domain in logging_domains for domain in stage_1_domains
]) ])
# Load all integrations # Load all integrations

View File

@ -2,7 +2,6 @@
import asyncio import asyncio
from base64 import b64decode, b64encode from base64 import b64decode, b64encode
import logging import logging
import re
import socket import socket
from datetime import timedelta from datetime import timedelta
@ -19,26 +18,22 @@ _LOGGER = logging.getLogger(__name__)
DEFAULT_RETRY = 3 DEFAULT_RETRY = 3
def ipv4_address(value):
"""Validate an ipv4 address."""
regex = re.compile(r'^\d+\.\d+\.\d+\.\d+$')
if not regex.match(value):
raise vol.Invalid('Invalid Ipv4 address, expected a.b.c.d')
return value
def data_packet(value): def data_packet(value):
"""Decode a data packet given for broadlink.""" """Decode a data packet given for broadlink."""
return b64decode(cv.string(value)) value = cv.string(value)
extra = len(value) % 4
if extra > 0:
value = value + ('=' * (4 - extra))
return b64decode(value)
SERVICE_SEND_SCHEMA = vol.Schema({ SERVICE_SEND_SCHEMA = vol.Schema({
vol.Required(CONF_HOST): ipv4_address, vol.Required(CONF_HOST): cv.string,
vol.Required(CONF_PACKET): vol.All(cv.ensure_list, [data_packet]) vol.Required(CONF_PACKET): vol.All(cv.ensure_list, [data_packet])
}) })
SERVICE_LEARN_SCHEMA = vol.Schema({ SERVICE_LEARN_SCHEMA = vol.Schema({
vol.Required(CONF_HOST): ipv4_address, vol.Required(CONF_HOST): cv.string,
}) })

View File

@ -63,10 +63,10 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry):
if not daikin_api: if not daikin_api:
return False return False
hass.data.setdefault(DOMAIN, {}).update({entry.entry_id: daikin_api}) hass.data.setdefault(DOMAIN, {}).update({entry.entry_id: daikin_api})
await asyncio.wait([ for component in COMPONENT_TYPES:
hass.config_entries.async_forward_entry_setup(entry, component) hass.async_create_task(
for component in COMPONENT_TYPES hass.config_entries.async_forward_entry_setup(
]) entry, component))
return True return True

View File

@ -3,7 +3,7 @@
"name": "Ecovacs", "name": "Ecovacs",
"documentation": "https://www.home-assistant.io/components/ecovacs", "documentation": "https://www.home-assistant.io/components/ecovacs",
"requirements": [ "requirements": [
"sucks==0.9.3" "sucks==0.9.4"
], ],
"dependencies": [], "dependencies": [],
"codeowners": [ "codeowners": [

View File

@ -3,8 +3,7 @@
"name": "Flux", "name": "Flux",
"documentation": "https://www.home-assistant.io/components/flux", "documentation": "https://www.home-assistant.io/components/flux",
"requirements": [], "requirements": [],
"dependencies": [ "dependencies": [],
"light" "after_dependencies": ["light"],
],
"codeowners": [] "codeowners": []
} }

View File

@ -3,7 +3,7 @@
"name": "Heos", "name": "Heos",
"documentation": "https://www.home-assistant.io/components/heos", "documentation": "https://www.home-assistant.io/components/heos",
"requirements": [ "requirements": [
"pyheos==0.4.0" "pyheos==0.4.1"
], ],
"dependencies": [], "dependencies": [],
"codeowners": [ "codeowners": [

View File

@ -1,4 +1,5 @@
"""Denon HEOS Media Player.""" """Denon HEOS Media Player."""
import asyncio
from functools import reduce, wraps from functools import reduce, wraps
import logging import logging
from operator import ior from operator import ior
@ -47,7 +48,7 @@ def log_command_error(command: str):
from pyheos import CommandError from pyheos import CommandError
try: try:
await func(*args, **kwargs) await func(*args, **kwargs)
except CommandError as ex: except (CommandError, asyncio.TimeoutError, ConnectionError) as ex:
_LOGGER.error("Unable to %s: %s", command, ex) _LOGGER.error("Unable to %s: %s", command, ex)
return wrapper return wrapper
return decorator return decorator
@ -85,6 +86,13 @@ class HeosMediaPlayer(MediaPlayerDevice):
async def _heos_event(self, event): async def _heos_event(self, event):
"""Handle connection event.""" """Handle connection event."""
from pyheos import CommandError, const
if event == const.EVENT_CONNECTED:
try:
await self._player.refresh()
except (CommandError, asyncio.TimeoutError, ConnectionError) as ex:
_LOGGER.error("Unable to refresh player %s: %s",
self._player, ex)
await self.async_update_ha_state(True) await self.async_update_ha_state(True)
async def _player_update(self, player_id, event): async def _player_update(self, player_id, event):

View File

@ -45,7 +45,8 @@ from .const import (
SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, SUPPORT_PLAY_MEDIA, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, SUPPORT_PLAY_MEDIA,
SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK, SUPPORT_SELECT_SOUND_MODE, SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK, SUPPORT_SELECT_SOUND_MODE,
SUPPORT_SELECT_SOURCE, SUPPORT_SHUFFLE_SET, SUPPORT_STOP, SUPPORT_TURN_OFF, SUPPORT_SELECT_SOURCE, SUPPORT_SHUFFLE_SET, SUPPORT_STOP, SUPPORT_TURN_OFF,
SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET) SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET,
SUPPORT_VOLUME_STEP)
from .reproduce_state import async_reproduce_states # noqa from .reproduce_state import async_reproduce_states # noqa
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -164,77 +165,77 @@ async def async_setup(hass, config):
component.async_register_entity_service( component.async_register_entity_service(
SERVICE_TURN_ON, MEDIA_PLAYER_SCHEMA, SERVICE_TURN_ON, MEDIA_PLAYER_SCHEMA,
'async_turn_on', SUPPORT_TURN_ON 'async_turn_on', [SUPPORT_TURN_ON]
) )
component.async_register_entity_service( component.async_register_entity_service(
SERVICE_TURN_OFF, MEDIA_PLAYER_SCHEMA, SERVICE_TURN_OFF, MEDIA_PLAYER_SCHEMA,
'async_turn_off', SUPPORT_TURN_OFF 'async_turn_off', [SUPPORT_TURN_OFF]
) )
component.async_register_entity_service( component.async_register_entity_service(
SERVICE_TOGGLE, MEDIA_PLAYER_SCHEMA, SERVICE_TOGGLE, MEDIA_PLAYER_SCHEMA,
'async_toggle', SUPPORT_TURN_OFF | SUPPORT_TURN_ON 'async_toggle', [SUPPORT_TURN_OFF | SUPPORT_TURN_ON]
) )
component.async_register_entity_service( component.async_register_entity_service(
SERVICE_VOLUME_UP, MEDIA_PLAYER_SCHEMA, SERVICE_VOLUME_UP, MEDIA_PLAYER_SCHEMA,
'async_volume_up', SUPPORT_VOLUME_SET 'async_volume_up', [SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP]
) )
component.async_register_entity_service( component.async_register_entity_service(
SERVICE_VOLUME_DOWN, MEDIA_PLAYER_SCHEMA, SERVICE_VOLUME_DOWN, MEDIA_PLAYER_SCHEMA,
'async_volume_down', SUPPORT_VOLUME_SET 'async_volume_down', [SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP]
) )
component.async_register_entity_service( component.async_register_entity_service(
SERVICE_MEDIA_PLAY_PAUSE, MEDIA_PLAYER_SCHEMA, SERVICE_MEDIA_PLAY_PAUSE, MEDIA_PLAYER_SCHEMA,
'async_media_play_pause', SUPPORT_PLAY | SUPPORT_PAUSE 'async_media_play_pause', [SUPPORT_PLAY | SUPPORT_PAUSE]
) )
component.async_register_entity_service( component.async_register_entity_service(
SERVICE_MEDIA_PLAY, MEDIA_PLAYER_SCHEMA, SERVICE_MEDIA_PLAY, MEDIA_PLAYER_SCHEMA,
'async_media_play', SUPPORT_PLAY 'async_media_play', [SUPPORT_PLAY]
) )
component.async_register_entity_service( component.async_register_entity_service(
SERVICE_MEDIA_PAUSE, MEDIA_PLAYER_SCHEMA, SERVICE_MEDIA_PAUSE, MEDIA_PLAYER_SCHEMA,
'async_media_pause', SUPPORT_PAUSE 'async_media_pause', [SUPPORT_PAUSE]
) )
component.async_register_entity_service( component.async_register_entity_service(
SERVICE_MEDIA_STOP, MEDIA_PLAYER_SCHEMA, SERVICE_MEDIA_STOP, MEDIA_PLAYER_SCHEMA,
'async_media_stop', SUPPORT_STOP 'async_media_stop', [SUPPORT_STOP]
) )
component.async_register_entity_service( component.async_register_entity_service(
SERVICE_MEDIA_NEXT_TRACK, MEDIA_PLAYER_SCHEMA, SERVICE_MEDIA_NEXT_TRACK, MEDIA_PLAYER_SCHEMA,
'async_media_next_track', SUPPORT_NEXT_TRACK 'async_media_next_track', [SUPPORT_NEXT_TRACK]
) )
component.async_register_entity_service( component.async_register_entity_service(
SERVICE_MEDIA_PREVIOUS_TRACK, MEDIA_PLAYER_SCHEMA, SERVICE_MEDIA_PREVIOUS_TRACK, MEDIA_PLAYER_SCHEMA,
'async_media_previous_track', SUPPORT_PREVIOUS_TRACK 'async_media_previous_track', [SUPPORT_PREVIOUS_TRACK]
) )
component.async_register_entity_service( component.async_register_entity_service(
SERVICE_CLEAR_PLAYLIST, MEDIA_PLAYER_SCHEMA, SERVICE_CLEAR_PLAYLIST, MEDIA_PLAYER_SCHEMA,
'async_clear_playlist', SUPPORT_CLEAR_PLAYLIST 'async_clear_playlist', [SUPPORT_CLEAR_PLAYLIST]
) )
component.async_register_entity_service( component.async_register_entity_service(
SERVICE_VOLUME_SET, MEDIA_PLAYER_SET_VOLUME_SCHEMA, SERVICE_VOLUME_SET, MEDIA_PLAYER_SET_VOLUME_SCHEMA,
lambda entity, call: entity.async_set_volume_level( lambda entity, call: entity.async_set_volume_level(
volume=call.data[ATTR_MEDIA_VOLUME_LEVEL]), volume=call.data[ATTR_MEDIA_VOLUME_LEVEL]),
SUPPORT_VOLUME_SET [SUPPORT_VOLUME_SET]
) )
component.async_register_entity_service( component.async_register_entity_service(
SERVICE_VOLUME_MUTE, MEDIA_PLAYER_MUTE_VOLUME_SCHEMA, SERVICE_VOLUME_MUTE, MEDIA_PLAYER_MUTE_VOLUME_SCHEMA,
lambda entity, call: entity.async_mute_volume( lambda entity, call: entity.async_mute_volume(
mute=call.data[ATTR_MEDIA_VOLUME_MUTED]), mute=call.data[ATTR_MEDIA_VOLUME_MUTED]),
SUPPORT_VOLUME_MUTE [SUPPORT_VOLUME_MUTE]
) )
component.async_register_entity_service( component.async_register_entity_service(
SERVICE_MEDIA_SEEK, MEDIA_PLAYER_MEDIA_SEEK_SCHEMA, SERVICE_MEDIA_SEEK, MEDIA_PLAYER_MEDIA_SEEK_SCHEMA,
lambda entity, call: entity.async_media_seek( lambda entity, call: entity.async_media_seek(
position=call.data[ATTR_MEDIA_SEEK_POSITION]), position=call.data[ATTR_MEDIA_SEEK_POSITION]),
SUPPORT_SEEK [SUPPORT_SEEK]
) )
component.async_register_entity_service( component.async_register_entity_service(
SERVICE_SELECT_SOURCE, MEDIA_PLAYER_SELECT_SOURCE_SCHEMA, SERVICE_SELECT_SOURCE, MEDIA_PLAYER_SELECT_SOURCE_SCHEMA,
'async_select_source', SUPPORT_SELECT_SOURCE 'async_select_source', [SUPPORT_SELECT_SOURCE]
) )
component.async_register_entity_service( component.async_register_entity_service(
SERVICE_SELECT_SOUND_MODE, MEDIA_PLAYER_SELECT_SOUND_MODE_SCHEMA, SERVICE_SELECT_SOUND_MODE, MEDIA_PLAYER_SELECT_SOUND_MODE_SCHEMA,
'async_select_sound_mode', SUPPORT_SELECT_SOUND_MODE 'async_select_sound_mode', [SUPPORT_SELECT_SOUND_MODE]
) )
component.async_register_entity_service( component.async_register_entity_service(
SERVICE_PLAY_MEDIA, MEDIA_PLAYER_PLAY_MEDIA_SCHEMA, SERVICE_PLAY_MEDIA, MEDIA_PLAYER_PLAY_MEDIA_SCHEMA,
@ -242,11 +243,11 @@ async def async_setup(hass, config):
media_type=call.data[ATTR_MEDIA_CONTENT_TYPE], media_type=call.data[ATTR_MEDIA_CONTENT_TYPE],
media_id=call.data[ATTR_MEDIA_CONTENT_ID], media_id=call.data[ATTR_MEDIA_CONTENT_ID],
enqueue=call.data.get(ATTR_MEDIA_ENQUEUE) enqueue=call.data.get(ATTR_MEDIA_ENQUEUE)
), SUPPORT_PLAY_MEDIA ), [SUPPORT_PLAY_MEDIA]
) )
component.async_register_entity_service( component.async_register_entity_service(
SERVICE_SHUFFLE_SET, MEDIA_PLAYER_SET_SHUFFLE_SCHEMA, SERVICE_SHUFFLE_SET, MEDIA_PLAYER_SET_SHUFFLE_SCHEMA,
'async_set_shuffle', SUPPORT_SHUFFLE_SET 'async_set_shuffle', [SUPPORT_SHUFFLE_SET]
) )
return True return True
@ -686,7 +687,8 @@ class MediaPlayerDevice(Entity):
await self.hass.async_add_job(self.volume_up) await self.hass.async_add_job(self.volume_up)
return return
if self.volume_level < 1: if self.volume_level < 1 \
and self.supported_features & SUPPORT_VOLUME_SET:
await self.async_set_volume_level(min(1, self.volume_level + .1)) await self.async_set_volume_level(min(1, self.volume_level + .1))
async def async_volume_down(self): async def async_volume_down(self):
@ -699,7 +701,8 @@ class MediaPlayerDevice(Entity):
await self.hass.async_add_job(self.volume_down) await self.hass.async_add_job(self.volume_down)
return return
if self.volume_level > 0: if self.volume_level > 0 \
and self.supported_features & SUPPORT_VOLUME_SET:
await self.async_set_volume_level( await self.async_set_volume_level(
max(0, self.volume_level - .1)) max(0, self.volume_level - .1))

View File

@ -12,6 +12,8 @@ from homeassistant.helpers import discovery
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.util import Throttle from homeassistant.util import Throttle
from .const import DOMAIN, DATA_NETATMO_AUTH
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DATA_PERSONS = 'netatmo_persons' DATA_PERSONS = 'netatmo_persons'
@ -20,8 +22,6 @@ DATA_WEBHOOK_URL = 'netatmo_webhook_url'
CONF_SECRET_KEY = 'secret_key' CONF_SECRET_KEY = 'secret_key'
CONF_WEBHOOKS = 'webhooks' CONF_WEBHOOKS = 'webhooks'
DOMAIN = 'netatmo'
SERVICE_ADDWEBHOOK = 'addwebhook' SERVICE_ADDWEBHOOK = 'addwebhook'
SERVICE_DROPWEBHOOK = 'dropwebhook' SERVICE_DROPWEBHOOK = 'dropwebhook'
@ -83,10 +83,9 @@ def setup(hass, config):
"""Set up the Netatmo devices.""" """Set up the Netatmo devices."""
import pyatmo import pyatmo
global NETATMO_AUTH
hass.data[DATA_PERSONS] = {} hass.data[DATA_PERSONS] = {}
try: try:
NETATMO_AUTH = pyatmo.ClientAuth( auth = pyatmo.ClientAuth(
config[DOMAIN][CONF_API_KEY], config[DOMAIN][CONF_SECRET_KEY], config[DOMAIN][CONF_API_KEY], config[DOMAIN][CONF_SECRET_KEY],
config[DOMAIN][CONF_USERNAME], config[DOMAIN][CONF_PASSWORD], config[DOMAIN][CONF_USERNAME], config[DOMAIN][CONF_PASSWORD],
'read_station read_camera access_camera ' 'read_station read_camera access_camera '
@ -96,6 +95,9 @@ def setup(hass, config):
_LOGGER.error("Unable to connect to Netatmo API") _LOGGER.error("Unable to connect to Netatmo API")
return False return False
# Store config to be used during entry setup
hass.data[DATA_NETATMO_AUTH] = auth
if config[DOMAIN][CONF_DISCOVERY]: if config[DOMAIN][CONF_DISCOVERY]:
for component in 'camera', 'sensor', 'binary_sensor', 'climate': for component in 'camera', 'sensor', 'binary_sensor', 'climate':
discovery.load_platform(hass, component, DOMAIN, {}, config) discovery.load_platform(hass, component, DOMAIN, {}, config)
@ -107,7 +109,7 @@ def setup(hass, config):
webhook_id) webhook_id)
hass.components.webhook.async_register( hass.components.webhook.async_register(
DOMAIN, 'Netatmo', webhook_id, handle_webhook) DOMAIN, 'Netatmo', webhook_id, handle_webhook)
NETATMO_AUTH.addwebhook(hass.data[DATA_WEBHOOK_URL]) auth.addwebhook(hass.data[DATA_WEBHOOK_URL])
hass.bus.listen_once( hass.bus.listen_once(
EVENT_HOMEASSISTANT_STOP, dropwebhook) EVENT_HOMEASSISTANT_STOP, dropwebhook)
@ -117,7 +119,7 @@ def setup(hass, config):
if url is None: if url is None:
url = hass.data[DATA_WEBHOOK_URL] url = hass.data[DATA_WEBHOOK_URL]
_LOGGER.info("Adding webhook for URL: %s", url) _LOGGER.info("Adding webhook for URL: %s", url)
NETATMO_AUTH.addwebhook(url) auth.addwebhook(url)
hass.services.register( hass.services.register(
DOMAIN, SERVICE_ADDWEBHOOK, _service_addwebhook, DOMAIN, SERVICE_ADDWEBHOOK, _service_addwebhook,
@ -126,7 +128,7 @@ def setup(hass, config):
def _service_dropwebhook(service): def _service_dropwebhook(service):
"""Service to drop webhooks during runtime.""" """Service to drop webhooks during runtime."""
_LOGGER.info("Dropping webhook") _LOGGER.info("Dropping webhook")
NETATMO_AUTH.dropwebhook() auth.dropwebhook()
hass.services.register( hass.services.register(
DOMAIN, SERVICE_DROPWEBHOOK, _service_dropwebhook, DOMAIN, SERVICE_DROPWEBHOOK, _service_dropwebhook,
@ -137,7 +139,8 @@ def setup(hass, config):
def dropwebhook(hass): def dropwebhook(hass):
"""Drop the webhook subscription.""" """Drop the webhook subscription."""
NETATMO_AUTH.dropwebhook() auth = hass.data[DATA_NETATMO_AUTH]
auth.dropwebhook()
async def handle_webhook(hass, webhook_id, request): async def handle_webhook(hass, webhook_id, request):

View File

@ -8,7 +8,8 @@ from homeassistant.components.binary_sensor import (
from homeassistant.const import CONF_TIMEOUT from homeassistant.const import CONF_TIMEOUT
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
from . import CameraData, NETATMO_AUTH from .const import DATA_NETATMO_AUTH
from . import CameraData
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -59,8 +60,11 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
module_name = None module_name = None
import pyatmo import pyatmo
auth = hass.data[DATA_NETATMO_AUTH]
try: try:
data = CameraData(hass, NETATMO_AUTH, home) data = CameraData(hass, auth, home)
if not data.get_camera_names(): if not data.get_camera_names():
return None return None
except pyatmo.NoDevice: except pyatmo.NoDevice:

View File

@ -9,7 +9,8 @@ from homeassistant.components.camera import (
from homeassistant.const import CONF_VERIFY_SSL from homeassistant.const import CONF_VERIFY_SSL
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
from . import CameraData, NETATMO_AUTH from .const import DATA_NETATMO_AUTH
from . import CameraData
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -37,8 +38,11 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
verify_ssl = config.get(CONF_VERIFY_SSL, True) verify_ssl = config.get(CONF_VERIFY_SSL, True)
quality = config.get(CONF_QUALITY, DEFAULT_QUALITY) quality = config.get(CONF_QUALITY, DEFAULT_QUALITY)
import pyatmo import pyatmo
auth = hass.data[DATA_NETATMO_AUTH]
try: try:
data = CameraData(hass, NETATMO_AUTH, home) data = CameraData(hass, auth, home)
for camera_name in data.get_camera_names(): for camera_name in data.get_camera_names():
camera_type = data.get_camera_type(camera=camera_name, home=home) camera_type = data.get_camera_type(camera=camera_name, home=home)
if CONF_CAMERAS in config: if CONF_CAMERAS in config:

View File

@ -14,7 +14,7 @@ from homeassistant.const import (
STATE_OFF, TEMP_CELSIUS, ATTR_TEMPERATURE, CONF_NAME) STATE_OFF, TEMP_CELSIUS, ATTR_TEMPERATURE, CONF_NAME)
from homeassistant.util import Throttle from homeassistant.util import Throttle
from . import NETATMO_AUTH from .const import DATA_NETATMO_AUTH
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -68,8 +68,11 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the NetAtmo Thermostat.""" """Set up the NetAtmo Thermostat."""
import pyatmo import pyatmo
homes_conf = config.get(CONF_HOMES) homes_conf = config.get(CONF_HOMES)
auth = hass.data[DATA_NETATMO_AUTH]
try: try:
home_data = HomeData(NETATMO_AUTH) home_data = HomeData(auth)
except pyatmo.NoDevice: except pyatmo.NoDevice:
return return
@ -88,7 +91,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
for home in homes: for home in homes:
_LOGGER.debug("Setting up %s ...", home) _LOGGER.debug("Setting up %s ...", home)
try: try:
room_data = ThermostatData(NETATMO_AUTH, home) room_data = ThermostatData(auth, home)
except pyatmo.NoDevice: except pyatmo.NoDevice:
continue continue
for room_id in room_data.get_room_ids(): for room_id in room_data.get_room_ids():

View File

@ -0,0 +1,5 @@
"""Constants used by the Netatmo component."""
DOMAIN = 'netatmo'
DATA_NETATMO = 'netatmo'
DATA_NETATMO_AUTH = 'netatmo_auth'

View File

@ -12,7 +12,7 @@ from homeassistant.const import (
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from . import NETATMO_AUTH from .const import DATA_NETATMO_AUTH
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -68,23 +68,26 @@ MODULE_TYPE_INDOOR = 'NAModule4'
def setup_platform(hass, config, add_entities, discovery_info=None): def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the available Netatmo weather sensors.""" """Set up the available Netatmo weather sensors."""
dev = [] dev = []
auth = hass.data[DATA_NETATMO_AUTH]
if CONF_MODULES in config: if CONF_MODULES in config:
manual_config(config, dev) manual_config(auth, config, dev)
else: else:
auto_config(config, dev) auto_config(auth, config, dev)
if dev: if dev:
add_entities(dev, True) add_entities(dev, True)
def manual_config(config, dev): def manual_config(auth, config, dev):
"""Handle manual configuration.""" """Handle manual configuration."""
import pyatmo import pyatmo
all_classes = all_product_classes() all_classes = all_product_classes()
not_handled = {} not_handled = {}
for data_class in all_classes: for data_class in all_classes:
data = NetAtmoData(NETATMO_AUTH, data_class, data = NetAtmoData(auth, data_class,
config.get(CONF_STATION)) config.get(CONF_STATION))
try: try:
# Iterate each module # Iterate each module
@ -107,12 +110,12 @@ def manual_config(config, dev):
_LOGGER.error('Module name: "%s" not found', module_name) _LOGGER.error('Module name: "%s" not found', module_name)
def auto_config(config, dev): def auto_config(auth, config, dev):
"""Handle auto configuration.""" """Handle auto configuration."""
import pyatmo import pyatmo
for data_class in all_product_classes(): for data_class in all_product_classes():
data = NetAtmoData(NETATMO_AUTH, data_class, config.get(CONF_STATION)) data = NetAtmoData(auth, data_class, config.get(CONF_STATION))
try: try:
for module_name in data.get_module_names(): for module_name in data.get_module_names():
for variable in \ for variable in \

View File

@ -88,7 +88,7 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry):
await async_setup_webhook(hass, entry, session) await async_setup_webhook(hass, entry, session)
client = MinutPointClient(hass, entry, session) client = MinutPointClient(hass, entry, session)
hass.data.setdefault(DOMAIN, {}).update({entry.entry_id: client}) hass.data.setdefault(DOMAIN, {}).update({entry.entry_id: client})
await client.update() hass.async_create_task(client.update())
return True return True

View File

@ -5,11 +5,12 @@ import re
import voluptuous as vol import voluptuous as vol
from homeassistant.components.media_player import ( from homeassistant.components.media_player import (
MediaPlayerDevice, PLATFORM_SCHEMA) PLATFORM_SCHEMA, MediaPlayerDevice)
from homeassistant.components.media_player.const import ( from homeassistant.components.media_player.const import (
DOMAIN, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, DOMAIN, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY,
SUPPORT_PREVIOUS_TRACK, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_TURN_OFF,
SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP) SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET,
SUPPORT_VOLUME_STEP)
from homeassistant.const import ( from homeassistant.const import (
CONF_HOST, CONF_NAME, CONF_PORT, STATE_OFF, STATE_PAUSED, STATE_PLAYING, CONF_HOST, CONF_NAME, CONF_PORT, STATE_OFF, STATE_PAUSED, STATE_PLAYING,
STATE_UNAVAILABLE) STATE_UNAVAILABLE)
@ -56,7 +57,7 @@ DEFAULT_PORT = 8090
SUPPORT_SOUNDTOUCH = SUPPORT_PAUSE | SUPPORT_VOLUME_STEP | \ SUPPORT_SOUNDTOUCH = SUPPORT_PAUSE | SUPPORT_VOLUME_STEP | \
SUPPORT_VOLUME_MUTE | SUPPORT_PREVIOUS_TRACK | \ SUPPORT_VOLUME_MUTE | SUPPORT_PREVIOUS_TRACK | \
SUPPORT_NEXT_TRACK | SUPPORT_TURN_OFF | \ SUPPORT_NEXT_TRACK | SUPPORT_TURN_OFF | \
SUPPORT_VOLUME_SET | SUPPORT_TURN_ON | SUPPORT_PLAY SUPPORT_VOLUME_SET | SUPPORT_TURN_ON | SUPPORT_PLAY | SUPPORT_PLAY_MEDIA
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST): cv.string, vol.Required(CONF_HOST): cv.string,

View File

@ -47,10 +47,10 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Zestimate sensor.""" """Set up the Zestimate sensor."""
name = config.get(CONF_NAME) name = config.get(CONF_NAME)
properties = config[CONF_ZPID] properties = config[CONF_ZPID]
params = {'zws-id': config[CONF_API_KEY]}
sensors = [] sensors = []
for zpid in properties: for zpid in properties:
params = {'zws-id': config[CONF_API_KEY]}
params['zpid'] = zpid params['zpid'] = zpid
sensors.append(ZestimateDataSensor(name, params)) sensors.append(ZestimateDataSensor(name, params))
add_entities(sensors, True) add_entities(sensors, True)

View File

@ -398,8 +398,12 @@ def process_ha_config_upgrade(hass: HomeAssistant) -> None:
if TTS_PRE_92 in config_raw: if TTS_PRE_92 in config_raw:
_LOGGER.info("Migrating google tts to google_translate tts") _LOGGER.info("Migrating google tts to google_translate tts")
config_raw = config_raw.replace(TTS_PRE_92, TTS_92) config_raw = config_raw.replace(TTS_PRE_92, TTS_92)
with open(config_path, 'wt', encoding='utf-8') as config_file: try:
config_file.write(config_raw) with open(config_path, 'wt', encoding='utf-8') as config_file:
config_file.write(config_raw)
except IOError:
_LOGGER.exception("Migrating to google_translate tts failed")
pass
with open(version_path, 'wt') as outp: with open(version_path, 'wt') as outp:
outp.write(__version__) outp.write(__version__)

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 = 92 MINOR_VERSION = 92
PATCH_VERSION = '0' PATCH_VERSION = '1'
__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

@ -327,7 +327,8 @@ async def _handle_service_platform_call(func, data, entities, context,
# Skip entities that don't have the required feature. # Skip entities that don't have the required feature.
if required_features is not None \ if required_features is not None \
and not entity.supported_features & required_features: and not any(entity.supported_features & feature_set
for feature_set in required_features):
continue continue
entity.async_set_context(context) entity.async_set_context(context)

View File

@ -151,9 +151,12 @@ async def _async_setup_component(hass: core.HomeAssistant,
if hasattr(component, 'async_setup'): if hasattr(component, 'async_setup'):
result = await component.async_setup( # type: ignore result = await component.async_setup( # type: ignore
hass, processed_config) hass, processed_config)
else: elif hasattr(component, 'setup'):
result = await hass.async_add_executor_job( result = await hass.async_add_executor_job(
component.setup, hass, processed_config) # type: ignore component.setup, hass, processed_config) # type: ignore
else:
log_error("No setup function defined.")
return False
except Exception: # pylint: disable=broad-except except Exception: # pylint: disable=broad-except
_LOGGER.exception("Error during setup of component %s", domain) _LOGGER.exception("Error during setup of component %s", domain)
async_notify_setup_error(hass, domain, True) async_notify_setup_error(hass, domain, True)
@ -176,7 +179,7 @@ async def _async_setup_component(hass: core.HomeAssistant,
for entry in hass.config_entries.async_entries(domain): for entry in hass.config_entries.async_entries(domain):
await entry.async_setup(hass, integration=integration) await entry.async_setup(hass, integration=integration)
hass.config.components.add(component.DOMAIN) # type: ignore hass.config.components.add(domain)
# Cleanup # Cleanup
if domain in hass.data[DATA_SETUP]: if domain in hass.data[DATA_SETUP]:
@ -184,7 +187,7 @@ async def _async_setup_component(hass: core.HomeAssistant,
hass.bus.async_fire( hass.bus.async_fire(
EVENT_COMPONENT_LOADED, EVENT_COMPONENT_LOADED,
{ATTR_COMPONENT: component.DOMAIN} # type: ignore {ATTR_COMPONENT: domain}
) )
return True return True

View File

@ -1073,7 +1073,7 @@ pygtt==1.1.2
pyhaversion==2.2.1 pyhaversion==2.2.1
# homeassistant.components.heos # homeassistant.components.heos
pyheos==0.4.0 pyheos==0.4.1
# homeassistant.components.hikvision # homeassistant.components.hikvision
pyhik==0.2.2 pyhik==0.2.2
@ -1650,7 +1650,7 @@ steamodd==4.21
stringcase==1.2.0 stringcase==1.2.0
# homeassistant.components.ecovacs # homeassistant.components.ecovacs
sucks==0.9.3 sucks==0.9.4
# homeassistant.components.onvif # homeassistant.components.onvif
suds-passworddigest-homeassistant==0.1.2a0.dev0 suds-passworddigest-homeassistant==0.1.2a0.dev0

View File

@ -217,7 +217,7 @@ pydeconz==54
pydispatcher==2.0.5 pydispatcher==2.0.5
# homeassistant.components.heos # homeassistant.components.heos
pyheos==0.4.0 pyheos==0.4.1
# homeassistant.components.homematic # homeassistant.components.homematic
pyhomematic==0.1.58 pyhomematic==0.1.58

View File

@ -4,10 +4,9 @@ from base64 import b64decode
from unittest.mock import MagicMock, patch, call from unittest.mock import MagicMock, patch, call
import pytest import pytest
import voluptuous as vol
from homeassistant.util.dt import utcnow from homeassistant.util.dt import utcnow
from homeassistant.components.broadlink import async_setup_service from homeassistant.components.broadlink import async_setup_service, data_packet
from homeassistant.components.broadlink.const import ( from homeassistant.components.broadlink.const import (
DOMAIN, SERVICE_LEARN, SERVICE_SEND) DOMAIN, SERVICE_LEARN, SERVICE_SEND)
@ -26,6 +25,13 @@ def dummy_broadlink():
yield broadlink yield broadlink
async def test_padding(hass):
"""Verify that non padding strings are allowed."""
assert data_packet('Jg') == b'&'
assert data_packet('Jg=') == b'&'
assert data_packet('Jg==') == b'&'
async def test_send(hass): async def test_send(hass):
"""Test send service.""" """Test send service."""
mock_device = MagicMock() mock_device = MagicMock()
@ -100,18 +106,3 @@ async def test_learn_timeout(hass):
assert mock_create.call_args == call( assert mock_create.call_args == call(
"No signal was received", "No signal was received",
title='Broadlink switch') title='Broadlink switch')
async def test_ipv4():
"""Test ipv4 parsing."""
from homeassistant.components.broadlink import ipv4_address
schema = vol.Schema(ipv4_address)
for value in ('invalid', '1', '192', '192.168',
'192.168.0', '192.168.0.A'):
with pytest.raises(vol.MultipleInvalid):
schema(value)
for value in ('192.168.0.1', '10.0.0.1'):
schema(value)

View File

@ -108,13 +108,40 @@ async def test_updates_start_from_signals(
state = hass.states.get('media_player.test_player') state = hass.states.get('media_player.test_player')
assert state.state == STATE_UNAVAILABLE assert state.state == STATE_UNAVAILABLE
# Test heos events update
async def test_updates_from_connection_event(
hass, config_entry, config, controller, input_sources, caplog):
"""Tests player updates from connection event after connection failure."""
# Connected
await setup_platform(hass, config_entry, config)
player = controller.players[1]
player.available = True player.available = True
player.heos.dispatcher.send( player.heos.dispatcher.send(
const.SIGNAL_HEOS_EVENT, const.EVENT_CONNECTED) const.SIGNAL_HEOS_EVENT, const.EVENT_CONNECTED)
await hass.async_block_till_done() await hass.async_block_till_done()
state = hass.states.get('media_player.test_player') state = hass.states.get('media_player.test_player')
assert state.state == STATE_PLAYING assert state.state == STATE_IDLE
assert player.refresh.call_count == 1
# Connected handles refresh failure
player.reset_mock()
player.refresh.side_effect = CommandError(None, "Failure", 1)
player.heos.dispatcher.send(
const.SIGNAL_HEOS_EVENT, const.EVENT_CONNECTED)
await hass.async_block_till_done()
state = hass.states.get('media_player.test_player')
assert player.refresh.call_count == 1
assert "Unable to refresh player" in caplog.text
# Disconnected
player.reset_mock()
player.available = False
player.heos.dispatcher.send(
const.SIGNAL_HEOS_EVENT, const.EVENT_DISCONNECTED)
await hass.async_block_till_done()
state = hass.states.get('media_player.test_player')
assert state.state == STATE_UNAVAILABLE
assert player.refresh.call_count == 0
async def test_updates_from_sources_updated( async def test_updates_from_sources_updated(

View File

@ -29,6 +29,13 @@ class AsyncMediaPlayer(mp.MediaPlayerDevice):
"""Volume level of the media player (0..1).""" """Volume level of the media player (0..1)."""
return self._volume return self._volume
@property
def supported_features(self):
"""Flag media player features that are supported."""
return mp.const.SUPPORT_VOLUME_SET | mp.const.SUPPORT_PLAY \
| mp.const.SUPPORT_PAUSE | mp.const.SUPPORT_TURN_OFF \
| mp.const.SUPPORT_TURN_ON
@asyncio.coroutine @asyncio.coroutine
def async_set_volume_level(self, volume): def async_set_volume_level(self, volume):
"""Set volume level, range 0..1.""" """Set volume level, range 0..1."""
@ -74,6 +81,13 @@ class SyncMediaPlayer(mp.MediaPlayerDevice):
"""Volume level of the media player (0..1).""" """Volume level of the media player (0..1)."""
return self._volume return self._volume
@property
def supported_features(self):
"""Flag media player features that are supported."""
return mp.const.SUPPORT_VOLUME_SET | mp.const.SUPPORT_VOLUME_STEP \
| mp.const.SUPPORT_PLAY | mp.const.SUPPORT_PAUSE \
| mp.const.SUPPORT_TURN_OFF | mp.const.SUPPORT_TURN_ON
def set_volume_level(self, volume): def set_volume_level(self, volume):
"""Set volume level, range 0..1.""" """Set volume level, range 0..1."""
self._volume = volume self._volume = volume

View File

@ -372,7 +372,7 @@ class TestSoundtouchMediaPlayer(unittest.TestCase):
mock.MagicMock()) mock.MagicMock())
assert mocked_soundtouch_device.call_count == 1 assert mocked_soundtouch_device.call_count == 1
all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH] all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH]
assert all_devices[0].supported_features == 17853 assert all_devices[0].supported_features == 18365
@mock.patch('libsoundtouch.device.SoundTouchDevice.power_off') @mock.patch('libsoundtouch.device.SoundTouchDevice.power_off')
@mock.patch('libsoundtouch.device.SoundTouchDevice.volume') @mock.patch('libsoundtouch.device.SoundTouchDevice.volume')

View File

@ -280,7 +280,7 @@ async def test_call_with_required_features(hass, mock_entities):
Mock(entities=mock_entities) Mock(entities=mock_entities)
], test_service_mock, ha.ServiceCall('test_domain', 'test_service', { ], test_service_mock, ha.ServiceCall('test_domain', 'test_service', {
'entity_id': 'all' 'entity_id': 'all'
}), required_features=1) }), required_features=[1])
assert len(mock_entities) == 2 assert len(mock_entities) == 2
# Called once because only one of the entities had the required features # Called once because only one of the entities had the required features
assert test_service_mock.call_count == 1 assert test_service_mock.call_count == 1