Compare commits

...

27 Commits

Author SHA1 Message Date
Pascal Vizeli
6bb95f6b58 Merge pull request #23137 from home-assistant/rc
0.91.4
2019-04-16 10:53:12 +02:00
Pascal Vizeli
c90219ad2e Bumped version to 0.91.4 2019-04-16 08:47:36 +00:00
Pascal Vizeli
0e7a2f163c Fix ingress bug with Firefox (#23121)
* Fix ingress bug with Firefox

* Fix mock

* Fix tests

* Fix test lint
2019-04-16 08:44:30 +00:00
Pascal Vizeli
536356ceec Merge pull request #22969 from home-assistant/rc
0.91.3
2019-04-10 16:42:00 +02:00
Pascal Vizeli
984af45bb2 Bumped version to 0.91.3 2019-04-10 13:22:19 +00:00
Pascal Vizeli
eab575e65d Bugfix: pass protocol out of header to application layer (#22955) 2019-04-10 13:19:54 +00:00
Robbie Trencheny
e7a17b710d Add cloudhook and remote UI vals to get_config (#22921) 2019-04-10 13:19:54 +00:00
Robbie Trencheny
a267df2abb More Mobile app sensor fixes (#22914)
* Ensure we only add a sensor once

* Ensure that we dont process updates for entities that arent what we were setup for

* Add debug logging to ease development of apps

* Use str representation
2019-04-10 13:19:53 +00:00
John Raahauge
9e56283eaf Fix position of add_entities of binary sensor (#22866)
* Bugfix - binary_sensor.py

* Added features to Concord232 Alarm Panel

* Added New Line End Of File

* Deleted Whitespace

* Back to original

Removed added feature and sticking to bugfix
2019-04-10 13:18:10 +00:00
Pascal Vizeli
8f9c2000ce Merge pull request #22883 from home-assistant/rc
0.91.2
2019-04-08 11:27:14 +02:00
Robbie Trencheny
a04d44d97a Minor sensor fixes (#22884)
* Minor sensor fixes

* Fix tests
2019-04-08 08:17:11 +00:00
Pascal Vizeli
3f73973970 Bumped version to 0.91.2 2019-04-08 08:00:22 +00:00
Pascal Vizeli
3a79e37cde Fix content_type handling ingress (#22864) 2019-04-08 07:59:05 +00:00
zewelor
3f15b6b2d3 Fix yeelight possible array change during iteration (#22849) 2019-04-08 07:59:04 +00:00
zewelor
c5d4b7c243 Use relative imports in yeelight (#22839) 2019-04-08 07:59:04 +00:00
Robbie Trencheny
236e484dc2 Fix for rate limits should be optional (#22823) 2019-04-08 07:59:02 +00:00
Jason Hunter
f51e8c3012 coerce duration and lookback to int so they can be used in template automation (#22819) 2019-04-08 07:59:01 +00:00
Robbie Trencheny
474fc21c66 Fix for optional values in the update_location webhook call (#22817)
* Fix for optional values in the update_location webhook call

* Square brackets instead of .get
2019-04-08 07:59:00 +00:00
Robbie Trencheny
82f6bed3a3 Add a new mobile_app webhook command to get config (#22813)
* Add a new mobile_app webhook command to get config

* Limit fields returned
2019-04-08 07:58:59 +00:00
zewelor
abb531c06b Improve yeelight imports (#22804) 2019-04-08 07:58:10 +00:00
Chris Helming
7a8aa79f19 Add optional rtsp_port for Foscam (#22786)
* add optional rtsp port for config

* getting rid of default=None

* removing vol.Any
2019-04-08 07:56:56 +00:00
Robbie Trencheny
ed9d1e776f Add new mobile_app webhook command: get_zones (#22604)
## Description:

Adds a new `mobile_app` webhook command, `get_zones`, which just returns all zones.

## Checklist:
  - [x] The code change is tested and works locally.
  - [x] Local tests pass with `tox`. **Your PR cannot be merged unless tests pass**
  - [x] There is no commented out code in this PR.
2019-04-08 07:56:55 +00:00
Wolfgang Malgadey
d8119b2281 Fix tado turn on off (#22291)
* fix for turn on and off, with new pyTado

missing blank line

* removed, because can't push

* uploaded the file through github again
2019-04-08 07:54:58 +00:00
Alexei Chetroi
a69b1a359d ZHA Light debug logging. (#22776) 2019-04-05 17:06:59 -07:00
David F. Mulcahey
f004f440d3 make the custom polling actually request state (#22778) 2019-04-05 17:01:06 -07:00
Nate Clark
dbe53a3947 Fix konnected unique_id computation for switches (#22777) 2019-04-05 16:52:30 -07:00
Rohan Kapoor
a44966f483 Correctly load Mopar's config (#22771) 2019-04-05 16:52:29 -07:00
26 changed files with 390 additions and 117 deletions

View File

@@ -88,8 +88,8 @@ CAMERA_SERVICE_PLAY_STREAM = CAMERA_SERVICE_SCHEMA.extend({
CAMERA_SERVICE_RECORD = CAMERA_SERVICE_SCHEMA.extend({
vol.Required(CONF_FILENAME): cv.template,
vol.Optional(CONF_DURATION, default=30): int,
vol.Optional(CONF_LOOKBACK, default=0): int,
vol.Optional(CONF_DURATION, default=30): vol.Coerce(int),
vol.Optional(CONF_LOOKBACK, default=0): vol.Coerce(int),
})
WS_TYPE_CAMERA_THUMBNAIL = 'camera_thumbnail'

View File

@@ -79,7 +79,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
)
)
add_entities(sensors, True)
add_entities(sensors, True)
def get_opening_type(zone):

View File

@@ -19,6 +19,7 @@ _LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['libpyfoscam==1.0']
CONF_IP = 'ip'
CONF_RTSP_PORT = 'rtsp_port'
DEFAULT_NAME = 'Foscam Camera'
DEFAULT_PORT = 88
@@ -31,6 +32,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_USERNAME): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
vol.Optional(CONF_RTSP_PORT): cv.port
})
@@ -58,11 +60,12 @@ class FoscamCam(Camera):
self._foscam_session = FoscamCamera(
ip_address, port, self._username, self._password, verbose=False)
self._rtsp_port = None
result, response = self._foscam_session.get_port_info()
if result == 0:
self._rtsp_port = response.get('rtspPort') or \
response.get('mediaPort')
self._rtsp_port = device_info.get(CONF_RTSP_PORT)
if not self._rtsp_port:
result, response = self._foscam_session.get_port_info()
if result == 0:
self._rtsp_port = response.get('rtspPort') or \
response.get('mediaPort')
def camera_image(self):
"""Return a still image response from the camera."""

View File

@@ -65,12 +65,25 @@ class HassIOIngress(HomeAssistantView):
post = _handle
put = _handle
delete = _handle
patch = _handle
options = _handle
async def _handle_websocket(
self, request: web.Request, token: str, path: str
) -> web.WebSocketResponse:
"""Ingress route for websocket."""
ws_server = web.WebSocketResponse()
if hdrs.SEC_WEBSOCKET_PROTOCOL in request.headers:
req_protocols = [
str(proto.strip())
for proto in
request.headers[hdrs.SEC_WEBSOCKET_PROTOCOL].split(",")
]
else:
req_protocols = ()
ws_server = web.WebSocketResponse(
protocols=req_protocols, autoclose=False, autoping=False
)
await ws_server.prepare(request)
# Preparing
@@ -83,7 +96,8 @@ class HassIOIngress(HomeAssistantView):
# Start proxy
async with self._websession.ws_connect(
url, headers=source_header
url, headers=source_header, protocols=req_protocols,
autoclose=False, autoping=False,
) as ws_client:
# Proxy requests
await asyncio.wait(
@@ -118,6 +132,7 @@ class HassIOIngress(HomeAssistantView):
return web.Response(
headers=headers,
status=result.status,
content_type=result.content_type,
body=body
)
@@ -145,8 +160,7 @@ def _init_header(
# filter flags
for name, value in request.headers.items():
if name in (hdrs.CONTENT_LENGTH, hdrs.CONTENT_TYPE,
hdrs.CONTENT_ENCODING):
if name in (hdrs.CONTENT_LENGTH, hdrs.CONTENT_ENCODING):
continue
headers[name] = value
@@ -197,22 +211,25 @@ def _is_websocket(request: web.Request) -> bool:
"""Return True if request is a websocket."""
headers = request.headers
if headers.get(hdrs.CONNECTION) == "Upgrade" and \
headers.get(hdrs.UPGRADE) == "websocket":
if "upgrade" in headers.get(hdrs.CONNECTION, "").lower() and \
headers.get(hdrs.UPGRADE, "").lower() == "websocket":
return True
return False
async def _websocket_forward(ws_from, ws_to):
"""Handle websocket message directly."""
async for msg in ws_from:
if msg.type == aiohttp.WSMsgType.TEXT:
await ws_to.send_str(msg.data)
elif msg.type == aiohttp.WSMsgType.BINARY:
await ws_to.send_bytes(msg.data)
elif msg.type == aiohttp.WSMsgType.PING:
await ws_to.ping()
elif msg.type == aiohttp.WSMsgType.PONG:
await ws_to.pong()
elif ws_to.closed:
await ws_to.close(code=ws_to.close_code, message=msg.extra)
try:
async for msg in ws_from:
if msg.type == aiohttp.WSMsgType.TEXT:
await ws_to.send_str(msg.data)
elif msg.type == aiohttp.WSMsgType.BINARY:
await ws_to.send_bytes(msg.data)
elif msg.type == aiohttp.WSMsgType.PING:
await ws_to.ping()
elif msg.type == aiohttp.WSMsgType.PONG:
await ws_to.pong()
elif ws_to.closed:
await ws_to.close(code=ws_to.close_code, message=msg.extra)
except RuntimeError:
_LOGGER.debug("Ingress Websocket runtime error")

View File

@@ -66,7 +66,8 @@ class HomeAssistantView:
urls = [self.url] + self.extra_urls
routes = []
for method in ('get', 'post', 'delete', 'put'):
for method in ('get', 'post', 'delete', 'put', 'patch', 'head',
'options'):
handler = getattr(self, method, None)
if not handler:

View File

@@ -41,9 +41,10 @@ class KonnectedSwitch(ToggleEntity):
self._pause = self._data.get(CONF_PAUSE)
self._repeat = self._data.get(CONF_REPEAT)
self._state = self._boolean_state(self._data.get(ATTR_STATE))
self._unique_id = '{}-{}'.format(device_id, hash(frozenset(
{self._pin_num, self._momentary, self._pause, self._repeat})))
self._name = self._data.get(CONF_NAME)
self._unique_id = '{}-{}-{}-{}-{}'.format(
device_id, self._pin_num, self._momentary,
self._pause, self._repeat)
@property
def unique_id(self) -> str:

View File

@@ -8,9 +8,10 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect
from .const import (ATTR_SENSOR_STATE,
ATTR_SENSOR_TYPE_BINARY_SENSOR as ENTITY_TYPE,
ATTR_SENSOR_UNIQUE_ID,
DATA_DEVICES, DOMAIN)
from .entity import MobileAppEntity
from .entity import MobileAppEntity, sensor_id
DEPENDENCIES = ['mobile_app']
@@ -36,6 +37,16 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
if data[CONF_WEBHOOK_ID] != webhook_id:
return
unique_id = sensor_id(data[CONF_WEBHOOK_ID],
data[ATTR_SENSOR_UNIQUE_ID])
entity = hass.data[DOMAIN][ENTITY_TYPE][unique_id]
if 'added' in entity:
return
entity['added'] = True
device = hass.data[DOMAIN][DATA_DEVICES][data[CONF_WEBHOOK_ID]]
async_add_entities([MobileAppBinarySensor(data, device, config_entry)])

View File

@@ -42,6 +42,11 @@ ATTR_OS_NAME = 'os_name'
ATTR_OS_VERSION = 'os_version'
ATTR_PUSH_TOKEN = 'push_token'
ATTR_PUSH_URL = 'push_url'
ATTR_PUSH_RATE_LIMITS = 'rateLimits'
ATTR_PUSH_RATE_LIMITS_ERRORS = 'errors'
ATTR_PUSH_RATE_LIMITS_MAXIMUM = 'maximum'
ATTR_PUSH_RATE_LIMITS_RESETS_AT = 'resetsAt'
ATTR_PUSH_RATE_LIMITS_SUCCESSFUL = 'successful'
ATTR_SUPPORTS_ENCRYPTION = 'supports_encryption'
ATTR_EVENT_DATA = 'event_data'
@@ -67,6 +72,8 @@ ERR_SENSOR_DUPLICATE_UNIQUE_ID = 'duplicate_unique_id'
WEBHOOK_TYPE_CALL_SERVICE = 'call_service'
WEBHOOK_TYPE_FIRE_EVENT = 'fire_event'
WEBHOOK_TYPE_GET_CONFIG = 'get_config'
WEBHOOK_TYPE_GET_ZONES = 'get_zones'
WEBHOOK_TYPE_REGISTER_SENSOR = 'register_sensor'
WEBHOOK_TYPE_RENDER_TEMPLATE = 'render_template'
WEBHOOK_TYPE_UPDATE_LOCATION = 'update_location'
@@ -74,6 +81,7 @@ WEBHOOK_TYPE_UPDATE_REGISTRATION = 'update_registration'
WEBHOOK_TYPE_UPDATE_SENSOR_STATES = 'update_sensor_states'
WEBHOOK_TYPES = [WEBHOOK_TYPE_CALL_SERVICE, WEBHOOK_TYPE_FIRE_EVENT,
WEBHOOK_TYPE_GET_CONFIG, WEBHOOK_TYPE_GET_ZONES,
WEBHOOK_TYPE_REGISTER_SENSOR, WEBHOOK_TYPE_RENDER_TEMPLATE,
WEBHOOK_TYPE_UPDATE_LOCATION,
WEBHOOK_TYPE_UPDATE_REGISTRATION,
@@ -163,7 +171,7 @@ REGISTER_SENSOR_SCHEMA = vol.Schema({
vol.Required(ATTR_SENSOR_NAME): cv.string,
vol.Required(ATTR_SENSOR_TYPE): vol.In(SENSOR_TYPES),
vol.Required(ATTR_SENSOR_UNIQUE_ID): cv.string,
vol.Required(ATTR_SENSOR_UOM): cv.string,
vol.Optional(ATTR_SENSOR_UOM): cv.string,
vol.Required(ATTR_SENSOR_STATE): vol.Any(bool, str, int, float),
vol.Optional(ATTR_SENSOR_ICON, default='mdi:cellphone'): cv.icon,
})

View File

@@ -13,6 +13,11 @@ from .const import (ATTR_DEVICE_ID, ATTR_DEVICE_NAME, ATTR_MANUFACTURER,
DOMAIN, SIGNAL_SENSOR_UPDATE)
def sensor_id(webhook_id, unique_id):
"""Return a unique sensor ID."""
return "{}_{}".format(webhook_id, unique_id)
class MobileAppEntity(Entity):
"""Representation of an mobile app entity."""
@@ -22,8 +27,8 @@ class MobileAppEntity(Entity):
self._device = device
self._entry = entry
self._registration = entry.data
self._sensor_id = "{}_{}".format(self._registration[CONF_WEBHOOK_ID],
config[ATTR_SENSOR_UNIQUE_ID])
self._sensor_id = sensor_id(self._registration[CONF_WEBHOOK_ID],
config[ATTR_SENSOR_UNIQUE_ID])
self._entity_type = config[ATTR_SENSOR_TYPE]
self.unsub_dispatcher = None
@@ -94,5 +99,10 @@ class MobileAppEntity(Entity):
@callback
def _handle_update(self, data):
"""Handle async event updates."""
incoming_id = sensor_id(data[CONF_WEBHOOK_ID],
data[ATTR_SENSOR_UNIQUE_ID])
if incoming_id != self._sensor_id:
return
self._config = data
self.async_schedule_update_ha_state()

View File

@@ -6,6 +6,7 @@ from typing import Callable, Dict, Tuple
from aiohttp.web import json_response, Response
from homeassistant.core import Context
from homeassistant.helpers.json import JSONEncoder
from homeassistant.helpers.typing import HomeAssistantType
from .const import (ATTR_APP_DATA, ATTR_APP_ID, ATTR_APP_NAME,
@@ -133,9 +134,9 @@ def savable_state(hass: HomeAssistantType) -> Dict:
def webhook_response(data, *, registration: Dict, status: int = 200,
headers: Dict = None) -> Response:
"""Return a encrypted response if registration supports it."""
data = json.dumps(data)
data = json.dumps(data, cls=JSONEncoder)
if CONF_SECRET in registration:
if registration[ATTR_SUPPORTS_ENCRYPTION]:
keylen, encrypt = setup_encrypt()
key = registration[CONF_SECRET].encode("utf-8")

View File

@@ -8,13 +8,18 @@ import async_timeout
from homeassistant.components.notify import (
ATTR_DATA, ATTR_MESSAGE, ATTR_TARGET, ATTR_TITLE, ATTR_TITLE_DEFAULT,
BaseNotificationService)
from homeassistant.components.mobile_app.const import (
ATTR_APP_DATA, ATTR_APP_ID, ATTR_APP_VERSION, ATTR_DEVICE_NAME,
ATTR_OS_VERSION, ATTR_PUSH_TOKEN, ATTR_PUSH_URL, DATA_CONFIG_ENTRIES,
DOMAIN)
from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.util.dt as dt_util
from .const import (ATTR_APP_DATA, ATTR_APP_ID, ATTR_APP_VERSION,
ATTR_DEVICE_NAME, ATTR_OS_VERSION, ATTR_PUSH_RATE_LIMITS,
ATTR_PUSH_RATE_LIMITS_ERRORS,
ATTR_PUSH_RATE_LIMITS_MAXIMUM,
ATTR_PUSH_RATE_LIMITS_RESETS_AT,
ATTR_PUSH_RATE_LIMITS_SUCCESSFUL, ATTR_PUSH_TOKEN,
ATTR_PUSH_URL, DATA_CONFIG_ENTRIES, DOMAIN)
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['mobile_app']
@@ -38,16 +43,21 @@ def push_registrations(hass):
# pylint: disable=invalid-name
def log_rate_limits(hass, device_name, resp, level=logging.INFO):
"""Output rate limit log line at given level."""
rate_limits = resp['rateLimits']
resetsAt = dt_util.parse_datetime(rate_limits['resetsAt'])
resetsAtTime = resetsAt - datetime.now(timezone.utc)
if ATTR_PUSH_RATE_LIMITS not in resp:
return
rate_limits = resp[ATTR_PUSH_RATE_LIMITS]
resetsAt = rate_limits[ATTR_PUSH_RATE_LIMITS_RESETS_AT]
resetsAtTime = (dt_util.parse_datetime(resetsAt) -
datetime.now(timezone.utc))
rate_limit_msg = ("mobile_app push notification rate limits for %s: "
"%d sent, %d allowed, %d errors, "
"resets in %s")
_LOGGER.log(level, rate_limit_msg,
device_name,
rate_limits['successful'],
rate_limits['maximum'], rate_limits['errors'],
rate_limits[ATTR_PUSH_RATE_LIMITS_SUCCESSFUL],
rate_limits[ATTR_PUSH_RATE_LIMITS_MAXIMUM],
rate_limits[ATTR_PUSH_RATE_LIMITS_ERRORS],
str(resetsAtTime).split(".")[0])

View File

@@ -7,9 +7,10 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect
from .const import (ATTR_SENSOR_STATE,
ATTR_SENSOR_TYPE_SENSOR as ENTITY_TYPE,
ATTR_SENSOR_UOM, DATA_DEVICES, DOMAIN)
ATTR_SENSOR_UNIQUE_ID, ATTR_SENSOR_UOM, DATA_DEVICES,
DOMAIN)
from .entity import MobileAppEntity
from .entity import MobileAppEntity, sensor_id
DEPENDENCIES = ['mobile_app']
@@ -35,6 +36,16 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
if data[CONF_WEBHOOK_ID] != webhook_id:
return
unique_id = sensor_id(data[CONF_WEBHOOK_ID],
data[ATTR_SENSOR_UNIQUE_ID])
entity = hass.data[DOMAIN][ENTITY_TYPE][unique_id]
if 'added' in entity:
return
entity['added'] = True
device = hass.data[DOMAIN][DATA_DEVICES][data[CONF_WEBHOOK_ID]]
async_add_entities([MobileAppSensor(data, device, config_entry)])
@@ -55,4 +66,4 @@ class MobileAppSensor(MobileAppEntity):
@property
def unit_of_measurement(self):
"""Return the unit of measurement this sensor expresses itself in."""
return self._config[ATTR_SENSOR_UOM]
return self._config.get(ATTR_SENSOR_UOM)

View File

@@ -4,10 +4,14 @@ import logging
from aiohttp.web import HTTPBadRequest, Response, Request
import voluptuous as vol
from homeassistant.components.cloud import (async_remote_ui_url,
CloudNotAvailable)
from homeassistant.components.device_tracker import (ATTR_ATTRIBUTES,
ATTR_DEV_ID,
DOMAIN as DT_DOMAIN,
SERVICE_SEE as DT_SEE)
from homeassistant.components.frontend import MANIFEST_JSON
from homeassistant.components.zone.const import DOMAIN as ZONE_DOMAIN
from homeassistant.const import (ATTR_DOMAIN, ATTR_SERVICE, ATTR_SERVICE_DATA,
CONF_WEBHOOK_ID, HTTP_BAD_REQUEST,
@@ -29,13 +33,15 @@ from .const import (ATTR_ALTITUDE, ATTR_BATTERY, ATTR_COURSE, ATTR_DEVICE_ID,
ATTR_TEMPLATE_VARIABLES, ATTR_VERTICAL_ACCURACY,
ATTR_WEBHOOK_DATA, ATTR_WEBHOOK_ENCRYPTED,
ATTR_WEBHOOK_ENCRYPTED_DATA, ATTR_WEBHOOK_TYPE,
CONF_SECRET, DATA_CONFIG_ENTRIES, DATA_DELETED_IDS,
DATA_STORE, DOMAIN, ERR_ENCRYPTION_REQUIRED,
ERR_SENSOR_DUPLICATE_UNIQUE_ID, ERR_SENSOR_NOT_REGISTERED,
SIGNAL_SENSOR_UPDATE, WEBHOOK_PAYLOAD_SCHEMA,
WEBHOOK_SCHEMAS, WEBHOOK_TYPE_CALL_SERVICE,
WEBHOOK_TYPE_FIRE_EVENT, WEBHOOK_TYPE_REGISTER_SENSOR,
WEBHOOK_TYPE_RENDER_TEMPLATE, WEBHOOK_TYPE_UPDATE_LOCATION,
CONF_CLOUDHOOK_URL, CONF_REMOTE_UI_URL, CONF_SECRET,
DATA_CONFIG_ENTRIES, DATA_DELETED_IDS, DATA_STORE, DOMAIN,
ERR_ENCRYPTION_REQUIRED, ERR_SENSOR_DUPLICATE_UNIQUE_ID,
ERR_SENSOR_NOT_REGISTERED, SIGNAL_SENSOR_UPDATE,
WEBHOOK_PAYLOAD_SCHEMA, WEBHOOK_SCHEMAS, WEBHOOK_TYPES,
WEBHOOK_TYPE_CALL_SERVICE, WEBHOOK_TYPE_FIRE_EVENT,
WEBHOOK_TYPE_GET_CONFIG, WEBHOOK_TYPE_GET_ZONES,
WEBHOOK_TYPE_REGISTER_SENSOR, WEBHOOK_TYPE_RENDER_TEMPLATE,
WEBHOOK_TYPE_UPDATE_LOCATION,
WEBHOOK_TYPE_UPDATE_REGISTRATION,
WEBHOOK_TYPE_UPDATE_SENSOR_STATES)
@@ -87,16 +93,22 @@ async def handle_webhook(hass: HomeAssistantType, webhook_id: str,
enc_data = req_data[ATTR_WEBHOOK_ENCRYPTED_DATA]
webhook_payload = _decrypt_payload(registration[CONF_SECRET], enc_data)
if webhook_type not in WEBHOOK_SCHEMAS:
if webhook_type not in WEBHOOK_TYPES:
_LOGGER.error('Received invalid webhook type: %s', webhook_type)
return empty_okay_response()
try:
data = WEBHOOK_SCHEMAS[webhook_type](webhook_payload)
except vol.Invalid as ex:
err = vol.humanize.humanize_error(webhook_payload, ex)
_LOGGER.error('Received invalid webhook payload: %s', err)
return empty_okay_response(headers=headers)
data = webhook_payload
_LOGGER.debug("Received webhook payload for type %s: %s", webhook_type,
data)
if webhook_type in WEBHOOK_SCHEMAS:
try:
data = WEBHOOK_SCHEMAS[webhook_type](webhook_payload)
except vol.Invalid as ex:
err = vol.humanize.humanize_error(webhook_payload, ex)
_LOGGER.error('Received invalid webhook payload: %s', err)
return empty_okay_response(headers=headers)
context = registration_context(registration)
@@ -139,18 +151,26 @@ async def handle_webhook(hass: HomeAssistantType, webhook_id: str,
if webhook_type == WEBHOOK_TYPE_UPDATE_LOCATION:
see_payload = {
ATTR_DEV_ID: registration[ATTR_DEVICE_ID],
ATTR_LOCATION_NAME: data.get(ATTR_LOCATION_NAME),
ATTR_GPS: data.get(ATTR_GPS),
ATTR_GPS_ACCURACY: data.get(ATTR_GPS_ACCURACY),
ATTR_BATTERY: data.get(ATTR_BATTERY),
ATTR_ATTRIBUTES: {
ATTR_SPEED: data.get(ATTR_SPEED),
ATTR_ALTITUDE: data.get(ATTR_ALTITUDE),
ATTR_COURSE: data.get(ATTR_COURSE),
ATTR_VERTICAL_ACCURACY: data.get(ATTR_VERTICAL_ACCURACY),
}
ATTR_GPS: data[ATTR_GPS],
ATTR_GPS_ACCURACY: data[ATTR_GPS_ACCURACY],
}
for key in (ATTR_LOCATION_NAME, ATTR_BATTERY):
value = data.get(key)
if value is not None:
see_payload[key] = value
attrs = {}
for key in (ATTR_ALTITUDE, ATTR_COURSE,
ATTR_SPEED, ATTR_VERTICAL_ACCURACY):
value = data.get(key)
if value is not None:
attrs[key] = value
if attrs:
see_payload[ATTR_ATTRIBUTES] = attrs
try:
await hass.services.async_call(DT_DOMAIN,
DT_SEE, see_payload,
@@ -214,7 +234,7 @@ async def handle_webhook(hass: HomeAssistantType, webhook_id: str,
data[ATTR_SENSOR_TYPE])
async_dispatcher_send(hass, register_signal, data)
return webhook_response({"status": "registered"},
return webhook_response({'success': True},
registration=registration, status=HTTP_CREATED,
headers=headers)
@@ -257,7 +277,40 @@ async def handle_webhook(hass: HomeAssistantType, webhook_id: str,
async_dispatcher_send(hass, SIGNAL_SENSOR_UPDATE, new_state)
resp[unique_id] = {"status": "okay"}
resp[unique_id] = {'success': True}
return webhook_response(resp, registration=registration,
headers=headers)
if webhook_type == WEBHOOK_TYPE_GET_ZONES:
zones = (hass.states.get(entity_id) for entity_id
in sorted(hass.states.async_entity_ids(ZONE_DOMAIN)))
return webhook_response(list(zones), registration=registration,
headers=headers)
if webhook_type == WEBHOOK_TYPE_GET_CONFIG:
hass_config = hass.config.as_dict()
resp = {
'latitude': hass_config['latitude'],
'longitude': hass_config['longitude'],
'elevation': hass_config['elevation'],
'unit_system': hass_config['unit_system'],
'location_name': hass_config['location_name'],
'time_zone': hass_config['time_zone'],
'components': hass_config['components'],
'version': hass_config['version'],
'theme_color': MANIFEST_JSON['theme_color'],
}
if CONF_CLOUDHOOK_URL in registration:
resp[CONF_CLOUDHOOK_URL] = registration[CONF_CLOUDHOOK_URL]
try:
resp[CONF_REMOTE_UI_URL] = async_remote_ui_url(hass)
except CloudNotAvailable:
pass
return webhook_response(resp, registration=registration,
headers=headers)

View File

@@ -53,12 +53,13 @@ def setup(hass, config):
"""Set up the Mopar component."""
import motorparts
conf = config[DOMAIN]
cookie = hass.config.path(COOKIE_FILE)
try:
session = motorparts.get_session(
config[CONF_USERNAME],
config[CONF_PASSWORD],
config[CONF_PIN],
conf[CONF_USERNAME],
conf[CONF_PASSWORD],
conf[CONF_PIN],
cookie_path=cookie
)
except motorparts.MoparError:
@@ -69,7 +70,7 @@ def setup(hass, config):
data.update(now=None)
track_time_interval(
hass, data.update, config[CONF_SCAN_INTERVAL]
hass, data.update, conf[CONF_SCAN_INTERVAL]
)
def handle_horn(call):

View File

@@ -121,3 +121,8 @@ class TadoDataStore:
"""Wrap for setZoneOverlay(..)."""
self.tado.setZoneOverlay(zone_id, mode, temperature, duration)
self.update(no_throttle=True) # pylint: disable=unexpected-keyword-arg
def set_zone_off(self, zone_id, mode):
"""Set a zone to off."""
self.tado.setZoneOverlay(zone_id, mode, None, None, 'HEATING', 'OFF')
self.update(no_throttle=True) # pylint: disable=unexpected-keyword-arg

View File

@@ -363,7 +363,7 @@ class TadoClimate(ClimateDevice):
if self._current_operation == CONST_MODE_OFF:
_LOGGER.info("Switching mytado.com to OFF for zone %s",
self.zone_name)
self._store.set_zone_overlay(self.zone_id, CONST_OVERLAY_MANUAL)
self._store.set_zone_off(self.zone_id, CONST_OVERLAY_MANUAL)
self._overlay_mode = self._current_operation
return

View File

@@ -134,7 +134,7 @@ def setup(hass, config):
discovery.listen(hass, SERVICE_YEELIGHT, device_discovered)
def update(event):
for device in yeelight_data.values():
for device in list(yeelight_data.values()):
device.update()
track_time_interval(
@@ -185,8 +185,8 @@ class YeelightDevice:
@property
def bulb(self):
"""Return bulb device."""
import yeelight
if self._bulb_device is None:
import yeelight
try:
self._bulb_device = yeelight.Bulb(self._ipaddr,
model=self._model)
@@ -241,33 +241,27 @@ class YeelightDevice:
def turn_on(self, duration=DEFAULT_TRANSITION, light_type=None):
"""Turn on device."""
import yeelight
if not light_type:
light_type = yeelight.enums.LightType.Main
from yeelight import BulbException
try:
self.bulb.turn_on(duration=duration, light_type=light_type)
except yeelight.BulbException as ex:
except BulbException as ex:
_LOGGER.error("Unable to turn the bulb on: %s", ex)
return
def turn_off(self, duration=DEFAULT_TRANSITION, light_type=None):
"""Turn off device."""
import yeelight
if not light_type:
light_type = yeelight.enums.LightType.Main
from yeelight import BulbException
try:
self.bulb.turn_off(duration=duration, light_type=light_type)
except yeelight.BulbException as ex:
except BulbException as ex:
_LOGGER.error("Unable to turn the bulb off: %s", ex)
return
def update(self):
"""Read new properties from the device."""
import yeelight
from yeelight import BulbException
if not self.bulb:
return
@@ -275,7 +269,7 @@ class YeelightDevice:
try:
self.bulb.get_properties(UPDATE_REQUEST_PROPERTIES)
self._available = True
except yeelight.BulbException as ex:
except BulbException as ex:
if self._available: # just inform once
_LOGGER.error("Unable to update bulb status: %s", ex)
self._available = False

View File

@@ -4,7 +4,7 @@ import logging
from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.components.yeelight import DATA_YEELIGHT, DATA_UPDATED
from . import DATA_YEELIGHT, DATA_UPDATED
DEPENDENCIES = ['yeelight']

View File

@@ -15,7 +15,7 @@ from homeassistant.components.light import (
SUPPORT_COLOR, SUPPORT_TRANSITION, SUPPORT_COLOR_TEMP, SUPPORT_FLASH,
SUPPORT_EFFECT, Light)
import homeassistant.util.color as color_util
from homeassistant.components.yeelight import (
from . import (
CONF_TRANSITION, DATA_YEELIGHT, CONF_MODE_MUSIC,
CONF_SAVE_ON_CHANGE, CONF_CUSTOM_EFFECTS, DATA_UPDATED,
YEELIGHT_SERVICE_SCHEMA, DOMAIN, ATTR_TRANSITIONS,
@@ -189,6 +189,8 @@ class YeelightLight(Light):
def __init__(self, device, custom_effects=None):
"""Initialize the Yeelight light."""
from yeelight.enums import LightType
self.config = device.config
self._device = device
@@ -202,6 +204,8 @@ class YeelightLight(Light):
self._min_mireds = None
self._max_mireds = None
self._light_type = LightType.Main
if custom_effects:
self._custom_effects = custom_effects
else:
@@ -281,8 +285,7 @@ class YeelightLight(Light):
@property
def light_type(self):
"""Return light type."""
import yeelight
return yeelight.enums.LightType.Main
return self._light_type
def _get_hs_from_properties(self):
rgb = self._get_property('rgb')
@@ -589,21 +592,19 @@ class YeelightAmbientLight(YeelightLight):
def __init__(self, *args, **kwargs):
"""Initialize the Yeelight Ambient light."""
from yeelight.enums import LightType
super().__init__(*args, **kwargs)
self._min_mireds = kelvin_to_mired(6500)
self._max_mireds = kelvin_to_mired(1700)
self._light_type = LightType.Ambient
@property
def name(self) -> str:
"""Return the name of the device if any."""
return "{} ambilight".format(self.device.name)
@property
def light_type(self):
"""Return light type."""
import yeelight
return yeelight.enums.LightType.Ambient
@property
def _is_nightlight_enabled(self):
return False

View File

@@ -172,6 +172,7 @@ class Light(ZhaEntity, light.Light):
duration = transition * 10 if transition else DEFAULT_DURATION
brightness = kwargs.get(light.ATTR_BRIGHTNESS)
t_log = {}
if (brightness is not None or transition) and \
self._supported_features & light.SUPPORT_BRIGHTNESS:
if brightness is not None:
@@ -182,7 +183,9 @@ class Light(ZhaEntity, light.Light):
level,
duration
)
t_log['move_to_level_with_on_off'] = success
if not success:
self.debug("turned on: %s", t_log)
return
self._state = bool(level)
if level:
@@ -190,7 +193,9 @@ class Light(ZhaEntity, light.Light):
if brightness is None or brightness:
success = await self._on_off_channel.on()
t_log['on_off'] = success
if not success:
self.debug("turned on: %s", t_log)
return
self._state = True
@@ -199,7 +204,9 @@ class Light(ZhaEntity, light.Light):
temperature = kwargs[light.ATTR_COLOR_TEMP]
success = await self._color_channel.move_to_color_temp(
temperature, duration)
t_log['move_to_color_temp'] = success
if not success:
self.debug("turned on: %s", t_log)
return
self._color_temp = temperature
@@ -212,10 +219,13 @@ class Light(ZhaEntity, light.Light):
int(xy_color[1] * 65535),
duration,
)
t_log['move_to_color'] = success
if not success:
self.debug("turned on: %s", t_log)
return
self._hs_color = hs_color
self.debug("turned on: %s", t_log)
self.async_schedule_update_ha_state()
async def async_turn_off(self, **kwargs):
@@ -229,7 +239,7 @@ class Light(ZhaEntity, light.Light):
)
else:
success = await self._on_off_channel.off()
_LOGGER.debug("%s was turned off: %s", self.entity_id, success)
self.debug("turned off: %s", success)
if not success:
return
self._state = False
@@ -238,13 +248,21 @@ class Light(ZhaEntity, light.Light):
async def async_update(self):
"""Attempt to retrieve on off state from the light."""
await super().async_update()
await self.async_get_state()
async def async_get_state(self, from_cache=True):
"""Attempt to retrieve on off state from the light."""
if self._on_off_channel:
self._state = await self._on_off_channel.get_attribute_value(
'on_off')
'on_off', from_cache=from_cache)
if self._level_channel:
self._brightness = await self._level_channel.get_attribute_value(
'current_level')
'current_level', from_cache=from_cache)
async def refresh(self, time):
"""Call async_update at an interval."""
await self.async_update()
"""Call async_get_state at an interval."""
await self.async_get_state(from_cache=False)
def debug(self, msg, *args):
"""Log debug message."""
_LOGGER.debug('%s: ' + msg, self.entity_id, *args)

View File

@@ -14,7 +14,7 @@ from .const import CONF_PASSIVE, DOMAIN, HOME_ZONE
@callback
def configured_zones(hass):
"""Return a set of the configured hosts."""
"""Return a set of the configured zones."""
return set((slugify(entry.data[CONF_NAME])) for
entry in hass.config_entries.async_entries(DOMAIN))

View File

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

View File

@@ -1,7 +1,6 @@
"""The tests for the hassio component."""
from aiohttp.hdrs import X_FORWARDED_FOR, X_FORWARDED_HOST, X_FORWARDED_PROTO
from aiohttp.client_exceptions import WSServerHandshakeError
import pytest
@@ -137,6 +136,72 @@ async def test_ingress_request_delete(
assert aioclient_mock.mock_calls[-1][3][X_FORWARDED_PROTO]
@pytest.mark.parametrize(
'build_type', [
("a3_vl", "test/beer/ping?index=1"), ("core", "index.html"),
("local", "panel/config"), ("jk_921", "editor.php?idx=3&ping=5"),
("fsadjf10312", "")
])
async def test_ingress_request_patch(
hassio_client, build_type, aioclient_mock):
"""Test no auth needed for ."""
aioclient_mock.patch("http://127.0.0.1/ingress/{}/{}".format(
build_type[0], build_type[1]), text="test")
resp = await hassio_client.patch(
'/api/hassio_ingress/{}/{}'.format(build_type[0], build_type[1]),
headers={"X-Test-Header": "beer"}
)
# Check we got right response
assert resp.status == 200
body = await resp.text()
assert body == "test"
# Check we forwarded command
assert len(aioclient_mock.mock_calls) == 1
assert aioclient_mock.mock_calls[-1][3]["X-Hassio-Key"] == "123456"
assert aioclient_mock.mock_calls[-1][3]["X-Ingress-Path"] == \
"/api/hassio_ingress/{}".format(build_type[0])
assert aioclient_mock.mock_calls[-1][3]["X-Test-Header"] == "beer"
assert aioclient_mock.mock_calls[-1][3][X_FORWARDED_FOR]
assert aioclient_mock.mock_calls[-1][3][X_FORWARDED_HOST]
assert aioclient_mock.mock_calls[-1][3][X_FORWARDED_PROTO]
@pytest.mark.parametrize(
'build_type', [
("a3_vl", "test/beer/ping?index=1"), ("core", "index.html"),
("local", "panel/config"), ("jk_921", "editor.php?idx=3&ping=5"),
("fsadjf10312", "")
])
async def test_ingress_request_options(
hassio_client, build_type, aioclient_mock):
"""Test no auth needed for ."""
aioclient_mock.options("http://127.0.0.1/ingress/{}/{}".format(
build_type[0], build_type[1]), text="test")
resp = await hassio_client.options(
'/api/hassio_ingress/{}/{}'.format(build_type[0], build_type[1]),
headers={"X-Test-Header": "beer"}
)
# Check we got right response
assert resp.status == 200
body = await resp.text()
assert body == "test"
# Check we forwarded command
assert len(aioclient_mock.mock_calls) == 1
assert aioclient_mock.mock_calls[-1][3]["X-Hassio-Key"] == "123456"
assert aioclient_mock.mock_calls[-1][3]["X-Ingress-Path"] == \
"/api/hassio_ingress/{}".format(build_type[0])
assert aioclient_mock.mock_calls[-1][3]["X-Test-Header"] == "beer"
assert aioclient_mock.mock_calls[-1][3][X_FORWARDED_FOR]
assert aioclient_mock.mock_calls[-1][3][X_FORWARDED_HOST]
assert aioclient_mock.mock_calls[-1][3][X_FORWARDED_PROTO]
@pytest.mark.parametrize(
'build_type', [
("a3_vl", "test/beer/ws"), ("core", "ws.php"),
@@ -150,11 +215,10 @@ async def test_ingress_websocket(
build_type[0], build_type[1]))
# Ignore error because we can setup a full IO infrastructure
with pytest.raises(WSServerHandshakeError):
await hassio_client.ws_connect(
'/api/hassio_ingress/{}/{}'.format(build_type[0], build_type[1]),
headers={"X-Test-Header": "beer"}
)
await hassio_client.ws_connect(
'/api/hassio_ingress/{}/{}'.format(build_type[0], build_type[1]),
headers={"X-Test-Header": "beer"}
)
# Check we forwarded command
assert len(aioclient_mock.mock_calls) == 1

View File

@@ -35,7 +35,7 @@ async def test_sensor(hass, create_registrations, webhook_client): # noqa: F401
assert reg_resp.status == 201
json = await reg_resp.json()
assert json == {'status': 'registered'}
assert json == {'success': True}
entity = hass.states.get('sensor.battery_state')
assert entity is not None
@@ -122,7 +122,7 @@ async def test_sensor_id_no_dupes(hass, create_registrations, # noqa: F401, F81
assert reg_resp.status == 201
reg_json = await reg_resp.json()
assert reg_json == {'status': 'registered'}
assert reg_json == {'success': True}
dupe_resp = await webhook_client.post(webhook_url, json=payload)

View File

@@ -4,8 +4,10 @@ import logging
import pytest
from homeassistant.components.mobile_app.const import CONF_SECRET
from homeassistant.components.zone import DOMAIN as ZONE_DOMAIN
from homeassistant.const import CONF_WEBHOOK_ID
from homeassistant.core import callback
from homeassistant.setup import async_setup_component
from tests.common import async_mock_service
@@ -100,6 +102,64 @@ async def test_webhook_update_registration(webhook_client, hass_client): # noqa
assert CONF_SECRET not in update_json
async def test_webhook_handle_get_zones(hass, create_registrations, # noqa: F401, F811, E501
webhook_client): # noqa: F811
"""Test that we can get zones properly."""
await async_setup_component(hass, ZONE_DOMAIN, {
ZONE_DOMAIN: {
'name': 'test',
'latitude': 32.880837,
'longitude': -117.237561,
'radius': 250,
}
})
resp = await webhook_client.post(
'/api/webhook/{}'.format(create_registrations[1]['webhook_id']),
json={'type': 'get_zones'}
)
assert resp.status == 200
json = await resp.json()
assert len(json) == 1
assert json[0]['entity_id'] == 'zone.home'
async def test_webhook_handle_get_config(hass, create_registrations, # noqa: F401, F811, E501
webhook_client): # noqa: F811
"""Test that we can get config properly."""
resp = await webhook_client.post(
'/api/webhook/{}'.format(create_registrations[1]['webhook_id']),
json={'type': 'get_config'}
)
assert resp.status == 200
json = await resp.json()
if 'components' in json:
json['components'] = set(json['components'])
if 'whitelist_external_dirs' in json:
json['whitelist_external_dirs'] = \
set(json['whitelist_external_dirs'])
hass_config = hass.config.as_dict()
expected_dict = {
'latitude': hass_config['latitude'],
'longitude': hass_config['longitude'],
'elevation': hass_config['elevation'],
'unit_system': hass_config['unit_system'],
'location_name': hass_config['location_name'],
'time_zone': hass_config['time_zone'],
'components': hass_config['components'],
'version': hass_config['version'],
'theme_color': '#03A9F4', # Default frontend theme color
}
assert expected_dict == json
async def test_webhook_returns_error_incorrect_json(webhook_client, # noqa: F401, F811, E501
create_registrations, # noqa: F401, F811, E501
caplog): # noqa: E501 F811

View File

@@ -82,6 +82,10 @@ class AiohttpClientMocker:
"""Register a mock options request."""
self.request('options', *args, **kwargs)
def patch(self, *args, **kwargs):
"""Register a mock patch request."""
self.request('patch', *args, **kwargs)
@property
def call_count(self):
"""Return the number of requests made."""
@@ -102,7 +106,7 @@ class AiohttpClientMocker:
async def match_request(self, method, url, *, data=None, auth=None,
params=None, headers=None, allow_redirects=None,
timeout=None, json=None, cookies=None):
timeout=None, json=None, cookies=None, **kwargs):
"""Match a request against pre-registered requests."""
data = data or json
url = URL(url)