Compare commits

...

22 Commits

Author SHA1 Message Date
Paulus Schoutsen
6745e83a6c Merge pull request #4394 from home-assistant/release-0-32-4
0.32.4
2016-11-14 22:04:43 -08:00
Paulus Schoutsen
44bc057fdb Version bump to 0.32.4 2016-11-14 21:34:40 -08:00
hexa-
96b8d8fcfa http: reimplement X-Forwarded-For parsing (#4355)
This feature needs to be enabled through the `http.use_x_forwarded_for` option,
satisfying security concerns of spoofed remote addresses in untrusted network
environments.

The testsuite was enhanced to explicitly test the functionality of the
header.

Fixes #4265.

Signed-off-by: Martin Weinelt <hexa@darmstadt.ccc.de>
2016-11-14 21:33:34 -08:00
Sean Dague
fc2df34206 Pin versions on linters for tests
The linters really need to specify an exact version, because when
either flake8 or pylint release a new version, a whole lot of new
issues are caught, causing failures on the code unrelated to the
patches being pushed.

Pinning is a best practice for linters. This allows patches which move
forward the linter version to happen with any code fixes required for
it to pass.
2016-11-14 21:32:02 -08:00
Paulus Schoutsen
09c29737de Fix device tracker sending invalid event data 2016-11-14 21:31:17 -08:00
Paulus Schoutsen
4c01b47945 device_tracker.see should not call async methods (#4377) 2016-11-14 21:31:06 -08:00
Paulus Schoutsen
080f56e0f5 Merge pull request #4342 from home-assistant/release-0-32-3
0.32.3
2016-11-10 21:59:39 -08:00
Paulus Schoutsen
173e15e733 Version bump to 0.32.3 2016-11-10 21:50:05 -08:00
Paulus Schoutsen
72407c2f95 Make yr compatible with 0.32 2016-11-10 21:49:56 -08:00
Paulus Schoutsen
1b79722b69 Fix KNX async I/O (#4267) 2016-11-10 21:43:50 -08:00
Pascal Vizeli
cc5233103c Fix rest switch default template (#4331) 2016-11-10 21:43:50 -08:00
Daniel Høyer Iversen
2feea1d1eb Add support for rgb light in led flux, fixes issue #4303 (#4332) 2016-11-10 21:43:50 -08:00
Johann Kellerman
2c39c39d52 Improve async generic camera's error handling (#4316)
* Handle errors

* Feedback

* DisconnectedError
2016-11-10 21:43:48 -08:00
Paulus Schoutsen
6e6b1ef7ab fix panasonic viera doing I/O in event loop (#4341) 2016-11-10 21:42:41 -08:00
Pascal Vizeli
55ddaf1ee7 Synology SSL fix & Error handling (#4325)
* Synology SSL fix & Error handling

* change handling for cookies/ssl

* fix use not deprecated functions

* fix lint

* change verify

* fix connector close to coro

* fix force close

* not needed since websession close connector too

* fix params

* fix lint
2016-11-10 21:42:37 -08:00
Pascal Vizeli
6860d9b096 Update SoCo to 0.12 (#4337)
* Update SoCo to 0.12

* fix req
2016-11-10 21:41:28 -08:00
Sean Dague
3e1cc4282e Fix "argument of type 'NoneType' is not iterable" during discovery (#4279)
* Fix "argument of type 'NoneType' is not iterable" during discovery

When yamaha receivers are dynamically discovered, there config is
empty, which means that we need to set zone_ignore to [] otherwise the
iteration over receivers fails.

* Bump rxv library version to fix play_status bug

rxv version 0.3 will issue the play_status command even for sources
that don't support it, causing stack traces during updates when
receivers are on HDMI inputs.

This was fixed in rxv 0.3.1. Bump to fix bug #4226.

* Don't discovery receivers that we've already configured

The discovery component doesn't know anything about already configured
receivers. This means that specifying a receiver manually will make it
show up twice if you have the discovery component enabled.

This puts a platform specific work around here that ensures that if
the media_player is found, we ignore the discovery system.
2016-11-10 21:41:28 -08:00
Jan Losinski
200bdb30ff Change pilight systemcode validation to integer (#4286)
* Change pilight systemcode validation to integer

According to the pilight code the systemcode should be an integer and
not a string (it is an int in the pilight code). Passing this as a
string caused errors from pilight:
"ERROR: elro_800_switch: insufficient number of arguments"

This fixes #4282

* Change pilight unit-id to positive integer

According to the pilight code the unit of an entity is also evrywhere
handled as an integer. So converting and passing this as string causes
pilight not to work.

This fixes #4282

Signed-off-by: Jan Losinski <losinski@wh2.tu-dresden.de>
2016-11-10 21:41:28 -08:00
Paulus Schoutsen
eb17ba970c Increase update delay (#4321) 2016-11-10 21:41:28 -08:00
Paulus Schoutsen
ffe4c425af Fix Tellstick doing I/O inside event loop (#4268) 2016-11-10 21:41:28 -08:00
Jesse Newland
a18fdbfbb8 Fix alarm.com I/O inside properties (#4307)
* Fix alarm.com I/O inside properties

* First line should end with a period

* Not needed

* Fetch state on init
2016-11-10 21:41:28 -08:00
Lewis Juggins
58600f25b3 Fix OWM async I/O (#4298) 2016-11-10 21:41:28 -08:00
25 changed files with 225 additions and 133 deletions

View File

@@ -39,7 +39,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
username = config.get(CONF_USERNAME)
password = config.get(CONF_PASSWORD)
add_devices([AlarmDotCom(hass, name, code, username, password)])
add_devices([AlarmDotCom(hass, name, code, username, password)], True)
class AlarmDotCom(alarm.AlarmControlPanel):
@@ -54,12 +54,17 @@ class AlarmDotCom(alarm.AlarmControlPanel):
self._code = str(code) if code else None
self._username = username
self._password = password
self._state = STATE_UNKNOWN
@property
def should_poll(self):
"""No polling needed."""
return True
def update(self):
"""Fetch the latest state."""
self._state = self._alarm.state
@property
def name(self):
"""Return the name of the alarm."""
@@ -73,11 +78,11 @@ class AlarmDotCom(alarm.AlarmControlPanel):
@property
def state(self):
"""Return the state of the device."""
if self._alarm.state == 'Disarmed':
if self._state == 'Disarmed':
return STATE_ALARM_DISARMED
elif self._alarm.state == 'Armed Stay':
elif self._state == 'Armed Stay':
return STATE_ALARM_ARMED_HOME
elif self._alarm.state == 'Armed Away':
elif self._state == 'Armed Away':
return STATE_ALARM_ARMED_AWAY
else:
return STATE_UNKNOWN

View File

@@ -91,7 +91,7 @@ class GenericCamera(Camera):
if url == self._last_url and self._limit_refetch:
return self._last_image
# aiohttp don't support DigestAuth jet
# aiohttp don't support DigestAuth yet
if self._authentication == HTTP_DIGEST_AUTHENTICATION:
def fetch():
"""Read image from a URL."""
@@ -109,15 +109,17 @@ class GenericCamera(Camera):
else:
try:
with async_timeout.timeout(10, loop=self.hass.loop):
respone = yield from self.hass.websession.get(
url,
auth=self._auth
)
self._last_image = yield from respone.read()
yield from respone.release()
response = yield from self.hass.websession.get(
url, auth=self._auth)
self._last_image = yield from response.read()
yield from response.release()
except asyncio.TimeoutError:
_LOGGER.error('Timeout getting camera image')
return self._last_image
except (aiohttp.errors.ClientError,
aiohttp.errors.ClientDisconnectedError) as err:
_LOGGER.error('Error getting new camera image: %s', err)
return self._last_image
self._last_url = url
return self._last_image

View File

@@ -9,13 +9,14 @@ import logging
import voluptuous as vol
import aiohttp
from aiohttp import web
from aiohttp.web_exceptions import HTTPGatewayTimeout
import async_timeout
from homeassistant.const import (
CONF_NAME, CONF_USERNAME, CONF_PASSWORD,
CONF_URL, CONF_WHITELIST, CONF_VERIFY_SSL)
CONF_URL, CONF_WHITELIST, CONF_VERIFY_SSL, EVENT_HOMEASSISTANT_STOP)
from homeassistant.components.camera import (
Camera, PLATFORM_SCHEMA)
import homeassistant.helpers.config_validation as cv
@@ -57,6 +58,16 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Setup a Synology IP Camera."""
if not config.get(CONF_VERIFY_SSL):
connector = aiohttp.TCPConnector(verify_ssl=False)
else:
connector = None
websession_init = aiohttp.ClientSession(
loop=hass.loop,
connector=connector
)
# Determine API to use for authentication
syno_api_url = SYNO_API_URL.format(
config.get(CONF_URL), WEBAPI_PATH, QUERY_CGI)
@@ -69,13 +80,12 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
}
try:
with async_timeout.timeout(TIMEOUT, loop=hass.loop):
query_req = yield from hass.websession.get(
query_req = yield from websession_init.get(
syno_api_url,
params=query_payload,
verify_ssl=config.get(CONF_VERIFY_SSL)
params=query_payload
)
except asyncio.TimeoutError:
_LOGGER.error("Timeout on %s", syno_api_url)
except (asyncio.TimeoutError, aiohttp.errors.ClientError):
_LOGGER.exception("Error on %s", syno_api_url)
return False
query_resp = yield from query_req.json()
@@ -93,12 +103,26 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
session_id = yield from get_session_id(
hass,
websession_init,
config.get(CONF_USERNAME),
config.get(CONF_PASSWORD),
syno_auth_url,
config.get(CONF_VERIFY_SSL)
syno_auth_url
)
websession_init.detach()
# init websession
websession = aiohttp.ClientSession(
loop=hass.loop, connector=connector, cookies={'id': session_id})
@asyncio.coroutine
def _async_close_websession(event):
"""Close webssesion on shutdown."""
yield from websession.close()
hass.bus.async_listen_once(
EVENT_HOMEASSISTANT_STOP, _async_close_websession)
# Use SessionID to get cameras in system
syno_camera_url = SYNO_API_URL.format(
config.get(CONF_URL), WEBAPI_PATH, camera_api)
@@ -110,14 +134,12 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
}
try:
with async_timeout.timeout(TIMEOUT, loop=hass.loop):
camera_req = yield from hass.websession.get(
camera_req = yield from websession.get(
syno_camera_url,
params=camera_payload,
verify_ssl=config.get(CONF_VERIFY_SSL),
cookies={'id': session_id}
params=camera_payload
)
except asyncio.TimeoutError:
_LOGGER.error("Timeout on %s", syno_camera_url)
except (asyncio.TimeoutError, aiohttp.errors.ClientError):
_LOGGER.exception("Error on %s", syno_camera_url)
return False
camera_resp = yield from camera_req.json()
@@ -126,13 +148,14 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
# add cameras
devices = []
tasks = []
for camera in cameras:
if not config.get(CONF_WHITELIST):
camera_id = camera['id']
snapshot_path = camera['snapshot_path']
device = SynologyCamera(
hass,
websession,
config,
camera_id,
camera['name'],
@@ -141,15 +164,13 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
camera_path,
auth_path
)
tasks.append(device.async_read_sid())
devices.append(device)
yield from asyncio.gather(*tasks, loop=hass.loop)
hass.loop.create_task(async_add_devices(devices))
yield from async_add_devices(devices)
@asyncio.coroutine
def get_session_id(hass, username, password, login_url, valid_cert):
def get_session_id(hass, websession, username, password, login_url):
"""Get a session id."""
auth_payload = {
'api': AUTH_API,
@@ -162,13 +183,12 @@ def get_session_id(hass, username, password, login_url, valid_cert):
}
try:
with async_timeout.timeout(TIMEOUT, loop=hass.loop):
auth_req = yield from hass.websession.get(
auth_req = yield from websession.get(
login_url,
params=auth_payload,
verify_ssl=valid_cert
params=auth_payload
)
except asyncio.TimeoutError:
_LOGGER.error("Timeout on %s", login_url)
except (asyncio.TimeoutError, aiohttp.errors.ClientError):
_LOGGER.exception("Error on %s", login_url)
return False
auth_resp = yield from auth_req.json()
@@ -180,36 +200,22 @@ def get_session_id(hass, username, password, login_url, valid_cert):
class SynologyCamera(Camera):
"""An implementation of a Synology NAS based IP camera."""
def __init__(self, config, camera_id, camera_name,
snapshot_path, streaming_path, camera_path, auth_path):
def __init__(self, hass, websession, config, camera_id,
camera_name, snapshot_path, streaming_path, camera_path,
auth_path):
"""Initialize a Synology Surveillance Station camera."""
super().__init__()
self.hass = hass
self._websession = websession
self._name = camera_name
self._username = config.get(CONF_USERNAME)
self._password = config.get(CONF_PASSWORD)
self._synology_url = config.get(CONF_URL)
self._api_url = config.get(CONF_URL) + 'webapi/'
self._login_url = config.get(CONF_URL) + '/webapi/' + 'auth.cgi'
self._camera_name = config.get(CONF_CAMERA_NAME)
self._stream_id = config.get(CONF_STREAM_ID)
self._valid_cert = config.get(CONF_VERIFY_SSL)
self._camera_id = camera_id
self._snapshot_path = snapshot_path
self._streaming_path = streaming_path
self._camera_path = camera_path
self._auth_path = auth_path
self._session_id = None
@asyncio.coroutine
def async_read_sid(self):
"""Get a session id."""
self._session_id = yield from get_session_id(
self.hass,
self._username,
self._password,
self._login_url,
self._valid_cert
)
def camera_image(self):
"""Return bytes of camera image."""
@@ -230,14 +236,12 @@ class SynologyCamera(Camera):
}
try:
with async_timeout.timeout(TIMEOUT, loop=self.hass.loop):
response = yield from self.hass.websession.get(
response = yield from self._websession.get(
image_url,
params=image_payload,
verify_ssl=self._valid_cert,
cookies={'id': self._session_id}
params=image_payload
)
except asyncio.TimeoutError:
_LOGGER.error("Timeout on %s", image_url)
except (asyncio.TimeoutError, aiohttp.errors.ClientError):
_LOGGER.exception("Error on %s", image_url)
return None
image = yield from response.read()
@@ -260,13 +264,12 @@ class SynologyCamera(Camera):
}
try:
with async_timeout.timeout(TIMEOUT, loop=self.hass.loop):
stream = yield from self.hass.websession.get(
stream = yield from self._websession.get(
streaming_url,
payload=streaming_payload,
verify_ssl=self._valid_cert,
cookies={'id': self._session_id}
params=streaming_payload
)
except asyncio.TimeoutError:
except (asyncio.TimeoutError, aiohttp.errors.ClientError):
_LOGGER.exception("Error on %s", streaming_url)
raise HTTPGatewayTimeout()
response = web.StreamResponse()
@@ -281,7 +284,7 @@ class SynologyCamera(Camera):
break
response.write(data)
finally:
self.hass.loop.create_task(stream.release())
self.hass.async_add_job(stream.release())
yield from response.write_eof()
@property

View File

@@ -56,6 +56,8 @@ class KNXThermostat(KNXMultiAddressDevice, ClimateDevice):
self._unit_of_measurement = TEMP_CELSIUS # KNX always used celsius
self._away = False # not yet supported
self._is_fan_on = False # not yet supported
self._current_temp = None
self._target_temp = None
@property
def should_poll(self):
@@ -70,16 +72,12 @@ class KNXThermostat(KNXMultiAddressDevice, ClimateDevice):
@property
def current_temperature(self):
"""Return the current temperature."""
from knxip.conversion import knx2_to_float
return knx2_to_float(self.value('temperature'))
return self._current_temp
@property
def target_temperature(self):
"""Return the temperature we try to reach."""
from knxip.conversion import knx2_to_float
return knx2_to_float(self.value('setpoint'))
return self._target_temp
def set_temperature(self, **kwargs):
"""Set new target temperature."""
@@ -94,3 +92,12 @@ class KNXThermostat(KNXMultiAddressDevice, ClimateDevice):
def set_operation_mode(self, operation_mode):
"""Set operation mode."""
raise NotImplementedError()
def update(self):
"""Update KNX climate."""
from knxip.conversion import knx2_to_float
super().update()
self._current_temp = knx2_to_float(self.value('temperature'))
self._target_temp = knx2_to_float(self.value('setpoint'))

View File

@@ -31,7 +31,7 @@ from homeassistant.util.yaml import dump
from homeassistant.helpers.event import track_utc_time_change
from homeassistant.const import (
ATTR_GPS_ACCURACY, ATTR_LATITUDE, ATTR_LONGITUDE,
DEVICE_DEFAULT_NAME, STATE_HOME, STATE_NOT_HOME)
DEVICE_DEFAULT_NAME, STATE_HOME, STATE_NOT_HOME, ATTR_ENTITY_ID)
DOMAIN = 'device_tracker'
DEPENDENCIES = ['zone']
@@ -242,7 +242,10 @@ class DeviceTracker(object):
if device.track:
device.update_ha_state()
self.hass.bus.async_fire(EVENT_NEW_DEVICE, device)
self.hass.bus.fire(EVENT_NEW_DEVICE, {
ATTR_ENTITY_ID: device.entity_id,
ATTR_HOST_NAME: device.host_name,
})
# During init, we ignore the group
if self.group is not None:

View File

@@ -76,6 +76,7 @@ def setup(hass, yaml_config):
ssl_certificate=None,
ssl_key=None,
cors_origins=[],
use_x_forwarded_for=False,
trusted_networks=[]
)

View File

@@ -28,7 +28,7 @@ from homeassistant import util
from homeassistant.const import (
SERVER_PORT, HTTP_HEADER_HA_AUTH, # HTTP_HEADER_CACHE_CONTROL,
CONTENT_TYPE_JSON, ALLOWED_CORS_HEADERS, EVENT_HOMEASSISTANT_STOP,
EVENT_HOMEASSISTANT_START)
EVENT_HOMEASSISTANT_START, HTTP_HEADER_X_FORWARDED_FOR)
import homeassistant.helpers.config_validation as cv
from homeassistant.components import persistent_notification
@@ -42,6 +42,7 @@ CONF_DEVELOPMENT = 'development'
CONF_SSL_CERTIFICATE = 'ssl_certificate'
CONF_SSL_KEY = 'ssl_key'
CONF_CORS_ORIGINS = 'cors_allowed_origins'
CONF_USE_X_FORWARDED_FOR = 'use_x_forwarded_for'
CONF_TRUSTED_NETWORKS = 'trusted_networks'
DATA_API_PASSWORD = 'api_password'
@@ -82,6 +83,7 @@ CONFIG_SCHEMA = vol.Schema({
vol.Optional(CONF_SSL_CERTIFICATE): cv.isfile,
vol.Optional(CONF_SSL_KEY): cv.isfile,
vol.Optional(CONF_CORS_ORIGINS): vol.All(cv.ensure_list, [cv.string]),
vol.Optional(CONF_USE_X_FORWARDED_FOR, default=False): cv.boolean,
vol.Optional(CONF_TRUSTED_NETWORKS):
vol.All(cv.ensure_list, [ip_network])
}),
@@ -125,6 +127,7 @@ def setup(hass, config):
ssl_certificate = conf.get(CONF_SSL_CERTIFICATE)
ssl_key = conf.get(CONF_SSL_KEY)
cors_origins = conf.get(CONF_CORS_ORIGINS, [])
use_x_forwarded_for = conf.get(CONF_USE_X_FORWARDED_FOR, False)
trusted_networks = [
ip_network(trusted_network)
for trusted_network in conf.get(CONF_TRUSTED_NETWORKS, [])]
@@ -138,6 +141,7 @@ def setup(hass, config):
ssl_certificate=ssl_certificate,
ssl_key=ssl_key,
cors_origins=cors_origins,
use_x_forwarded_for=use_x_forwarded_for,
trusted_networks=trusted_networks
)
@@ -248,7 +252,7 @@ class HomeAssistantWSGI(object):
def __init__(self, hass, development, api_password, ssl_certificate,
ssl_key, server_host, server_port, cors_origins,
trusted_networks):
use_x_forwarded_for, trusted_networks):
"""Initialize the WSGI Home Assistant server."""
import aiohttp_cors
@@ -260,6 +264,7 @@ class HomeAssistantWSGI(object):
self.ssl_key = ssl_key
self.server_host = server_host
self.server_port = server_port
self.use_x_forwarded_for = use_x_forwarded_for
self.trusted_networks = trusted_networks
self.event_forwarder = None
self._handler = None
@@ -366,11 +371,15 @@ class HomeAssistantWSGI(object):
yield from self._handler.finish_connections(60.0)
yield from self.app.cleanup()
@staticmethod
def get_real_ip(request):
def get_real_ip(self, request):
"""Return the clients correct ip address, even in proxied setups."""
peername = request.transport.get_extra_info('peername')
return peername[0] if peername is not None else None
if self.use_x_forwarded_for \
and HTTP_HEADER_X_FORWARDED_FOR in request.headers:
return request.headers.get(
HTTP_HEADER_X_FORWARDED_FOR).split(',')[0]
else:
peername = request.transport.get_extra_info('peername')
return peername[0] if peername is not None else None
def is_trusted_ip(self, remote_addr):
"""Match an ip address against trusted CIDR networks."""
@@ -452,7 +461,7 @@ def request_handler_factory(view, handler):
@asyncio.coroutine
def handle(request):
"""Handle incoming request."""
remote_addr = HomeAssistantWSGI.get_real_ip(request)
remote_addr = view.hass.http.get_real_ip(request)
# Auth code verbose on purpose
authenticated = False

View File

@@ -161,8 +161,6 @@ class KNXGroupAddress(Entity):
@property
def is_on(self):
"""Return True if the value is not 0 is on, else False."""
if self.should_poll:
self.update()
return self._state != 0
@property

View File

@@ -23,6 +23,7 @@ REQUIREMENTS = ['https://github.com/Danielhiversen/flux_led/archive/0.8.zip'
_LOGGER = logging.getLogger(__name__)
CONF_AUTOMATIC_ADD = 'automatic_add'
ATTR_MODE = 'mode'
DOMAIN = 'flux_led'
@@ -31,6 +32,8 @@ SUPPORT_FLUX_LED = (SUPPORT_BRIGHTNESS | SUPPORT_EFFECT |
DEVICE_SCHEMA = vol.Schema({
vol.Optional(CONF_NAME): cv.string,
vol.Optional(ATTR_MODE, default='rgbw'):
vol.All(cv.string, vol.In(['rgbw', 'rgb'])),
})
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
@@ -48,6 +51,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
device = {}
device['name'] = device_config[CONF_NAME]
device['ipaddr'] = ipaddr
device[ATTR_MODE] = device_config[ATTR_MODE]
light = FluxLight(device)
if light.is_valid:
lights.append(light)
@@ -65,6 +69,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
if ipaddr in light_ips:
continue
device['name'] = device['id'] + " " + ipaddr
device[ATTR_MODE] = 'rgbw'
light = FluxLight(device)
if light.is_valid:
lights.append(light)
@@ -82,6 +87,7 @@ class FluxLight(Light):
self._name = device['name']
self._ipaddr = device['ipaddr']
self._mode = device[ATTR_MODE]
self.is_valid = True
self._bulb = None
try:
@@ -132,7 +138,11 @@ class FluxLight(Light):
if rgb:
self._bulb.setRgb(*tuple(rgb))
elif brightness:
self._bulb.setWarmWhite255(brightness)
if self._mode == 'rgbw':
self._bulb.setWarmWhite255(brightness)
elif self._mode == 'rgb':
(red, green, blue) = self._bulb.getRgb()
self._bulb.setRgb(red, green, blue, brightness=brightness)
elif effect == EFFECT_RANDOM:
self._bulb.setRgb(random.randrange(0, 255),
random.randrange(0, 255),

View File

@@ -79,16 +79,16 @@ class PanasonicVieraTVDevice(MediaPlayerDevice):
self._playing = True
self._state = STATE_UNKNOWN
self._remote = remote
self._volume = 0
def update(self):
"""Retrieve the latest data."""
try:
self._muted = self._remote.get_mute()
self._volume = self._remote.get_volume() / 100
self._state = STATE_ON
except OSError:
self._state = STATE_OFF
return False
return True
def send_key(self, key):
"""Send a key to the tv and handles exceptions."""
@@ -113,13 +113,7 @@ class PanasonicVieraTVDevice(MediaPlayerDevice):
@property
def volume_level(self):
"""Volume level of the media player (0..1)."""
volume = 0
try:
volume = self._remote.get_volume() / 100
self._state = STATE_ON
except OSError:
self._state = STATE_OFF
return volume
return self._volume
@property
def is_volume_muted(self):

View File

@@ -21,9 +21,7 @@ from homeassistant.const import (
from homeassistant.config import load_yaml_config_file
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['https://github.com/SoCo/SoCo/archive/'
'cf8c2701165562eccbf1ecc879bf7060ceb0993e.zip#'
'SoCo==0.12']
REQUIREMENTS = ['SoCo==0.12']
_LOGGER = logging.getLogger(__name__)

View File

@@ -18,7 +18,7 @@ from homeassistant.const import (CONF_NAME, CONF_HOST, STATE_OFF, STATE_ON,
STATE_PLAYING, STATE_IDLE)
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['rxv==0.3.0']
REQUIREMENTS = ['rxv==0.3.1']
_LOGGER = logging.getLogger(__name__)
@@ -35,6 +35,7 @@ CONF_SOURCE_IGNORE = 'source_ignore'
CONF_ZONE_IGNORE = 'zone_ignore'
DEFAULT_NAME = 'Yamaha Receiver'
KNOWN = 'yamaha_known_receivers'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
@@ -50,6 +51,11 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the Yamaha platform."""
import rxv
# keep track of configured receivers so that we don't end up
# discovering a receiver dynamically that we have static config
# for.
if hass.data.get(KNOWN, None) is None:
hass.data[KNOWN] = set()
name = config.get(CONF_NAME)
host = config.get(CONF_HOST)
@@ -62,12 +68,17 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
model = discovery_info[1]
ctrl_url = discovery_info[2]
desc_url = discovery_info[3]
if ctrl_url in hass.data[KNOWN]:
_LOGGER.info("%s already manually configured", ctrl_url)
return
receivers = rxv.RXV(
ctrl_url,
model_name=model,
friendly_name=name,
unit_desc_url=desc_url).zone_controllers()
_LOGGER.info("Receivers: %s", receivers)
# when we are dynamically discovered config is empty
zone_ignore = []
elif host is None:
receivers = []
for recv in rxv.find():
@@ -78,6 +89,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
for receiver in receivers:
if receiver.zone not in zone_ignore:
hass.data[KNOWN].add(receiver.ctrl_url)
add_devices([
YamahaDevice(name, receiver, source_ignore, source_names)])

View File

@@ -113,15 +113,24 @@ class KNXSensorFloatClass(KNXGroupAddress, KNXSensorBaseClass):
self._unit_of_measurement = unit_of_measurement
self._minimum_value = minimum_sensor_value
self._maximum_value = maximum_sensor_value
self._value = None
KNXGroupAddress.__init__(self, hass, config)
@property
def state(self):
"""Return the Value of the KNX Sensor."""
return self._value
def update(self):
"""Update KNX sensor."""
from knxip.conversion import knx2_to_float
super().update()
self._value = None
if self._data:
from knxip.conversion import knx2_to_float
value = knx2_to_float(self._data)
if self._minimum_value <= value <= self._maximum_value:
return value
return None
self._value = value

View File

@@ -98,6 +98,7 @@ class TellstickSensor(Entity):
self.datatype = datatype
self.sensor = sensor
self._unit_of_measurement = sensor_info.unit or None
self._value = None
self._name = '{} {}'.format(name, sensor_info.name)
@@ -109,9 +110,13 @@ class TellstickSensor(Entity):
@property
def state(self):
"""Return the state of the sensor."""
return self.sensor.value(self.datatype).value
return self._value
@property
def unit_of_measurement(self):
"""Return the unit of measurement of this entity, if any."""
return self._unit_of_measurement
def update(self):
"""Update tellstick sensor."""
self._value = self.sensor.value(self.datatype).value

View File

@@ -10,7 +10,7 @@ import logging
from xml.parsers.expat import ExpatError
import async_timeout
from aiohttp.web import HTTPException
import aiohttp
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
@@ -154,12 +154,9 @@ class YrData(object):
try_again('{} returned {}'.format(self._url, resp.status))
return
text = yield from resp.text()
self.hass.loop.create_task(resp.release())
except asyncio.TimeoutError as err:
try_again(err)
return
except HTTPException as err:
resp.close()
self.hass.async_add_job(resp.release)
except (asyncio.TimeoutError, aiohttp.errors.ClientError,
aiohttp.errors.ClientDisconnectedError) as err:
try_again(err)
return
@@ -218,4 +215,5 @@ class YrData(object):
dev._state = new_state
tasks.append(dev.async_update_ha_state())
yield from asyncio.gather(*tasks, loop=self.hass.loop)
if tasks:
yield from asyncio.wait(tasks, loop=self.hass.loop)

View File

@@ -27,10 +27,10 @@ DEPENDENCIES = ['pilight']
COMMAND_SCHEMA = pilight.RF_CODE_SCHEMA.extend({
vol.Optional('on'): cv.positive_int,
vol.Optional('off'): cv.positive_int,
vol.Optional(CONF_UNIT): cv.string,
vol.Optional(CONF_UNIT): cv.positive_int,
vol.Optional(CONF_ID): cv.positive_int,
vol.Optional(CONF_STATE): cv.string,
vol.Optional(CONF_SYSTEMCODE): cv.string,
vol.Optional(CONF_SYSTEMCODE): cv.positive_int,
})
SWITCHES_SCHEMA = vol.Schema({

View File

@@ -12,11 +12,12 @@ import voluptuous as vol
from homeassistant.components.switch import (SwitchDevice, PLATFORM_SCHEMA)
from homeassistant.const import (CONF_NAME, CONF_RESOURCE, CONF_TIMEOUT)
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.template import Template
CONF_BODY_OFF = 'body_off'
CONF_BODY_ON = 'body_on'
DEFAULT_BODY_OFF = 'OFF'
DEFAULT_BODY_ON = 'ON'
DEFAULT_BODY_OFF = Template('OFF')
DEFAULT_BODY_ON = Template('ON')
DEFAULT_NAME = 'REST Switch'
DEFAULT_TIMEOUT = 10
CONF_IS_ON_TEMPLATE = 'is_on_template'

View File

@@ -67,7 +67,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
data = WeatherData(owm, latitude, longitude)
add_devices([OpenWeatherMapWeather(
name, data, hass.config.units.temperature_unit)])
name, data, hass.config.units.temperature_unit)], True)
class OpenWeatherMapWeather(WeatherEntity):
@@ -78,8 +78,7 @@ class OpenWeatherMapWeather(WeatherEntity):
self._name = name
self._owm = owm
self._temperature_unit = temperature_unit
self.date = None
self.update()
self.data = None
@property
def name(self):

View File

@@ -2,7 +2,7 @@
"""Constants used by Home Assistant components."""
MAJOR_VERSION = 0
MINOR_VERSION = 32
PATCH_VERSION = '2'
PATCH_VERSION = '4'
__short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION)
__version__ = '{}.{}'.format(__short_version__, PATCH_VERSION)
REQUIRED_PYTHON_VER = (3, 4, 2)
@@ -360,6 +360,7 @@ HTTP_HEADER_CONTENT_LENGTH = 'Content-Length'
HTTP_HEADER_CACHE_CONTROL = 'Cache-Control'
HTTP_HEADER_EXPIRES = 'Expires'
HTTP_HEADER_ORIGIN = 'Origin'
HTTP_HEADER_X_FORWARDED_FOR = 'X-Forwarded-For'
HTTP_HEADER_X_REQUESTED_WITH = 'X-Requested-With'
HTTP_HEADER_ACCEPT = 'Accept'
HTTP_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN = 'Access-Control-Allow-Origin'

View File

@@ -242,7 +242,7 @@ class Entity(object):
end = timer()
if end - start > 0.2:
if end - start > 0.4:
_LOGGER.warning('Updating state for %s took %.3f seconds. '
'Please report platform to the developers at '
'https://goo.gl/Nvioub', self.entity_id,

View File

@@ -24,6 +24,9 @@ PyMata==2.13
# homeassistant.components.rpi_gpio
# RPi.GPIO==0.6.1
# homeassistant.components.media_player.sonos
SoCo==0.12
# homeassistant.components.notify.twitter
TwitterAPI==2.4.2
@@ -157,9 +160,6 @@ https://github.com/GadgetReactor/pyHS100/archive/1f771b7d8090a91c6a58931532e4273
# homeassistant.components.switch.dlink
https://github.com/LinuxChristian/pyW215/archive/v0.3.5.zip#pyW215==0.3.5
# homeassistant.components.media_player.sonos
https://github.com/SoCo/SoCo/archive/cf8c2701165562eccbf1ecc879bf7060ceb0993e.zip#SoCo==0.12
# homeassistant.components.media_player.webostv
# homeassistant.components.notify.webostv
https://github.com/TheRealLink/pylgtv/archive/v0.1.2.zip#pylgtv==0.1.2
@@ -464,7 +464,7 @@ radiotherm==1.2
# rpi-rf==0.9.5
# homeassistant.components.media_player.yamaha
rxv==0.3.0
rxv==0.3.1
# homeassistant.components.media_player.samsungtv
samsungctl==0.5.1

View File

@@ -1,5 +1,10 @@
flake8>=3.0.4
pylint>=1.5.6
# linters such as flake8 and pylint should be pinned, as new releases
# make new things fail. Manually update these pins when pulling in a
# new version
flake8==3.0.4
pylint==1.6.4
mypy-lang==0.4.5
pydocstyle==1.1.1
coveralls>=1.1
pytest>=2.9.2
pytest-aiohttp>=0.1.3
@@ -7,7 +12,5 @@ pytest-asyncio>=0.5.0
pytest-cov>=2.3.1
pytest-timeout>=1.0.0
pytest-catchlog>=1.2.2
pydocstyle>=1.0.0
requests_mock>=1.0
mypy-lang>=0.4
mock-open>=1.3.1

View File

@@ -1,5 +1,6 @@
"""The tests for the device tracker component."""
# pylint: disable=protected-access
import json
import logging
import unittest
from unittest.mock import call, patch
@@ -15,6 +16,7 @@ from homeassistant.const import (
STATE_HOME, STATE_NOT_HOME, CONF_PLATFORM)
import homeassistant.components.device_tracker as device_tracker
from homeassistant.exceptions import HomeAssistantError
from homeassistant.remote import JSONEncoder
from tests.common import (
get_test_home_assistant, fire_time_changed, fire_service_discovered,
@@ -324,7 +326,16 @@ class TestComponentsDeviceTracker(unittest.TestCase):
device_tracker.see(self.hass, 'mac_1', host_name='hello')
self.hass.block_till_done()
self.assertEqual(1, len(test_events))
assert len(test_events) == 1
# Assert we can serialize the event
json.dumps(test_events[0].as_dict(), cls=JSONEncoder)
assert test_events[0].data == {
'entity_id': 'device_tracker.hello',
'host_name': 'hello',
}
# pylint: disable=invalid-name
def test_not_write_duplicate_yaml_keys(self):

View File

@@ -22,6 +22,10 @@ HA_HEADERS = {
# Don't add 127.0.0.1/::1 as trusted, as it may interfere with other test cases
TRUSTED_NETWORKS = ['192.0.2.0/24', '2001:DB8:ABCD::/48', '100.64.0.1',
'FD01:DB8::1']
TRUSTED_ADDRESSES = ['100.64.0.1', '192.0.2.100', 'FD01:DB8::1',
'2001:DB8:ABCD::1']
UNTRUSTED_ADDRESSES = ['198.51.100.1', '2001:DB8:FA1::1', '127.0.0.1', '::1']
CORS_ORIGINS = [HTTP_BASE_URL, HTTP_BASE]
@@ -85,10 +89,19 @@ class TestHttp:
assert req.status_code == 401
def test_access_denied_with_x_forwarded_for(self, caplog):
"""Test access denied through the X-Forwarded-For http header."""
hass.http.use_x_forwarded_for = True
for remote_addr in UNTRUSTED_ADDRESSES:
req = requests.get(_url(const.URL_API), headers={
const.HTTP_HEADER_X_FORWARDED_FOR: remote_addr})
assert req.status_code == 401, \
"{} shouldn't be trusted".format(remote_addr)
def test_access_denied_with_untrusted_ip(self, caplog):
"""Test access with an untrusted ip address."""
for remote_addr in ['198.51.100.1', '2001:DB8:FA1::1', '127.0.0.1',
'::1']:
for remote_addr in UNTRUSTED_ADDRESSES:
with patch('homeassistant.components.http.'
'HomeAssistantWSGI.get_real_ip',
return_value=remote_addr):
@@ -138,10 +151,19 @@ class TestHttp:
# assert const.URL_API in logs
assert API_PASSWORD not in logs
def test_access_with_trusted_ip(self, caplog):
def test_access_granted_with_x_forwarded_for(self, caplog):
"""Test access denied through the X-Forwarded-For http header."""
hass.http.use_x_forwarded_for = True
for remote_addr in TRUSTED_ADDRESSES:
req = requests.get(_url(const.URL_API), headers={
const.HTTP_HEADER_X_FORWARDED_FOR: remote_addr})
assert req.status_code == 200, \
"{} should be trusted".format(remote_addr)
def test_access_granted_with_trusted_ip(self, caplog):
"""Test access with trusted addresses."""
for remote_addr in ['100.64.0.1', '192.0.2.100', 'FD01:DB8::1',
'2001:DB8:ABCD::1']:
for remote_addr in TRUSTED_ADDRESSES:
with patch('homeassistant.components.http.'
'HomeAssistantWSGI.get_real_ip',
return_value=remote_addr):

View File

@@ -165,7 +165,8 @@ class TestCheckConfig(unittest.TestCase):
self.assertDictEqual({
'components': {'http': {'api_password': 'abc123',
'server_port': 8123}},
'server_port': 8123,
'use_x_forwarded_for': False}},
'except': {},
'secret_cache': {secrets_path: {'http_pw': 'abc123'}},
'secrets': {'http_pw': 'abc123'},