Merge pull request #16027 from home-assistant/rc

0.76.0
This commit is contained in:
Paulus Schoutsen 2018-08-17 18:41:40 +02:00 committed by GitHub
commit 70412fc0ba
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
193 changed files with 4264 additions and 1543 deletions

View File

@ -215,6 +215,9 @@ omit =
homeassistant/components/opencv.py
homeassistant/components/*/opencv.py
homeassistant/components/openuv.py
homeassistant/components/*/openuv.py
homeassistant/components/pilight.py
homeassistant/components/*/pilight.py
@ -440,6 +443,7 @@ omit =
homeassistant/components/device_tracker/netgear.py
homeassistant/components/device_tracker/nmap_tracker.py
homeassistant/components/device_tracker/ping.py
homeassistant/components/device_tracker/ritassist.py
homeassistant/components/device_tracker/sky_hub.py
homeassistant/components/device_tracker/snmp.py
homeassistant/components/device_tracker/swisscom.py
@ -509,6 +513,7 @@ omit =
homeassistant/components/media_player/denon.py
homeassistant/components/media_player/denonavr.py
homeassistant/components/media_player/directv.py
homeassistant/components/media_player/dlna_dmr.py
homeassistant/components/media_player/dunehd.py
homeassistant/components/media_player/emby.py
homeassistant/components/media_player/epson.py
@ -532,6 +537,7 @@ omit =
homeassistant/components/media_player/pandora.py
homeassistant/components/media_player/philips_js.py
homeassistant/components/media_player/pioneer.py
homeassistant/components/media_player/pjlink.py
homeassistant/components/media_player/plex.py
homeassistant/components/media_player/roku.py
homeassistant/components/media_player/russound_rio.py
@ -632,6 +638,7 @@ omit =
homeassistant/components/sensor/eddystone_temperature.py
homeassistant/components/sensor/eliqonline.py
homeassistant/components/sensor/emoncms.py
homeassistant/components/sensor/enphase_envoy.py
homeassistant/components/sensor/envirophat.py
homeassistant/components/sensor/etherscan.py
homeassistant/components/sensor/fastdotcom.py

View File

@ -13,7 +13,8 @@ matrix:
- python: "3.5.3"
env: TOXENV=typing
- python: "3.5.3"
env: TOXENV=py35
env: TOXENV=cov
after_success: coveralls
- python: "3.6"
env: TOXENV=py36
- python: "3.7"
@ -45,4 +46,3 @@ deploy:
on:
branch: dev
condition: $TOXENV = lint
after_success: coveralls

View File

@ -98,6 +98,8 @@ homeassistant/components/konnected.py @heythisisnate
homeassistant/components/*/konnected.py @heythisisnate
homeassistant/components/matrix.py @tinloaf
homeassistant/components/*/matrix.py @tinloaf
homeassistant/components/openuv.py @bachya
homeassistant/components/*/openuv.py @bachya
homeassistant/components/qwikswitch.py @kellerza
homeassistant/components/*/qwikswitch.py @kellerza
homeassistant/components/rainmachine/* @bachya

View File

@ -10,7 +10,6 @@ LABEL maintainer="Paulus Schoutsen <Paulus@PaulusSchoutsen.nl>"
#ENV INSTALL_OPENALPR no
#ENV INSTALL_FFMPEG no
#ENV INSTALL_LIBCEC no
#ENV INSTALL_PHANTOMJS no
#ENV INSTALL_SSOCR no
#ENV INSTALL_IPERF3 no

View File

@ -1,5 +1,5 @@
Home Assistant |Build Status| |Coverage Status| |Chat Status|
=============================================================
Home Assistant |Build Status| |Coverage Status| |Chat Status| |Reviewed by Hound|
=================================================================================
Home Assistant is a home automation platform running on Python 3. It is able to track and control all devices at home and offer a platform for automating control.
@ -33,6 +33,8 @@ of a component, check the `Home Assistant help section <https://home-assistant.i
:target: https://coveralls.io/r/home-assistant/home-assistant?branch=master
.. |Chat Status| image:: https://img.shields.io/discord/330944238910963714.svg
:target: https://discord.gg/c5DvZ4e
.. |Reviewed by Hound| image:: https://img.shields.io/badge/Reviewed_by-Hound-8E64B0.svg
:target: https://houndci.com
.. |screenshot-states| image:: https://raw.github.com/home-assistant/home-assistant/master/docs/screenshots.png
:target: https://home-assistant.io/demo/
.. |screenshot-components| image:: https://raw.github.com/home-assistant/home-assistant/dev/docs/screenshot-components.png

View File

@ -2,18 +2,23 @@
import asyncio
import logging
from collections import OrderedDict
from typing import List, Awaitable
import jwt
from homeassistant import data_entry_flow
from homeassistant.core import callback
from homeassistant.core import callback, HomeAssistant
from homeassistant.util import dt as dt_util
from . import models
from . import auth_store
from .providers import auth_provider_from_config
_LOGGER = logging.getLogger(__name__)
async def auth_manager_from_config(hass, provider_configs):
async def auth_manager_from_config(
hass: HomeAssistant,
provider_configs: List[dict]) -> Awaitable['AuthManager']:
"""Initialize an auth manager from config."""
store = auth_store.AuthStore(hass)
if provider_configs:
@ -51,7 +56,6 @@ class AuthManager:
self.login_flow = data_entry_flow.FlowManager(
hass, self._async_create_login_flow,
self._async_finish_login_flow)
self._access_tokens = OrderedDict()
@property
def active(self):
@ -178,43 +182,64 @@ class AuthManager:
return await self._store.async_create_refresh_token(user, client_id)
async def async_get_refresh_token(self, token):
async def async_get_refresh_token(self, token_id):
"""Get refresh token by id."""
return await self._store.async_get_refresh_token(token_id)
async def async_get_refresh_token_by_token(self, token):
"""Get refresh token by token."""
return await self._store.async_get_refresh_token(token)
return await self._store.async_get_refresh_token_by_token(token)
@callback
def async_create_access_token(self, refresh_token):
"""Create a new access token."""
access_token = models.AccessToken(refresh_token=refresh_token)
self._access_tokens[access_token.token] = access_token
return access_token
# pylint: disable=no-self-use
return jwt.encode({
'iss': refresh_token.id,
'iat': dt_util.utcnow(),
'exp': dt_util.utcnow() + refresh_token.access_token_expiration,
}, refresh_token.jwt_key, algorithm='HS256').decode()
@callback
def async_get_access_token(self, token):
"""Get an access token."""
tkn = self._access_tokens.get(token)
if tkn is None:
_LOGGER.debug('Attempt to get non-existing access token')
async def async_validate_access_token(self, token):
"""Return if an access token is valid."""
try:
unverif_claims = jwt.decode(token, verify=False)
except jwt.InvalidTokenError:
return None
if tkn.expired or not tkn.refresh_token.user.is_active:
if tkn.expired:
_LOGGER.debug('Attempt to get expired access token')
else:
_LOGGER.debug('Attempt to get access token for inactive user')
self._access_tokens.pop(token)
refresh_token = await self.async_get_refresh_token(
unverif_claims.get('iss'))
if refresh_token is None:
jwt_key = ''
issuer = ''
else:
jwt_key = refresh_token.jwt_key
issuer = refresh_token.id
try:
jwt.decode(
token,
jwt_key,
leeway=10,
issuer=issuer,
algorithms=['HS256']
)
except jwt.InvalidTokenError:
return None
return tkn
if not refresh_token.user.is_active:
return None
async def _async_create_login_flow(self, handler, *, source, data):
return refresh_token
async def _async_create_login_flow(self, handler, *, context, data):
"""Create a login flow."""
auth_provider = self._providers[handler]
return await auth_provider.async_credential_flow()
return await auth_provider.async_credential_flow(context)
async def _async_finish_login_flow(self, result):
async def _async_finish_login_flow(self, context, result):
"""Result of a credential login flow."""
if result['type'] != data_entry_flow.RESULT_TYPE_CREATE_ENTRY:
return None

View File

@ -1,6 +1,7 @@
"""Storage for auth models."""
from collections import OrderedDict
from datetime import timedelta
import hmac
from homeassistant.util import dt as dt_util
@ -110,22 +111,36 @@ class AuthStore:
async def async_create_refresh_token(self, user, client_id=None):
"""Create a new token for a user."""
refresh_token = models.RefreshToken(user=user, client_id=client_id)
user.refresh_tokens[refresh_token.token] = refresh_token
user.refresh_tokens[refresh_token.id] = refresh_token
await self.async_save()
return refresh_token
async def async_get_refresh_token(self, token):
"""Get refresh token by token."""
async def async_get_refresh_token(self, token_id):
"""Get refresh token by id."""
if self._users is None:
await self.async_load()
for user in self._users.values():
refresh_token = user.refresh_tokens.get(token)
refresh_token = user.refresh_tokens.get(token_id)
if refresh_token is not None:
return refresh_token
return None
async def async_get_refresh_token_by_token(self, token):
"""Get refresh token by token."""
if self._users is None:
await self.async_load()
found = None
for user in self._users.values():
for refresh_token in user.refresh_tokens.values():
if hmac.compare_digest(refresh_token.token, token):
found = refresh_token
return found
async def async_load(self):
"""Load the users."""
data = await self._store.async_load()
@ -153,9 +168,11 @@ class AuthStore:
data=cred_dict['data'],
))
refresh_tokens = OrderedDict()
for rt_dict in data['refresh_tokens']:
# Filter out the old keys that don't have jwt_key (pre-0.76)
if 'jwt_key' not in rt_dict:
continue
token = models.RefreshToken(
id=rt_dict['id'],
user=users[rt_dict['user_id']],
@ -164,18 +181,9 @@ class AuthStore:
access_token_expiration=timedelta(
seconds=rt_dict['access_token_expiration']),
token=rt_dict['token'],
jwt_key=rt_dict['jwt_key']
)
refresh_tokens[token.id] = token
users[rt_dict['user_id']].refresh_tokens[token.token] = token
for ac_dict in data['access_tokens']:
refresh_token = refresh_tokens[ac_dict['refresh_token_id']]
token = models.AccessToken(
refresh_token=refresh_token,
created_at=dt_util.parse_datetime(ac_dict['created_at']),
token=ac_dict['token'],
)
refresh_token.access_tokens.append(token)
users[rt_dict['user_id']].refresh_tokens[token.id] = token
self._users = users
@ -213,27 +221,15 @@ class AuthStore:
'access_token_expiration':
refresh_token.access_token_expiration.total_seconds(),
'token': refresh_token.token,
'jwt_key': refresh_token.jwt_key,
}
for user in self._users.values()
for refresh_token in user.refresh_tokens.values()
]
access_tokens = [
{
'id': user.id,
'refresh_token_id': refresh_token.id,
'created_at': access_token.created_at.isoformat(),
'token': access_token.token,
}
for user in self._users.values()
for refresh_token in user.refresh_tokens.values()
for access_token in refresh_token.access_tokens
]
data = {
'users': users,
'credentials': credentials,
'access_tokens': access_tokens,
'refresh_tokens': refresh_tokens,
}

View File

@ -39,26 +39,8 @@ class RefreshToken:
default=ACCESS_TOKEN_EXPIRATION)
token = attr.ib(type=str,
default=attr.Factory(lambda: generate_secret(64)))
access_tokens = attr.ib(type=list, default=attr.Factory(list), cmp=False)
@attr.s(slots=True)
class AccessToken:
"""Access token to access the API.
These will only ever be stored in memory and not be persisted.
"""
refresh_token = attr.ib(type=RefreshToken)
created_at = attr.ib(type=datetime, default=attr.Factory(dt_util.utcnow))
token = attr.ib(type=str,
default=attr.Factory(generate_secret))
@property
def expired(self):
"""Return if this token has expired."""
expires = self.created_at + self.refresh_token.access_token_expiration
return dt_util.utcnow() > expires
jwt_key = attr.ib(type=str,
default=attr.Factory(lambda: generate_secret(64)))
@attr.s(slots=True)

View File

@ -123,7 +123,7 @@ class AuthProvider:
# Implement by extending class
async def async_credential_flow(self):
async def async_credential_flow(self, context):
"""Return the data flow for logging in with auth provider."""
raise NotImplementedError

View File

@ -158,7 +158,7 @@ class HassAuthProvider(AuthProvider):
self.data = Data(self.hass)
await self.data.async_load()
async def async_credential_flow(self):
async def async_credential_flow(self, context):
"""Return a flow to login."""
return LoginFlow(self)

View File

@ -31,7 +31,7 @@ class InvalidAuthError(HomeAssistantError):
class ExampleAuthProvider(AuthProvider):
"""Example auth provider based on hardcoded usernames and passwords."""
async def async_credential_flow(self):
async def async_credential_flow(self, context):
"""Return a flow to login."""
return LoginFlow(self)

View File

@ -36,7 +36,7 @@ class LegacyApiPasswordAuthProvider(AuthProvider):
DEFAULT_TITLE = 'Legacy API Password'
async def async_credential_flow(self):
async def async_credential_flow(self, context):
"""Return a flow to login."""
return LoginFlow(self)

View File

@ -10,6 +10,7 @@ Component design guidelines:
import asyncio
import itertools as it
import logging
from typing import Awaitable
import homeassistant.core as ha
import homeassistant.config as conf_util
@ -109,7 +110,7 @@ def async_reload_core_config(hass):
@asyncio.coroutine
def async_setup(hass, config):
def async_setup(hass: ha.HomeAssistant, config: dict) -> Awaitable[bool]:
"""Set up general services related to Home Assistant."""
@asyncio.coroutine
def async_handle_turn_service(service):

View File

@ -21,7 +21,7 @@ _LOGGER = logging.getLogger(__name__)
_CONFIGURING = {}
REQUIREMENTS = ['py-august==0.4.0']
REQUIREMENTS = ['py-august==0.6.0']
DEFAULT_TIMEOUT = 10
ACTIVITY_FETCH_LIMIT = 10

View File

@ -155,7 +155,7 @@ class GrantTokenView(HomeAssistantView):
access_token = hass.auth.async_create_access_token(refresh_token)
return self.json({
'access_token': access_token.token,
'access_token': access_token,
'token_type': 'Bearer',
'refresh_token': refresh_token.token,
'expires_in':
@ -178,7 +178,7 @@ class GrantTokenView(HomeAssistantView):
'error': 'invalid_request',
}, status_code=400)
refresh_token = await hass.auth.async_get_refresh_token(token)
refresh_token = await hass.auth.async_get_refresh_token_by_token(token)
if refresh_token is None:
return self.json({
@ -193,7 +193,7 @@ class GrantTokenView(HomeAssistantView):
access_token = hass.auth.async_create_access_token(refresh_token)
return self.json({
'access_token': access_token.token,
'access_token': access_token,
'token_type': 'Bearer',
'expires_in':
int(refresh_token.access_token_expiration.total_seconds()),

View File

@ -1,6 +1,10 @@
"""Helpers to resolve client ID/secret."""
import asyncio
from html.parser import HTMLParser
from ipaddress import ip_address, ip_network
from urllib.parse import urlparse
from urllib.parse import urlparse, urljoin
from aiohttp.client_exceptions import ClientError
# IP addresses of loopback interfaces
ALLOWED_IPS = (
@ -16,7 +20,7 @@ ALLOWED_NETWORKS = (
)
def verify_redirect_uri(client_id, redirect_uri):
async def verify_redirect_uri(hass, client_id, redirect_uri):
"""Verify that the client and redirect uri match."""
try:
client_id_parts = _parse_client_id(client_id)
@ -25,16 +29,75 @@ def verify_redirect_uri(client_id, redirect_uri):
redirect_parts = _parse_url(redirect_uri)
# IndieAuth 4.2.2 allows for redirect_uri to be on different domain
# but needs to be specified in link tag when fetching `client_id`.
# This is not implemented.
# Verify redirect url and client url have same scheme and domain.
return (
is_valid = (
client_id_parts.scheme == redirect_parts.scheme and
client_id_parts.netloc == redirect_parts.netloc
)
if is_valid:
return True
# IndieAuth 4.2.2 allows for redirect_uri to be on different domain
# but needs to be specified in link tag when fetching `client_id`.
redirect_uris = await fetch_redirect_uris(hass, client_id)
return redirect_uri in redirect_uris
class LinkTagParser(HTMLParser):
"""Parser to find link tags."""
def __init__(self, rel):
"""Initialize a link tag parser."""
super().__init__()
self.rel = rel
self.found = []
def handle_starttag(self, tag, attrs):
"""Handle finding a start tag."""
if tag != 'link':
return
attrs = dict(attrs)
if attrs.get('rel') == self.rel:
self.found.append(attrs.get('href'))
async def fetch_redirect_uris(hass, url):
"""Find link tag with redirect_uri values.
IndieAuth 4.2.2
The client SHOULD publish one or more <link> tags or Link HTTP headers with
a rel attribute of redirect_uri at the client_id URL.
We limit to the first 10kB of the page.
We do not implement extracting redirect uris from headers.
"""
session = hass.helpers.aiohttp_client.async_get_clientsession()
parser = LinkTagParser('redirect_uri')
chunks = 0
try:
resp = await session.get(url, timeout=5)
async for data in resp.content.iter_chunked(1024):
parser.feed(data.decode())
chunks += 1
if chunks == 10:
break
except (asyncio.TimeoutError, ClientError):
pass
# Authorization endpoints verifying that a redirect_uri is allowed for use
# by a client MUST look for an exact match of the given redirect_uri in the
# request against the list of redirect_uris discovered after resolving any
# relative URLs.
return [urljoin(url, found) for found in parser.found]
def verify_client_id(client_id):
"""Verify that the client id is valid."""

View File

@ -54,7 +54,6 @@ have type "create_entry" and "result" key will contain an authorization code.
"flow_id": "8f7e42faab604bcab7ac43c44ca34d58",
"handler": ["insecure_example", null],
"result": "411ee2f916e648d691e937ae9344681e",
"source": "user",
"title": "Example",
"type": "create_entry",
"version": 1
@ -68,8 +67,6 @@ from homeassistant.components.http.ban import process_wrong_login, \
log_invalid_auth
from homeassistant.components.http.data_validator import RequestDataValidator
from homeassistant.components.http.view import HomeAssistantView
from homeassistant.helpers.data_entry_flow import (
FlowManagerIndexView, FlowManagerResourceView)
from . import indieauth
@ -97,13 +94,41 @@ class AuthProvidersView(HomeAssistantView):
} for provider in request.app['hass'].auth.auth_providers])
class LoginFlowIndexView(FlowManagerIndexView):
def _prepare_result_json(result):
"""Convert result to JSON."""
if result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY:
data = result.copy()
data.pop('result')
data.pop('data')
return data
if result['type'] != data_entry_flow.RESULT_TYPE_FORM:
return result
import voluptuous_serialize
data = result.copy()
schema = data['data_schema']
if schema is None:
data['data_schema'] = []
else:
data['data_schema'] = voluptuous_serialize.convert(schema)
return data
class LoginFlowIndexView(HomeAssistantView):
"""View to create a config flow."""
url = '/auth/login_flow'
name = 'api:auth:login_flow'
requires_auth = False
def __init__(self, flow_mgr):
"""Initialize the flow manager index view."""
self._flow_mgr = flow_mgr
async def get(self, request):
"""Do not allow index of flows in progress."""
return aiohttp.web.Response(status=405)
@ -116,15 +141,26 @@ class LoginFlowIndexView(FlowManagerIndexView):
@log_invalid_auth
async def post(self, request, data):
"""Create a new login flow."""
if not indieauth.verify_redirect_uri(data['client_id'],
data['redirect_uri']):
if not await indieauth.verify_redirect_uri(
request.app['hass'], data['client_id'], data['redirect_uri']):
return self.json_message('invalid client id or redirect uri', 400)
# pylint: disable=no-value-for-parameter
return await super().post(request)
if isinstance(data['handler'], list):
handler = tuple(data['handler'])
else:
handler = data['handler']
try:
result = await self._flow_mgr.async_init(handler, context={})
except data_entry_flow.UnknownHandler:
return self.json_message('Invalid handler specified', 404)
except data_entry_flow.UnknownStep:
return self.json_message('Handler does not support init', 400)
return self.json(_prepare_result_json(result))
class LoginFlowResourceView(FlowManagerResourceView):
class LoginFlowResourceView(HomeAssistantView):
"""View to interact with the flow manager."""
url = '/auth/login_flow/{flow_id}'
@ -133,10 +169,10 @@ class LoginFlowResourceView(FlowManagerResourceView):
def __init__(self, flow_mgr, store_credentials):
"""Initialize the login flow resource view."""
super().__init__(flow_mgr)
self._flow_mgr = flow_mgr
self._store_credentials = store_credentials
async def get(self, request, flow_id):
async def get(self, request):
"""Do not allow getting status of a flow in progress."""
return self.json_message('Invalid flow specified', 404)
@ -164,9 +200,18 @@ class LoginFlowResourceView(FlowManagerResourceView):
if result['errors'] is not None and \
result['errors'].get('base') == 'invalid_auth':
await process_wrong_login(request)
return self.json(self._prepare_result_json(result))
return self.json(_prepare_result_json(result))
result.pop('data')
result['result'] = self._store_credentials(client_id, result['result'])
return self.json(result)
async def delete(self, request, flow_id):
"""Cancel a flow in progress."""
try:
self._flow_mgr.async_abort(flow_id)
except data_entry_flow.UnknownFlow:
return self.json_message('Invalid flow specified', 404)
return self.json_message('Flow aborted')

View File

@ -122,7 +122,6 @@ class BayesianBinarySensor(BinarySensorDevice):
def async_added_to_hass(self):
"""Call when entity about to be added."""
@callback
# pylint: disable=invalid-name
def async_threshold_sensor_state_listener(entity, old_state,
new_state):
"""Handle sensor state changes."""

View File

@ -16,10 +16,7 @@ DEPENDENCIES = ['homematicip_cloud']
_LOGGER = logging.getLogger(__name__)
ATTR_WINDOW_STATE = 'window_state'
ATTR_EVENT_DELAY = 'event_delay'
ATTR_MOTION_DETECTED = 'motion_detected'
ATTR_ILLUMINATION = 'illumination'
STATE_SMOKE_OFF = 'IDLE_OFF'
async def async_setup_platform(hass, config, async_add_devices,
@ -30,15 +27,18 @@ async def async_setup_platform(hass, config, async_add_devices,
async def async_setup_entry(hass, config_entry, async_add_devices):
"""Set up the HomematicIP binary sensor from a config entry."""
from homematicip.device import (ShutterContact, MotionDetectorIndoor)
from homematicip.aio.device import (
AsyncShutterContact, AsyncMotionDetectorIndoor, AsyncSmokeDetector)
home = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]].home
devices = []
for device in home.devices:
if isinstance(device, ShutterContact):
if isinstance(device, AsyncShutterContact):
devices.append(HomematicipShutterContact(home, device))
elif isinstance(device, MotionDetectorIndoor):
elif isinstance(device, AsyncMotionDetectorIndoor):
devices.append(HomematicipMotionDetector(home, device))
elif isinstance(device, AsyncSmokeDetector):
devices.append(HomematicipSmokeDetector(home, device))
if devices:
async_add_devices(devices)
@ -47,10 +47,6 @@ async def async_setup_entry(hass, config_entry, async_add_devices):
class HomematicipShutterContact(HomematicipGenericDevice, BinarySensorDevice):
"""HomematicIP shutter contact."""
def __init__(self, home, device):
"""Initialize the shutter contact."""
super().__init__(home, device)
@property
def device_class(self):
"""Return the class of this sensor."""
@ -69,11 +65,7 @@ class HomematicipShutterContact(HomematicipGenericDevice, BinarySensorDevice):
class HomematicipMotionDetector(HomematicipGenericDevice, BinarySensorDevice):
"""MomematicIP motion detector."""
def __init__(self, home, device):
"""Initialize the shutter contact."""
super().__init__(home, device)
"""HomematicIP motion detector."""
@property
def device_class(self):
@ -86,3 +78,17 @@ class HomematicipMotionDetector(HomematicipGenericDevice, BinarySensorDevice):
if self._device.sabotage:
return True
return self._device.motionDetected
class HomematicipSmokeDetector(HomematicipGenericDevice, BinarySensorDevice):
"""HomematicIP smoke detector."""
@property
def device_class(self):
"""Return the class of this sensor."""
return 'smoke'
@property
def is_on(self):
"""Return true if smoke is detected."""
return self._device.smokeDetectorAlarmType != STATE_SMOKE_OFF

View File

@ -0,0 +1,103 @@
"""
This platform provides binary sensors for OpenUV data.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.openuv/
"""
import logging
from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.const import CONF_MONITORED_CONDITIONS
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.components.openuv import (
BINARY_SENSORS, DATA_PROTECTION_WINDOW, DOMAIN, TOPIC_UPDATE,
TYPE_PROTECTION_WINDOW, OpenUvEntity)
from homeassistant.util.dt import as_local, parse_datetime, utcnow
DEPENDENCIES = ['openuv']
_LOGGER = logging.getLogger(__name__)
ATTR_PROTECTION_WINDOW_STARTING_TIME = 'start_time'
ATTR_PROTECTION_WINDOW_STARTING_UV = 'start_uv'
ATTR_PROTECTION_WINDOW_ENDING_TIME = 'end_time'
ATTR_PROTECTION_WINDOW_ENDING_UV = 'end_uv'
async def async_setup_platform(
hass, config, async_add_devices, discovery_info=None):
"""Set up the OpenUV binary sensor platform."""
if discovery_info is None:
return
openuv = hass.data[DOMAIN]
binary_sensors = []
for sensor_type in discovery_info[CONF_MONITORED_CONDITIONS]:
name, icon = BINARY_SENSORS[sensor_type]
binary_sensors.append(
OpenUvBinarySensor(openuv, sensor_type, name, icon))
async_add_devices(binary_sensors, True)
class OpenUvBinarySensor(OpenUvEntity, BinarySensorDevice):
"""Define a binary sensor for OpenUV."""
def __init__(self, openuv, sensor_type, name, icon):
"""Initialize the sensor."""
super().__init__(openuv)
self._icon = icon
self._latitude = openuv.client.latitude
self._longitude = openuv.client.longitude
self._name = name
self._sensor_type = sensor_type
self._state = None
@property
def icon(self):
"""Return the icon."""
return self._icon
@property
def is_on(self):
"""Return the status of the sensor."""
return self._state
@property
def should_poll(self):
"""Disable polling."""
return False
@property
def unique_id(self) -> str:
"""Return a unique, HASS-friendly identifier for this entity."""
return '{0}_{1}_{2}'.format(
self._latitude, self._longitude, self._sensor_type)
@callback
def _update_data(self):
"""Update the state."""
self.async_schedule_update_ha_state(True)
async def async_added_to_hass(self):
"""Register callbacks."""
async_dispatcher_connect(
self.hass, TOPIC_UPDATE, self._update_data)
async def async_update(self):
"""Update the state."""
data = self.openuv.data[DATA_PROTECTION_WINDOW]['result']
if self._sensor_type == TYPE_PROTECTION_WINDOW:
self._state = parse_datetime(
data['from_time']) <= utcnow() <= parse_datetime(
data['to_time'])
self._attrs.update({
ATTR_PROTECTION_WINDOW_ENDING_TIME:
as_local(parse_datetime(data['to_time'])),
ATTR_PROTECTION_WINDOW_ENDING_UV: data['to_uv'],
ATTR_PROTECTION_WINDOW_STARTING_UV: data['from_uv'],
ATTR_PROTECTION_WINDOW_STARTING_TIME:
as_local(parse_datetime(data['from_time'])),
})

View File

@ -86,7 +86,6 @@ class ThresholdSensor(BinarySensorDevice):
self._state = False
self.sensor_value = None
# pylint: disable=invalid-name
@callback
def async_threshold_sensor_state_listener(
entity, old_state, new_state):

View File

@ -4,93 +4,34 @@ Support for Velbus Binary Sensors.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.velbus/
"""
import asyncio
import logging
import voluptuous as vol
from homeassistant.const import CONF_NAME, CONF_DEVICES
from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.components.binary_sensor import PLATFORM_SCHEMA
from homeassistant.components.velbus import DOMAIN
import homeassistant.helpers.config_validation as cv
DEPENDENCIES = ['velbus']
from homeassistant.components.velbus import (
DOMAIN as VELBUS_DOMAIN, VelbusEntity)
_LOGGER = logging.getLogger(__name__)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_DEVICES): vol.All(cv.ensure_list, [
{
vol.Required('module'): cv.positive_int,
vol.Required('channel'): cv.positive_int,
vol.Required(CONF_NAME): cv.string,
vol.Optional('is_pushbutton'): cv.boolean
}
])
})
DEPENDENCIES = ['velbus']
def setup_platform(hass, config, add_devices, discovery_info=None):
async def async_setup_platform(hass, config, async_add_devices,
discovery_info=None):
"""Set up Velbus binary sensors."""
velbus = hass.data[DOMAIN]
add_devices(VelbusBinarySensor(sensor, velbus)
for sensor in config[CONF_DEVICES])
if discovery_info is None:
return
sensors = []
for sensor in discovery_info:
module = hass.data[VELBUS_DOMAIN].get_module(sensor[0])
channel = sensor[1]
sensors.append(VelbusBinarySensor(module, channel))
async_add_devices(sensors)
class VelbusBinarySensor(BinarySensorDevice):
class VelbusBinarySensor(VelbusEntity, BinarySensorDevice):
"""Representation of a Velbus Binary Sensor."""
def __init__(self, binary_sensor, velbus):
"""Initialize a Velbus light."""
self._velbus = velbus
self._name = binary_sensor[CONF_NAME]
self._module = binary_sensor['module']
self._channel = binary_sensor['channel']
self._is_pushbutton = 'is_pushbutton' in binary_sensor \
and binary_sensor['is_pushbutton']
self._state = False
@asyncio.coroutine
def async_added_to_hass(self):
"""Add listener for Velbus messages on bus."""
yield from self.hass.async_add_job(
self._velbus.subscribe, self._on_message)
def _on_message(self, message):
import velbus
if isinstance(message, velbus.PushButtonStatusMessage):
if message.address == self._module and \
self._channel in message.get_channels():
if self._is_pushbutton:
if self._channel in message.closed:
self._toggle()
else:
pass
else:
self._toggle()
def _toggle(self):
if self._state is True:
self._state = False
else:
self._state = True
self.schedule_update_ha_state()
@property
def should_poll(self):
"""No polling needed."""
return False
@property
def name(self):
"""Return the display name of this sensor."""
return self._name
@property
def is_on(self):
"""Return true if the sensor is on."""
return self._state
return self._module.is_closed(self._channel)

View File

@ -17,7 +17,7 @@ import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['holidays==0.9.5']
REQUIREMENTS = ['holidays==0.9.6']
# List of all countries currently supported by holidays
# There seems to be no way to get the list out at runtime
@ -25,9 +25,9 @@ ALL_COUNTRIES = ['Argentina', 'AR', 'Australia', 'AU', 'Austria', 'AT',
'Belgium', 'BE', 'Canada', 'CA', 'Colombia', 'CO', 'Czech',
'CZ', 'Denmark', 'DK', 'England', 'EuropeanCentralBank',
'ECB', 'TAR', 'Finland', 'FI', 'France', 'FRA', 'Germany',
'DE', 'Hungary', 'HU', 'Ireland', 'Isle of Man', 'Italy',
'IT', 'Japan', 'JP', 'Mexico', 'MX', 'Netherlands', 'NL',
'NewZealand', 'NZ', 'Northern Ireland',
'DE', 'Hungary', 'HU', 'India', 'IND', 'Ireland',
'Isle of Man', 'Italy', 'IT', 'Japan', 'JP', 'Mexico', 'MX',
'Netherlands', 'NL', 'NewZealand', 'NZ', 'Northern Ireland',
'Norway', 'NO', 'Polish', 'PL', 'Portugal', 'PT',
'PortugalExt', 'PTE', 'Scotland', 'Slovenia', 'SI',
'Slovakia', 'SK', 'South Africa', 'ZA', 'Spain', 'ES',

View File

@ -26,9 +26,6 @@ CONF_PROJECT_DUE_DATE = 'due_date_days'
CONF_PROJECT_LABEL_WHITELIST = 'labels'
CONF_PROJECT_WHITELIST = 'include_projects'
# https://github.com/PyCQA/pylint/pull/2320
# pylint: disable=fixme
# Calendar Platform: Does this calendar event last all day?
ALL_DAY = 'all_day'
# Attribute: All tasks in this project

View File

@ -57,6 +57,7 @@ class YiCamera(Camera):
self._last_url = None
self._manager = hass.data[DATA_FFMPEG]
self._name = config[CONF_NAME]
self._is_on = True
self.host = config[CONF_HOST]
self.port = config[CONF_PORT]
self.path = config[CONF_PATH]
@ -68,6 +69,11 @@ class YiCamera(Camera):
"""Camera brand."""
return DEFAULT_BRAND
@property
def is_on(self):
"""Determine whether the camera is on."""
return self._is_on
@property
def name(self):
"""Return the name of this camera."""
@ -81,7 +87,7 @@ class YiCamera(Camera):
try:
await ftp.connect(self.host)
await ftp.login(self.user, self.passwd)
except StatusCodeError as err:
except (ConnectionRefusedError, StatusCodeError) as err:
raise PlatformNotReady(err)
try:
@ -101,12 +107,13 @@ class YiCamera(Camera):
return None
await ftp.quit()
self._is_on = True
return 'ftp://{0}:{1}@{2}:{3}{4}/{5}/{6}'.format(
self.user, self.passwd, self.host, self.port, self.path,
latest_dir, videos[-1])
except (ConnectionRefusedError, StatusCodeError) as err:
_LOGGER.error('Error while fetching video: %s', err)
self._is_on = False
return None
async def async_camera_image(self):
@ -114,7 +121,7 @@ class YiCamera(Camera):
from haffmpeg import ImageFrame, IMAGE_JPEG
url = await self._get_latest_video_url()
if url != self._last_url:
if url and url != self._last_url:
ffmpeg = ImageFrame(self._manager.binary, loop=self.hass.loop)
self._last_image = await asyncio.shield(
ffmpeg.get_image(
@ -130,6 +137,9 @@ class YiCamera(Camera):
"""Generate an HTTP MJPEG stream from the camera."""
from haffmpeg import CameraMjpeg
if not self._is_on:
return
stream = CameraMjpeg(self._manager.binary, loop=self.hass.loop)
await stream.open_camera(
self._last_url, extra_cmd=self._extra_arguments)

View File

@ -1,5 +1,5 @@
"""Component to embed Google Cast."""
from homeassistant import data_entry_flow
from homeassistant import config_entries
from homeassistant.helpers import config_entry_flow
@ -15,7 +15,7 @@ async def async_setup(hass, config):
if conf is not None:
hass.async_create_task(hass.config_entries.flow.async_init(
DOMAIN, source=data_entry_flow.SOURCE_IMPORT))
DOMAIN, context={'source': config_entries.SOURCE_IMPORT}))
return True

View File

@ -18,7 +18,7 @@ from homeassistant.const import (
CONF_HOST, TEMP_FAHRENHEIT, ATTR_TEMPERATURE, PRECISION_HALVES)
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['radiotherm==1.3']
REQUIREMENTS = ['radiotherm==1.4.1']
_LOGGER = logging.getLogger(__name__)

View File

@ -7,7 +7,7 @@ from homeassistant.helpers.data_entry_flow import (
FlowManagerIndexView, FlowManagerResourceView)
REQUIREMENTS = ['voluptuous-serialize==1']
REQUIREMENTS = ['voluptuous-serialize==2.0.0']
@asyncio.coroutine
@ -96,7 +96,7 @@ class ConfigManagerFlowIndexView(FlowManagerIndexView):
return self.json([
flw for flw in hass.config_entries.flow.async_progress()
if flw['source'] != data_entry_flow.SOURCE_USER])
if flw['context']['source'] != config_entries.SOURCE_USER])
class ConfigManagerFlowResourceView(FlowManagerResourceView):

View File

@ -4,8 +4,10 @@ Support for Tahoma cover - shutters etc.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/cover.tahoma/
"""
from datetime import timedelta
import logging
from homeassistant.util.dt import utcnow
from homeassistant.components.cover import CoverDevice, ATTR_POSITION
from homeassistant.components.tahoma import (
DOMAIN as TAHOMA_DOMAIN, TahomaDevice)
@ -14,6 +16,13 @@ DEPENDENCIES = ['tahoma']
_LOGGER = logging.getLogger(__name__)
ATTR_MEM_POS = 'memorized_position'
ATTR_RSSI_LEVEL = 'rssi_level'
ATTR_LOCK_START_TS = 'lock_start_ts'
ATTR_LOCK_END_TS = 'lock_end_ts'
ATTR_LOCK_LEVEL = 'lock_level'
ATTR_LOCK_ORIG = 'lock_originator'
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Tahoma covers."""
@ -27,27 +36,107 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
class TahomaCover(TahomaDevice, CoverDevice):
"""Representation a Tahoma Cover."""
def __init__(self, tahoma_device, controller):
"""Initialize the device."""
super().__init__(tahoma_device, controller)
self._closure = 0
# 100 equals open
self._position = 100
self._closed = False
self._rssi_level = None
self._icon = None
# Can be 0 and bigger
self._lock_timer = 0
self._lock_start_ts = None
self._lock_end_ts = None
# Can be 'comfortLevel1', 'comfortLevel2', 'comfortLevel3',
# 'comfortLevel4', 'environmentProtection', 'humanProtection',
# 'userLevel1', 'userLevel2'
self._lock_level = None
# Can be 'LSC', 'SAAC', 'SFC', 'UPS', 'externalGateway', 'localUser',
# 'myself', 'rain', 'security', 'temperature', 'timer', 'user', 'wind'
self._lock_originator = None
def update(self):
"""Update method."""
self.controller.get_states([self.tahoma_device])
# For vertical covers
self._closure = self.tahoma_device.active_states.get(
'core:ClosureState')
# For horizontal covers
if self._closure is None:
self._closure = self.tahoma_device.active_states.get(
'core:DeploymentState')
# For all, if available
if 'core:PriorityLockTimerState' in self.tahoma_device.active_states:
old_lock_timer = self._lock_timer
self._lock_timer = \
self.tahoma_device.active_states['core:PriorityLockTimerState']
# Derive timestamps from _lock_timer, only if not already set or
# something has changed
if self._lock_timer > 0:
_LOGGER.debug("Update %s, lock_timer: %d", self._name,
self._lock_timer)
if self._lock_start_ts is None:
self._lock_start_ts = utcnow()
if self._lock_end_ts is None or \
old_lock_timer != self._lock_timer:
self._lock_end_ts = utcnow() +\
timedelta(seconds=self._lock_timer)
else:
self._lock_start_ts = None
self._lock_end_ts = None
else:
self._lock_timer = 0
self._lock_start_ts = None
self._lock_end_ts = None
self._lock_level = self.tahoma_device.active_states.get(
'io:PriorityLockLevelState')
self._lock_originator = self.tahoma_device.active_states.get(
'io:PriorityLockOriginatorState')
self._rssi_level = self.tahoma_device.active_states.get(
'core:RSSILevelState')
# Define which icon to use
if self._lock_timer > 0:
if self._lock_originator == 'wind':
self._icon = 'mdi:weather-windy'
else:
self._icon = 'mdi:lock-alert'
else:
self._icon = None
# Define current position.
# _position: 0 is closed, 100 is fully open.
# 'core:ClosureState': 100 is closed, 0 is fully open.
if self._closure is not None:
self._position = 100 - self._closure
if self._position <= 5:
self._position = 0
if self._position >= 95:
self._position = 100
self._closed = self._position == 0
else:
self._position = None
if 'core:OpenClosedState' in self.tahoma_device.active_states:
self._closed = \
self.tahoma_device.active_states['core:OpenClosedState']\
== 'closed'
else:
self._closed = False
_LOGGER.debug("Update %s, position: %d", self._name, self._position)
@property
def current_cover_position(self):
"""
Return current position of cover.
0 is closed, 100 is fully open.
"""
try:
position = 100 - \
self.tahoma_device.active_states['core:ClosureState']
if position <= 5:
return 0
if position >= 95:
return 100
return position
except KeyError:
return None
"""Return current position of cover."""
return self._position
def set_cover_position(self, **kwargs):
"""Move the cover to a specific position."""
@ -56,8 +145,7 @@ class TahomaCover(TahomaDevice, CoverDevice):
@property
def is_closed(self):
"""Return if the cover is closed."""
if self.current_cover_position is not None:
return self.current_cover_position == 0
return self._closed
@property
def device_class(self):
@ -66,13 +154,47 @@ class TahomaCover(TahomaDevice, CoverDevice):
return 'window'
return None
@property
def device_state_attributes(self):
"""Return the device state attributes."""
attr = {}
super_attr = super().device_state_attributes
if super_attr is not None:
attr.update(super_attr)
if 'core:Memorized1PositionState' in self.tahoma_device.active_states:
attr[ATTR_MEM_POS] = self.tahoma_device.active_states[
'core:Memorized1PositionState']
if self._rssi_level is not None:
attr[ATTR_RSSI_LEVEL] = self._rssi_level
if self._lock_start_ts is not None:
attr[ATTR_LOCK_START_TS] = self._lock_start_ts.isoformat()
if self._lock_end_ts is not None:
attr[ATTR_LOCK_END_TS] = self._lock_end_ts.isoformat()
if self._lock_level is not None:
attr[ATTR_LOCK_LEVEL] = self._lock_level
if self._lock_originator is not None:
attr[ATTR_LOCK_ORIG] = self._lock_originator
return attr
@property
def icon(self):
"""Return the icon to use in the frontend, if any."""
return self._icon
def open_cover(self, **kwargs):
"""Open the cover."""
self.apply_action('open')
if self.tahoma_device.type == 'io:HorizontalAwningIOComponent':
self.apply_action('close')
else:
self.apply_action('open')
def close_cover(self, **kwargs):
"""Close the cover."""
self.apply_action('close')
if self.tahoma_device.type == 'io:HorizontalAwningIOComponent':
self.apply_action('open')
else:
self.apply_action('close')
def stop_cover(self, **kwargs):
"""Stop the cover."""
@ -87,5 +209,10 @@ class TahomaCover(TahomaDevice, CoverDevice):
'rts:ExteriorVenetianBlindRTSComponent',
'rts:BlindRTSComponent'):
self.apply_action('my')
elif self.tahoma_device.type in \
('io:HorizontalAwningIOComponent',
'io:RollerShutterGenericIOComponent',
'io:VerticalExteriorAwningIOComponent'):
self.apply_action('stop')
else:
self.apply_action('stopIdentify')

View File

@ -24,7 +24,8 @@
"data": {
"allow_clip_sensor": "Import virtueller Sensoren zulassen",
"allow_deconz_groups": "Import von deCONZ-Gruppen zulassen"
}
},
"title": "Weitere Konfigurationsoptionen f\u00fcr deCONZ"
}
},
"title": "deCONZ Zigbee Gateway"

View File

@ -6,6 +6,7 @@ https://home-assistant.io/components/deconz/
"""
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.const import (
CONF_API_KEY, CONF_EVENT, CONF_HOST,
CONF_ID, CONF_PORT, EVENT_HOMEASSISTANT_STOP)
@ -22,7 +23,7 @@ from .const import (
CONF_ALLOW_CLIP_SENSOR, CONFIG_FILE, DATA_DECONZ_EVENT,
DATA_DECONZ_ID, DATA_DECONZ_UNSUB, DOMAIN, _LOGGER)
REQUIREMENTS = ['pydeconz==42']
REQUIREMENTS = ['pydeconz==43']
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
@ -60,7 +61,9 @@ async def async_setup(hass, config):
deconz_config = config[DOMAIN]
if deconz_config and not configured_hosts(hass):
hass.async_add_job(hass.config_entries.flow.async_init(
DOMAIN, source='import', data=deconz_config
DOMAIN,
context={'source': config_entries.SOURCE_IMPORT},
data=deconz_config
))
return True
@ -96,7 +99,7 @@ async def async_setup_entry(hass, config_entry):
hass.data[DATA_DECONZ_EVENT] = []
hass.data[DATA_DECONZ_UNSUB] = []
for component in ['binary_sensor', 'light', 'scene', 'sensor']:
for component in ['binary_sensor', 'light', 'scene', 'sensor', 'switch']:
hass.async_create_task(hass.config_entries.async_forward_entry_setup(
config_entry, component))

View File

@ -33,6 +33,10 @@ class DeconzFlowHandler(data_entry_flow.FlowHandler):
self.bridges = []
self.deconz_config = {}
async def async_step_user(self, user_input=None):
"""Handle a flow initialized by the user."""
return await self.async_step_init(user_input)
async def async_step_init(self, user_input=None):
"""Handle a deCONZ config flow start.

View File

@ -14,3 +14,7 @@ CONF_ALLOW_DECONZ_GROUPS = 'allow_deconz_groups'
ATTR_DARK = 'dark'
ATTR_ON = 'on'
POWER_PLUGS = ["On/Off plug-in unit", "Smart plug"]
SIRENS = ["Warning device"]
SWITCH_TYPES = POWER_PLUGS + SIRENS

View File

@ -5,24 +5,22 @@ For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/device_tracker.bt_home_hub_5/
"""
import logging
import re
import xml.etree.ElementTree as ET
import json
from urllib.parse import unquote
import requests
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.components.device_tracker import (
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
from homeassistant.components.device_tracker import (DOMAIN, PLATFORM_SCHEMA,
DeviceScanner)
from homeassistant.const import CONF_HOST
REQUIREMENTS = ['bthomehub5-devicelist==0.1.1']
_LOGGER = logging.getLogger(__name__)
_MAC_REGEX = re.compile(r'(([0-9A-Fa-f]{1,2}\:){5}[0-9A-Fa-f]{1,2})')
CONF_DEFAULT_IP = '192.168.1.254'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST): cv.string
vol.Optional(CONF_HOST, default=CONF_DEFAULT_IP): cv.string,
})
@ -38,18 +36,19 @@ class BTHomeHub5DeviceScanner(DeviceScanner):
def __init__(self, config):
"""Initialise the scanner."""
import bthomehub5_devicelist
_LOGGER.info("Initialising BT Home Hub 5")
self.host = config.get(CONF_HOST, '192.168.1.254')
self.host = config[CONF_HOST]
self.last_results = {}
self.url = 'http://{}/nonAuth/home_status.xml'.format(self.host)
# Test the router is accessible
data = _get_homehub_data(self.url)
data = bthomehub5_devicelist.get_devicelist(self.host)
self.success_init = data is not None
def scan_devices(self):
"""Scan for new devices and return a list with found device IDs."""
self._update_info()
self.update_info()
return (device for device in self.last_results)
@ -57,71 +56,23 @@ class BTHomeHub5DeviceScanner(DeviceScanner):
"""Return the name of the given device or None if we don't know."""
# If not initialised and not already scanned and not found.
if device not in self.last_results:
self._update_info()
self.update_info()
if not self.last_results:
return None
return self.last_results.get(device)
def _update_info(self):
"""Ensure the information from the BT Home Hub 5 is up to date.
Return boolean if scanning successful.
"""
if not self.success_init:
return False
def update_info(self):
"""Ensure the information from the BT Home Hub 5 is up to date."""
import bthomehub5_devicelist
_LOGGER.info("Scanning")
data = _get_homehub_data(self.url)
data = bthomehub5_devicelist.get_devicelist(self.host)
if not data:
_LOGGER.warning("Error scanning devices")
return False
return
self.last_results = data
return True
def _get_homehub_data(url):
"""Retrieve data from BT Home Hub 5 and return parsed result."""
try:
response = requests.get(url, timeout=5)
except requests.exceptions.Timeout:
_LOGGER.exception("Connection to the router timed out")
return
if response.status_code == 200:
return _parse_homehub_response(response.text)
_LOGGER.error("Invalid response from Home Hub: %s", response)
def _parse_homehub_response(data_str):
"""Parse the BT Home Hub 5 data format."""
root = ET.fromstring(data_str)
dirty_json = root.find('known_device_list').get('value')
# Normalise the JavaScript data to JSON.
clean_json = unquote(dirty_json.replace('\'', '\"')
.replace('{', '{\"')
.replace(':\"', '\":\"')
.replace('\",', '\",\"'))
known_devices = [x for x in json.loads(clean_json) if x]
devices = {}
for device in known_devices:
name = device.get('name')
mac = device.get('mac')
if _MAC_REGEX.match(mac) or ',' in mac:
for mac_addr in mac.split(','):
if _MAC_REGEX.match(mac_addr):
devices[mac_addr] = name
else:
devices[mac] = name
return devices

View File

@ -17,7 +17,7 @@ from homeassistant.helpers.event import track_time_interval
from homeassistant.helpers.typing import ConfigType
from homeassistant.util import slugify
REQUIREMENTS = ['locationsharinglib==2.0.7']
REQUIREMENTS = ['locationsharinglib==2.0.11']
_LOGGER = logging.getLogger(__name__)
@ -26,18 +26,21 @@ ATTR_FULL_NAME = 'full_name'
ATTR_LAST_SEEN = 'last_seen'
ATTR_NICKNAME = 'nickname'
CONF_MAX_GPS_ACCURACY = 'max_gps_accuracy'
CREDENTIALS_FILE = '.google_maps_location_sharing.cookies'
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=30)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Required(CONF_USERNAME): cv.string,
vol.Optional(CONF_MAX_GPS_ACCURACY, default=100000): vol.Coerce(float),
})
def setup_scanner(hass, config: ConfigType, see, discovery_info=None):
"""Set up the scanner."""
"""Set up the Google Maps Location sharing scanner."""
scanner = GoogleMapsScanner(hass, config, see)
return scanner.success_init
@ -53,6 +56,7 @@ class GoogleMapsScanner:
self.see = see
self.username = config[CONF_USERNAME]
self.password = config[CONF_PASSWORD]
self.max_gps_accuracy = config[CONF_MAX_GPS_ACCURACY]
try:
self.service = Service(self.username, self.password,
@ -76,6 +80,14 @@ class GoogleMapsScanner:
_LOGGER.warning("No location(s) shared with this account")
return
if self.max_gps_accuracy is not None and \
person.accuracy > self.max_gps_accuracy:
_LOGGER.info("Ignoring %s update because expected GPS "
"accuracy %s is not met: %s",
person.nickname, self.max_gps_accuracy,
person.accuracy)
continue
attrs = {
ATTR_ADDRESS: person.address,
ATTR_FULL_NAME: person.full_name,

View File

@ -85,8 +85,7 @@ class HuaweiDeviceScanner(DeviceScanner):
active_clients = [client for client in data if client.state]
self.last_results = active_clients
# pylint: disable=logging-not-lazy
_LOGGER.debug("Active clients: " + "\n"
_LOGGER.debug("Active clients: %s", "\n"
.join((client.mac + " " + client.name)
for client in active_clients))
return True

View File

@ -5,18 +5,18 @@ For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/device_tracker.keenetic_ndms2/
"""
import logging
from collections import namedtuple
import requests
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.components.device_tracker import (
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
from homeassistant.const import (
CONF_HOST, CONF_PASSWORD, CONF_USERNAME
CONF_HOST, CONF_PORT, CONF_PASSWORD, CONF_USERNAME
)
REQUIREMENTS = ['ndms2_client==0.0.3']
_LOGGER = logging.getLogger(__name__)
# Interface name to track devices for. Most likely one will not need to
@ -25,11 +25,13 @@ _LOGGER = logging.getLogger(__name__)
CONF_INTERFACE = 'interface'
DEFAULT_INTERFACE = 'Home'
DEFAULT_PORT = 23
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST): cv.string,
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PORT, default=DEFAULT_PORT): cv.port,
vol.Required(CONF_PASSWORD): cv.string,
vol.Required(CONF_INTERFACE, default=DEFAULT_INTERFACE): cv.string,
})
@ -42,21 +44,22 @@ def get_scanner(_hass, config):
return scanner if scanner.success_init else None
Device = namedtuple('Device', ['mac', 'name'])
class KeeneticNDMS2DeviceScanner(DeviceScanner):
"""This class scans for devices using keenetic NDMS2 web interface."""
def __init__(self, config):
"""Initialize the scanner."""
from ndms2_client import Client, TelnetConnection
self.last_results = []
self._url = 'http://%s/rci/show/ip/arp' % config[CONF_HOST]
self._interface = config[CONF_INTERFACE]
self._username = config.get(CONF_USERNAME)
self._password = config.get(CONF_PASSWORD)
self._client = Client(TelnetConnection(
config.get(CONF_HOST),
config.get(CONF_PORT),
config.get(CONF_USERNAME),
config.get(CONF_PASSWORD),
))
self.success_init = self._update_info()
_LOGGER.info("Scanner initialized")
@ -69,53 +72,32 @@ class KeeneticNDMS2DeviceScanner(DeviceScanner):
def get_device_name(self, device):
"""Return the name of the given device or None if we don't know."""
filter_named = [result.name for result in self.last_results
if result.mac == device]
name = next((
result.name for result in self.last_results
if result.mac == device), None)
return name
if filter_named:
return filter_named[0]
return None
def get_extra_attributes(self, device):
"""Return the IP of the given device."""
attributes = next((
{'ip': result.ip} for result in self.last_results
if result.mac == device), {})
return attributes
def _update_info(self):
"""Get ARP from keenetic router."""
_LOGGER.info("Fetching...")
_LOGGER.debug("Fetching devices from router...")
last_results = []
# doing a request
from ndms2_client import ConnectionException
try:
from requests.auth import HTTPDigestAuth
res = requests.get(self._url, timeout=10, auth=HTTPDigestAuth(
self._username, self._password
))
except requests.exceptions.Timeout:
_LOGGER.error(
"Connection to the router timed out at URL %s", self._url)
self.last_results = [
dev
for dev in self._client.get_devices()
if dev.interface == self._interface
]
_LOGGER.debug("Successfully fetched data from router")
return True
except ConnectionException:
_LOGGER.error("Error fetching data from router")
return False
if res.status_code != 200:
_LOGGER.error(
"Connection failed with http code %s", res.status_code)
return False
try:
result = res.json()
except ValueError:
# If json decoder could not parse the response
_LOGGER.error("Failed to parse response from router")
return False
# parsing response
for info in result:
if info.get('interface') != self._interface:
continue
mac = info.get('mac')
name = info.get('name')
# No address = no item :)
if mac is None:
continue
last_results.append(Device(mac.upper(), name))
self.last_results = last_results
_LOGGER.info("Request successful")
return True

View File

@ -15,7 +15,7 @@ from homeassistant.const import (
CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_PORT, CONF_SSL,
CONF_DEVICES, CONF_EXCLUDE)
REQUIREMENTS = ['pynetgear==0.4.0']
REQUIREMENTS = ['pynetgear==0.4.1']
_LOGGER = logging.getLogger(__name__)

View File

@ -38,7 +38,7 @@ class Host:
self.dev_id = dev_id
self._count = config[CONF_PING_COUNT]
if sys.platform == 'win32':
self._ping_cmd = ['ping', '-n 1', '-w', '1000', self.ip_address]
self._ping_cmd = ['ping', '-n', '1', '-w', '1000', self.ip_address]
else:
self._ping_cmd = ['ping', '-n', '-q', '-c1', '-W1',
self.ip_address]

View File

@ -0,0 +1,87 @@
"""
Support for RitAssist Platform.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/device_tracker.ritassist/
"""
import logging
import requests
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.components.device_tracker import PLATFORM_SCHEMA
from homeassistant.const import CONF_USERNAME, CONF_PASSWORD
from homeassistant.helpers.event import track_utc_time_change
REQUIREMENTS = ['ritassist==0.5']
_LOGGER = logging.getLogger(__name__)
CONF_CLIENT_ID = 'client_id'
CONF_CLIENT_SECRET = 'client_secret'
CONF_INCLUDE = 'include'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Required(CONF_CLIENT_ID): cv.string,
vol.Required(CONF_CLIENT_SECRET): cv.string,
vol.Optional(CONF_INCLUDE, default=[]):
vol.All(cv.ensure_list, [cv.string])
})
def setup_scanner(hass, config: dict, see, discovery_info=None):
"""Set up the DeviceScanner and check if login is valid."""
scanner = RitAssistDeviceScanner(config, see)
if not scanner.login(hass):
_LOGGER.error('RitAssist authentication failed')
return False
return True
class RitAssistDeviceScanner:
"""Define a scanner for the RitAssist platform."""
def __init__(self, config, see):
"""Initialize RitAssistDeviceScanner."""
from ritassist import API
self._include = config.get(CONF_INCLUDE)
self._see = see
self._api = API(config.get(CONF_CLIENT_ID),
config.get(CONF_CLIENT_SECRET),
config.get(CONF_USERNAME),
config.get(CONF_PASSWORD))
def setup(self, hass):
"""Setup a timer and start gathering devices."""
self._refresh()
track_utc_time_change(hass,
lambda now: self._refresh(),
second=range(0, 60, 30))
def login(self, hass):
"""Perform a login on the RitAssist API."""
if self._api.login():
self.setup(hass)
return True
return False
def _refresh(self) -> None:
"""Refresh device information from the platform."""
try:
devices = self._api.get_devices()
for device in devices:
if (not self._include or
device.license_plate in self._include):
self._see(dev_id=device.plate_as_id,
gps=(device.latitude, device.longitude),
attributes=device.state_attributes,
icon='mdi:car')
except requests.exceptions.ConnectionError:
_LOGGER.error('ConnectionError: Could not connect to RitAssist')

View File

@ -14,7 +14,7 @@ from homeassistant.components.device_tracker import (
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
from homeassistant.const import CONF_HOST
REQUIREMENTS = ['pysnmp==4.4.4']
REQUIREMENTS = ['pysnmp==4.4.5']
_LOGGER = logging.getLogger(__name__)

View File

@ -13,7 +13,7 @@ import os
import voluptuous as vol
from homeassistant import data_entry_flow
from homeassistant import config_entries
from homeassistant.core import callback
from homeassistant.const import EVENT_HOMEASSISTANT_START
import homeassistant.helpers.config_validation as cv
@ -21,7 +21,7 @@ from homeassistant.helpers.event import async_track_point_in_utc_time
from homeassistant.helpers.discovery import async_load_platform, async_discover
import homeassistant.util.dt as dt_util
REQUIREMENTS = ['netdisco==1.5.0']
REQUIREMENTS = ['netdisco==2.0.0']
DOMAIN = 'discovery'
@ -89,6 +89,7 @@ SERVICE_HANDLERS = {
OPTIONAL_SERVICE_HANDLERS = {
SERVICE_HOMEKIT: ('homekit_controller', None),
'dlna_dmr': ('media_player', 'dlna_dmr'),
}
CONF_IGNORE = 'ignore'
@ -137,7 +138,7 @@ async def async_setup(hass, config):
if service in CONFIG_ENTRY_HANDLERS:
await hass.config_entries.flow.async_init(
CONFIG_ENTRY_HANDLERS[service],
source=data_entry_flow.SOURCE_DISCOVERY,
context={'source': config_entries.SOURCE_DISCOVERY},
data=info
)
return

View File

@ -18,6 +18,9 @@ _LOGGER = logging.getLogger(__name__)
CONF_NIGHT_MODE = 'night_mode'
ATTR_IS_NIGHT_MODE = 'is_night_mode'
ATTR_IS_AUTO_MODE = 'is_auto_mode'
DEPENDENCIES = ['dyson']
DYSON_FAN_DEVICES = 'dyson_fan_devices'
@ -158,7 +161,7 @@ class DysonPureCoolLinkDevice(FanEntity):
def is_on(self):
"""Return true if the entity is on."""
if self._device.state:
return self._device.state.fan_state == "FAN"
return self._device.state.fan_mode == "FAN"
return False
@property
@ -232,3 +235,11 @@ class DysonPureCoolLinkDevice(FanEntity):
def supported_features(self) -> int:
"""Flag supported features."""
return SUPPORT_OSCILLATE | SUPPORT_SET_SPEED
@property
def device_state_attributes(self) -> dict:
"""Return optional state attributes."""
return {
ATTR_IS_NIGHT_MODE: self.is_night_mode,
ATTR_IS_AUTO_MODE: self.is_auto_mode
}

View File

@ -1,187 +0,0 @@
"""
Support for Velbus platform.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/fan.velbus/
"""
import asyncio
import logging
import voluptuous as vol
from homeassistant.components.fan import (
SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH, FanEntity, SUPPORT_SET_SPEED,
PLATFORM_SCHEMA)
from homeassistant.components.velbus import DOMAIN
from homeassistant.const import CONF_NAME, CONF_DEVICES, STATE_OFF
import homeassistant.helpers.config_validation as cv
DEPENDENCIES = ['velbus']
_LOGGER = logging.getLogger(__name__)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_DEVICES): vol.All(cv.ensure_list, [
{
vol.Required('module'): cv.positive_int,
vol.Required('channel_low'): cv.positive_int,
vol.Required('channel_medium'): cv.positive_int,
vol.Required('channel_high'): cv.positive_int,
vol.Required(CONF_NAME): cv.string,
}
])
})
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up Fans."""
velbus = hass.data[DOMAIN]
add_devices(VelbusFan(fan, velbus) for fan in config[CONF_DEVICES])
class VelbusFan(FanEntity):
"""Representation of a Velbus Fan."""
def __init__(self, fan, velbus):
"""Initialize a Velbus light."""
self._velbus = velbus
self._name = fan[CONF_NAME]
self._module = fan['module']
self._channel_low = fan['channel_low']
self._channel_medium = fan['channel_medium']
self._channel_high = fan['channel_high']
self._channels = [self._channel_low, self._channel_medium,
self._channel_high]
self._channels_state = [False, False, False]
self._speed = STATE_OFF
@asyncio.coroutine
def async_added_to_hass(self):
"""Add listener for Velbus messages on bus."""
def _init_velbus():
"""Initialize Velbus on startup."""
self._velbus.subscribe(self._on_message)
self.get_status()
yield from self.hass.async_add_job(_init_velbus)
def _on_message(self, message):
import velbus
if isinstance(message, velbus.RelayStatusMessage) and \
message.address == self._module and \
message.channel in self._channels:
if message.channel == self._channel_low:
self._channels_state[0] = message.is_on()
elif message.channel == self._channel_medium:
self._channels_state[1] = message.is_on()
elif message.channel == self._channel_high:
self._channels_state[2] = message.is_on()
self._calculate_speed()
self.schedule_update_ha_state()
def _calculate_speed(self):
if self._is_off():
self._speed = STATE_OFF
elif self._is_low():
self._speed = SPEED_LOW
elif self._is_medium():
self._speed = SPEED_MEDIUM
elif self._is_high():
self._speed = SPEED_HIGH
def _is_off(self):
return self._channels_state[0] is False and \
self._channels_state[1] is False and \
self._channels_state[2] is False
def _is_low(self):
return self._channels_state[0] is True and \
self._channels_state[1] is False and \
self._channels_state[2] is False
def _is_medium(self):
return self._channels_state[0] is True and \
self._channels_state[1] is True and \
self._channels_state[2] is False
def _is_high(self):
return self._channels_state[0] is True and \
self._channels_state[1] is False and \
self._channels_state[2] is True
@property
def name(self):
"""Return the display name of this light."""
return self._name
@property
def should_poll(self):
"""Disable polling."""
return False
@property
def speed(self):
"""Return the current speed."""
return self._speed
@property
def speed_list(self):
"""Get the list of available speeds."""
return [STATE_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH]
def turn_on(self, speed=None, **kwargs):
"""Turn on the entity."""
if speed is None:
speed = SPEED_MEDIUM
self.set_speed(speed)
def turn_off(self, **kwargs):
"""Turn off the entity."""
self.set_speed(STATE_OFF)
def set_speed(self, speed):
"""Set the speed of the fan."""
channels_off = []
channels_on = []
if speed == STATE_OFF:
channels_off = self._channels
elif speed == SPEED_LOW:
channels_off = [self._channel_medium, self._channel_high]
channels_on = [self._channel_low]
elif speed == SPEED_MEDIUM:
channels_off = [self._channel_high]
channels_on = [self._channel_low, self._channel_medium]
elif speed == SPEED_HIGH:
channels_off = [self._channel_medium]
channels_on = [self._channel_low, self._channel_high]
for channel in channels_off:
self._relay_off(channel)
for channel in channels_on:
self._relay_on(channel)
self.schedule_update_ha_state()
def _relay_on(self, channel):
import velbus
message = velbus.SwitchRelayOnMessage()
message.set_defaults(self._module)
message.relay_channels = [channel]
self._velbus.send(message)
def _relay_off(self, channel):
import velbus
message = velbus.SwitchRelayOffMessage()
message.set_defaults(self._module)
message.relay_channels = [channel]
self._velbus.send(message)
def get_status(self):
"""Retrieve current status."""
import velbus
message = velbus.ModuleStatusRequestMessage()
message.set_defaults(self._module)
message.channels = self._channels
self._velbus.send(message)
@property
def supported_features(self):
"""Flag supported features."""
return SUPPORT_SET_SPEED

View File

@ -26,7 +26,7 @@ from homeassistant.helpers.translation import async_get_translations
from homeassistant.loader import bind_hass
from homeassistant.util.yaml import load_yaml
REQUIREMENTS = ['home-assistant-frontend==20180804.0']
REQUIREMENTS = ['home-assistant-frontend==20180816.1']
DOMAIN = 'frontend'
DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log',
@ -249,6 +249,7 @@ async def async_setup(hass, config):
index_view = IndexView(repo_path, js_version, hass.auth.active)
hass.http.register_view(index_view)
hass.http.register_view(AuthorizeView(repo_path, js_version))
@callback
def async_finalize_panel(panel):
@ -334,6 +335,35 @@ def _async_setup_themes(hass, themes):
hass.services.async_register(DOMAIN, SERVICE_RELOAD_THEMES, reload_themes)
class AuthorizeView(HomeAssistantView):
"""Serve the frontend."""
url = '/auth/authorize'
name = 'auth:authorize'
requires_auth = False
def __init__(self, repo_path, js_option):
"""Initialize the frontend view."""
self.repo_path = repo_path
self.js_option = js_option
async def get(self, request: web.Request):
"""Redirect to the authorize page."""
latest = self.repo_path is not None or \
_is_latest(self.js_option, request)
if latest:
location = '/frontend_latest/authorize.html'
else:
location = '/frontend_es5/authorize.html'
location += '?{}'.format(request.query_string)
return web.Response(status=302, headers={
'location': location
})
class IndexView(HomeAssistantView):
"""Serve the frontend."""

View File

@ -178,7 +178,7 @@ def async_setup(hass, config):
refresh_token = None
if 'hassio_user' in data:
user = yield from hass.auth.async_get_user(data['hassio_user'])
if user:
if user and user.refresh_tokens:
refresh_token = list(user.refresh_tokens.values())[0]
if refresh_token is None:

View File

@ -17,9 +17,11 @@
"hapid": "Accesspoint ID (SGTIN)",
"name": "Name (optional, wird als Pr\u00e4fix f\u00fcr alle Ger\u00e4te verwendet)",
"pin": "PIN Code (optional)"
}
},
"title": "HometicIP Accesspoint ausw\u00e4hlen"
},
"link": {
"description": "Dr\u00fccken Sie den blauen Taster auf dem Accesspoint, sowie den Senden Button um HomematicIP mit Home Assistant zu verbinden.\n\n![Position des Tasters auf dem AP](/static/images/config_flows/config_homematicip_cloud.png)",
"title": "Verkn\u00fcpfe den Accesspoint"
}
},

View File

@ -22,7 +22,7 @@
},
"link": {
"description": "Trykk p\u00e5 den bl\u00e5 knappen p\u00e5 tilgangspunktet og send knappen for \u00e5 registrere HomematicIP med Home Assistant. \n\n![Plassering av knapp p\u00e5 bridge](/static/images/config_flows/config_homematicip_cloud.png)",
"title": "Link Tilgangspunkt"
"title": "Link tilgangspunkt"
}
},
"title": "HomematicIP Sky"

View File

@ -21,7 +21,7 @@
"title": "Escolher ponto de acesso HomematicIP"
},
"link": {
"description": "Pressione o bot\u00e3o azul no accesspoint e o bot\u00e3o enviar para registrar HomematicIP com Home Assistant.\n\n! [Localiza\u00e7\u00e3o do bot\u00e3o na ponte] (/ static/images/config_flows/config_homematicip_cloud.png)",
"description": "Pressione o bot\u00e3o azul no ponto de acesso e o bot\u00e3o enviar para registrar HomematicIP com o Home Assistant.\n\n![Localiza\u00e7\u00e3o do bot\u00e3o na ponte](/ static/images/config_flows/config_homematicip_cloud.png)",
"title": "Associar ponto de acesso"
}
},

View File

@ -10,6 +10,7 @@ import logging
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant import config_entries
from .const import (
DOMAIN, HMIPC_HAPID, HMIPC_AUTHTOKEN, HMIPC_NAME,
@ -41,7 +42,8 @@ async def async_setup(hass, config):
for conf in accesspoints:
if conf[CONF_ACCESSPOINT] not in configured_haps(hass):
hass.async_add_job(hass.config_entries.flow.async_init(
DOMAIN, source='import', data={
DOMAIN, context={'source': config_entries.SOURCE_IMPORT},
data={
HMIPC_HAPID: conf[CONF_ACCESSPOINT],
HMIPC_AUTHTOKEN: conf[CONF_AUTHTOKEN],
HMIPC_NAME: conf[CONF_NAME],

View File

@ -27,6 +27,10 @@ class HomematicipCloudFlowHandler(data_entry_flow.FlowHandler):
"""Initialize HomematicIP Cloud config flow."""
self.auth = None
async def async_step_user(self, user_input=None):
"""Handle a flow initialized by the user."""
return await self.async_step_init(user_input)
async def async_step_init(self, user_input=None):
"""Handle a flow start."""
errors = {}

View File

@ -8,6 +8,7 @@ from ipaddress import ip_network
import logging
import os
import ssl
from typing import Optional
from aiohttp import web
from aiohttp.web_exceptions import HTTPMovedPermanently
@ -16,7 +17,6 @@ import voluptuous as vol
from homeassistant.const import (
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, SERVER_PORT)
import homeassistant.helpers.config_validation as cv
import homeassistant.remote as rem
import homeassistant.util as hass_util
from homeassistant.util.logging import HideSensitiveDataFilter
from homeassistant.util import ssl as ssl_util
@ -49,6 +49,10 @@ CONF_TRUSTED_PROXIES = 'trusted_proxies'
CONF_TRUSTED_NETWORKS = 'trusted_networks'
CONF_LOGIN_ATTEMPTS_THRESHOLD = 'login_attempts_threshold'
CONF_IP_BAN_ENABLED = 'ip_ban_enabled'
CONF_SSL_PROFILE = 'ssl_profile'
SSL_MODERN = 'modern'
SSL_INTERMEDIATE = 'intermediate'
_LOGGER = logging.getLogger(__name__)
@ -66,15 +70,17 @@ HTTP_SCHEMA = vol.Schema({
vol.Optional(CONF_SSL_KEY): cv.isfile,
vol.Optional(CONF_CORS_ORIGINS, default=[]):
vol.All(cv.ensure_list, [cv.string]),
vol.Optional(CONF_USE_X_FORWARDED_FOR, default=False): cv.boolean,
vol.Optional(CONF_TRUSTED_PROXIES, default=[]):
vol.Inclusive(CONF_USE_X_FORWARDED_FOR, 'proxy'): cv.boolean,
vol.Inclusive(CONF_TRUSTED_PROXIES, 'proxy'):
vol.All(cv.ensure_list, [ip_network]),
vol.Optional(CONF_TRUSTED_NETWORKS, default=[]):
vol.All(cv.ensure_list, [ip_network]),
vol.Optional(CONF_LOGIN_ATTEMPTS_THRESHOLD,
default=NO_LOGIN_ATTEMPT_THRESHOLD):
vol.Any(cv.positive_int, NO_LOGIN_ATTEMPT_THRESHOLD),
vol.Optional(CONF_IP_BAN_ENABLED, default=True): cv.boolean
vol.Optional(CONF_IP_BAN_ENABLED, default=True): cv.boolean,
vol.Optional(CONF_SSL_PROFILE, default=SSL_MODERN):
vol.In([SSL_INTERMEDIATE, SSL_MODERN]),
})
CONFIG_SCHEMA = vol.Schema({
@ -82,6 +88,28 @@ CONFIG_SCHEMA = vol.Schema({
}, extra=vol.ALLOW_EXTRA)
class ApiConfig:
"""Configuration settings for API server."""
def __init__(self, host: str, port: Optional[int] = SERVER_PORT,
use_ssl: bool = False,
api_password: Optional[str] = None) -> None:
"""Initialize a new API config object."""
self.host = host
self.port = port
self.api_password = api_password
if host.startswith(("http://", "https://")):
self.base_url = host
elif use_ssl:
self.base_url = "https://{}".format(host)
else:
self.base_url = "http://{}".format(host)
if port is not None:
self.base_url += ':{}'.format(port)
async def async_setup(hass, config):
"""Set up the HTTP API and debug interface."""
conf = config.get(DOMAIN)
@ -96,11 +124,12 @@ async def async_setup(hass, config):
ssl_peer_certificate = conf.get(CONF_SSL_PEER_CERTIFICATE)
ssl_key = conf.get(CONF_SSL_KEY)
cors_origins = conf[CONF_CORS_ORIGINS]
use_x_forwarded_for = conf[CONF_USE_X_FORWARDED_FOR]
trusted_proxies = conf[CONF_TRUSTED_PROXIES]
use_x_forwarded_for = conf.get(CONF_USE_X_FORWARDED_FOR, False)
trusted_proxies = conf.get(CONF_TRUSTED_PROXIES, [])
trusted_networks = conf[CONF_TRUSTED_NETWORKS]
is_ban_enabled = conf[CONF_IP_BAN_ENABLED]
login_threshold = conf[CONF_LOGIN_ATTEMPTS_THRESHOLD]
ssl_profile = conf[CONF_SSL_PROFILE]
if api_password is not None:
logging.getLogger('aiohttp.access').addFilter(
@ -119,7 +148,8 @@ async def async_setup(hass, config):
trusted_proxies=trusted_proxies,
trusted_networks=trusted_networks,
login_threshold=login_threshold,
is_ban_enabled=is_ban_enabled
is_ban_enabled=is_ban_enabled,
ssl_profile=ssl_profile,
)
async def stop_server(event):
@ -146,8 +176,8 @@ async def async_setup(hass, config):
host = hass_util.get_local_ip()
port = server_port
hass.config.api = rem.API(host, api_password, port,
ssl_certificate is not None)
hass.config.api = ApiConfig(host, port, ssl_certificate is not None,
api_password)
return True
@ -159,7 +189,7 @@ class HomeAssistantHTTP:
ssl_certificate, ssl_peer_certificate,
ssl_key, server_host, server_port, cors_origins,
use_x_forwarded_for, trusted_proxies, trusted_networks,
login_threshold, is_ban_enabled):
login_threshold, is_ban_enabled, ssl_profile):
"""Initialize the HTTP Home Assistant server."""
app = self.app = web.Application(
middlewares=[staticresource_middleware])
@ -199,6 +229,7 @@ class HomeAssistantHTTP:
self.server_host = server_host
self.server_port = server_port
self.is_ban_enabled = is_ban_enabled
self.ssl_profile = ssl_profile
self._handler = None
self.server = None
@ -285,7 +316,10 @@ class HomeAssistantHTTP:
if self.ssl_certificate:
try:
context = ssl_util.server_context()
if self.ssl_profile == SSL_INTERMEDIATE:
context = ssl_util.server_context_intermediate()
else:
context = ssl_util.server_context_modern()
context.load_cert_chain(self.ssl_certificate, self.ssl_key)
except OSError as error:
_LOGGER.error("Could not read SSL certificate from %s: %s",

View File

@ -106,11 +106,11 @@ async def async_validate_auth_header(request, api_password=None):
if auth_type == 'Bearer':
hass = request.app['hass']
access_token = hass.auth.async_get_access_token(auth_val)
if access_token is None:
refresh_token = await hass.auth.async_validate_access_token(auth_val)
if refresh_token is None:
return False
request['hass_user'] = access_token.refresh_token.user
request['hass_user'] = refresh_token.user
return True
if auth_type == 'Basic' and api_password is not None:

View File

@ -24,6 +24,6 @@
"title": "Hub verbinden"
}
},
"title": ""
"title": "Philips Hue"
}
}

View File

@ -1,5 +1,9 @@
{
"config": {
"abort": {
"all_configured": "Toate pun\u021bile Philips Hue sunt deja configurate",
"discover_timeout": "Imposibil de descoperit podurile Hue"
},
"error": {
"linking": "A ap\u0103rut o eroare de leg\u0103tur\u0103 necunoscut\u0103.",
"register_failed": "Nu a reu\u0219it \u00eenregistrarea, \u00eencerca\u021bi din nou"

View File

@ -9,7 +9,7 @@ import logging
import voluptuous as vol
from homeassistant import data_entry_flow
from homeassistant import config_entries
from homeassistant.const import CONF_FILENAME, CONF_HOST
from homeassistant.helpers import aiohttp_client, config_validation as cv
@ -108,7 +108,8 @@ async def async_setup(hass, config):
# deadlock: creating a config entry will set up the component but the
# setup would block till the entry is created!
hass.async_add_job(hass.config_entries.flow.async_init(
DOMAIN, source=data_entry_flow.SOURCE_IMPORT, data={
DOMAIN, context={'source': config_entries.SOURCE_IMPORT},
data={
'host': bridge_conf[CONF_HOST],
'path': bridge_conf[CONF_FILENAME],
}

View File

@ -51,7 +51,8 @@ class HueBridge:
# linking procedure. When linking succeeds, it will remove the
# old config entry.
hass.async_add_job(hass.config_entries.flow.async_init(
DOMAIN, source='import', data={
DOMAIN, context={'source': config_entries.SOURCE_IMPORT},
data={
'host': host,
}
))

View File

@ -50,6 +50,10 @@ class HueFlowHandler(data_entry_flow.FlowHandler):
"""Initialize the Hue flow."""
self.host = None
async def async_step_user(self, user_input=None):
"""Handle a flow initialized by the user."""
return await self.async_step_init(user_input)
async def async_step_init(self, user_input=None):
"""Handle a flow start."""
from aiohue.discovery import discover_nupnp

View File

@ -17,25 +17,29 @@ import homeassistant.helpers.config_validation as cv
from homeassistant.components.image_processing import (
PLATFORM_SCHEMA, ImageProcessingFaceEntity, ATTR_CONFIDENCE, CONF_SOURCE,
CONF_ENTITY_ID, CONF_NAME, DOMAIN)
from homeassistant.const import (CONF_IP_ADDRESS, CONF_PORT)
from homeassistant.const import (
CONF_IP_ADDRESS, CONF_PORT, CONF_PASSWORD, CONF_USERNAME,
HTTP_BAD_REQUEST, HTTP_OK, HTTP_UNAUTHORIZED)
_LOGGER = logging.getLogger(__name__)
ATTR_BOUNDING_BOX = 'bounding_box'
ATTR_CLASSIFIER = 'classifier'
ATTR_IMAGE_ID = 'image_id'
ATTR_ID = 'id'
ATTR_MATCHED = 'matched'
FACEBOX_NAME = 'name'
CLASSIFIER = 'facebox'
DATA_FACEBOX = 'facebox_classifiers'
EVENT_CLASSIFIER_TEACH = 'image_processing.teach_classifier'
FILE_PATH = 'file_path'
SERVICE_TEACH_FACE = 'facebox_teach_face'
TIMEOUT = 9
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_IP_ADDRESS): cv.string,
vol.Required(CONF_PORT): cv.port,
vol.Optional(CONF_USERNAME): cv.string,
vol.Optional(CONF_PASSWORD): cv.string,
})
SERVICE_TEACH_SCHEMA = vol.Schema({
@ -45,6 +49,26 @@ SERVICE_TEACH_SCHEMA = vol.Schema({
})
def check_box_health(url, username, password):
"""Check the health of the classifier and return its id if healthy."""
kwargs = {}
if username:
kwargs['auth'] = requests.auth.HTTPBasicAuth(username, password)
try:
response = requests.get(
url,
**kwargs
)
if response.status_code == HTTP_UNAUTHORIZED:
_LOGGER.error("AuthenticationError on %s", CLASSIFIER)
return None
if response.status_code == HTTP_OK:
return response.json()['hostname']
except requests.exceptions.ConnectionError:
_LOGGER.error("ConnectionError: Is %s running?", CLASSIFIER)
return None
def encode_image(image):
"""base64 encode an image stream."""
base64_img = base64.b64encode(image).decode('ascii')
@ -63,10 +87,10 @@ def parse_faces(api_faces):
for entry in api_faces:
face = {}
if entry['matched']: # This data is only in matched faces.
face[ATTR_NAME] = entry['name']
face[FACEBOX_NAME] = entry['name']
face[ATTR_IMAGE_ID] = entry['id']
else: # Lets be explicit.
face[ATTR_NAME] = None
face[FACEBOX_NAME] = None
face[ATTR_IMAGE_ID] = None
face[ATTR_CONFIDENCE] = round(100.0*entry['confidence'], 2)
face[ATTR_MATCHED] = entry['matched']
@ -75,17 +99,46 @@ def parse_faces(api_faces):
return known_faces
def post_image(url, image):
def post_image(url, image, username, password):
"""Post an image to the classifier."""
kwargs = {}
if username:
kwargs['auth'] = requests.auth.HTTPBasicAuth(username, password)
try:
response = requests.post(
url,
json={"base64": encode_image(image)},
timeout=TIMEOUT
**kwargs
)
if response.status_code == HTTP_UNAUTHORIZED:
_LOGGER.error("AuthenticationError on %s", CLASSIFIER)
return None
return response
except requests.exceptions.ConnectionError:
_LOGGER.error("ConnectionError: Is %s running?", CLASSIFIER)
return None
def teach_file(url, name, file_path, username, password):
"""Teach the classifier a name associated with a file."""
kwargs = {}
if username:
kwargs['auth'] = requests.auth.HTTPBasicAuth(username, password)
try:
with open(file_path, 'rb') as open_file:
response = requests.post(
url,
data={FACEBOX_NAME: name, ATTR_ID: file_path},
files={'file': open_file},
**kwargs
)
if response.status_code == HTTP_UNAUTHORIZED:
_LOGGER.error("AuthenticationError on %s", CLASSIFIER)
elif response.status_code == HTTP_BAD_REQUEST:
_LOGGER.error("%s teaching of file %s failed with message:%s",
CLASSIFIER, file_path, response.text)
except requests.exceptions.ConnectionError:
_LOGGER.error("ConnectionError: Is %s running?", CLASSIFIER)
def valid_file_path(file_path):
@ -104,13 +157,20 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
if DATA_FACEBOX not in hass.data:
hass.data[DATA_FACEBOX] = []
ip_address = config[CONF_IP_ADDRESS]
port = config[CONF_PORT]
username = config.get(CONF_USERNAME)
password = config.get(CONF_PASSWORD)
url_health = "http://{}:{}/healthz".format(ip_address, port)
hostname = check_box_health(url_health, username, password)
if hostname is None:
return
entities = []
for camera in config[CONF_SOURCE]:
facebox = FaceClassifyEntity(
config[CONF_IP_ADDRESS],
config[CONF_PORT],
camera[CONF_ENTITY_ID],
camera.get(CONF_NAME))
ip_address, port, username, password, hostname,
camera[CONF_ENTITY_ID], camera.get(CONF_NAME))
entities.append(facebox)
hass.data[DATA_FACEBOX].append(facebox)
add_devices(entities)
@ -129,33 +189,37 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
classifier.teach(name, file_path)
hass.services.register(
DOMAIN,
SERVICE_TEACH_FACE,
service_handle,
DOMAIN, SERVICE_TEACH_FACE, service_handle,
schema=SERVICE_TEACH_SCHEMA)
class FaceClassifyEntity(ImageProcessingFaceEntity):
"""Perform a face classification."""
def __init__(self, ip, port, camera_entity, name=None):
def __init__(self, ip_address, port, username, password, hostname,
camera_entity, name=None):
"""Init with the API key and model id."""
super().__init__()
self._url_check = "http://{}:{}/{}/check".format(ip, port, CLASSIFIER)
self._url_teach = "http://{}:{}/{}/teach".format(ip, port, CLASSIFIER)
self._url_check = "http://{}:{}/{}/check".format(
ip_address, port, CLASSIFIER)
self._url_teach = "http://{}:{}/{}/teach".format(
ip_address, port, CLASSIFIER)
self._username = username
self._password = password
self._hostname = hostname
self._camera = camera_entity
if name:
self._name = name
else:
camera_name = split_entity_id(camera_entity)[1]
self._name = "{} {}".format(
CLASSIFIER, camera_name)
self._name = "{} {}".format(CLASSIFIER, camera_name)
self._matched = {}
def process_image(self, image):
"""Process an image."""
response = post_image(self._url_check, image)
if response is not None:
response = post_image(
self._url_check, image, self._username, self._password)
if response:
response_json = response.json()
if response_json['success']:
total_faces = response_json['facesCount']
@ -173,34 +237,8 @@ class FaceClassifyEntity(ImageProcessingFaceEntity):
if (not self.hass.config.is_allowed_path(file_path)
or not valid_file_path(file_path)):
return
with open(file_path, 'rb') as open_file:
response = requests.post(
self._url_teach,
data={ATTR_NAME: name, 'id': file_path},
files={'file': open_file})
if response.status_code == 200:
self.hass.bus.fire(
EVENT_CLASSIFIER_TEACH, {
ATTR_CLASSIFIER: CLASSIFIER,
ATTR_NAME: name,
FILE_PATH: file_path,
'success': True,
'message': None
})
elif response.status_code == 400:
_LOGGER.warning(
"%s teaching of file %s failed with message:%s",
CLASSIFIER, file_path, response.text)
self.hass.bus.fire(
EVENT_CLASSIFIER_TEACH, {
ATTR_CLASSIFIER: CLASSIFIER,
ATTR_NAME: name,
FILE_PATH: file_path,
'success': False,
'message': response.text
})
teach_file(
self._url_teach, name, file_path, self._username, self._password)
@property
def camera_entity(self):
@ -218,4 +256,5 @@ class FaceClassifyEntity(ImageProcessingFaceEntity):
return {
'matched_faces': self._matched,
'total_matched_faces': len(self._matched),
'hostname': self._hostname
}

View File

@ -4,9 +4,9 @@ Support for deCONZ light.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/light.deconz/
"""
from homeassistant.components.deconz import (
DOMAIN as DATA_DECONZ, DATA_DECONZ_ID, DATA_DECONZ_UNSUB)
from homeassistant.components.deconz.const import CONF_ALLOW_DECONZ_GROUPS
from homeassistant.components.deconz.const import (
CONF_ALLOW_DECONZ_GROUPS, DOMAIN as DATA_DECONZ,
DATA_DECONZ_ID, DATA_DECONZ_UNSUB, SWITCH_TYPES)
from homeassistant.components.light import (
ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_FLASH, ATTR_HS_COLOR,
ATTR_TRANSITION, EFFECT_COLORLOOP, FLASH_LONG, FLASH_SHORT,
@ -32,7 +32,8 @@ async def async_setup_entry(hass, config_entry, async_add_devices):
"""Add light from deCONZ."""
entities = []
for light in lights:
entities.append(DeconzLight(light))
if light.type not in SWITCH_TYPES:
entities.append(DeconzLight(light))
async_add_devices(entities, True)
hass.data[DATA_DECONZ_UNSUB].append(
@ -189,3 +190,12 @@ class DeconzLight(Light):
del data['on']
await self._light.async_set_state(data)
@property
def device_state_attributes(self):
"""Return the device state attributes."""
attributes = {}
attributes['is_deconz_group'] = self._light.type == 'LightGroup'
if self._light.type == 'LightGroup':
attributes['all_on'] = self._light.all_on
return attributes

View File

@ -254,8 +254,6 @@ def _mean_tuple(*args):
return tuple(sum(l) / len(l) for l in zip(*args))
# https://github.com/PyCQA/pylint/issues/1831
# pylint: disable=bad-whitespace
def _reduce_attribute(states: List[State],
key: str,
default: Optional[Any] = None,

View File

@ -1,104 +0,0 @@
"""
Support for Velbus lights.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/light.velbus/
"""
import asyncio
import logging
import voluptuous as vol
from homeassistant.const import CONF_NAME, CONF_DEVICES
from homeassistant.components.light import Light, PLATFORM_SCHEMA
from homeassistant.components.velbus import DOMAIN
import homeassistant.helpers.config_validation as cv
DEPENDENCIES = ['velbus']
_LOGGER = logging.getLogger(__name__)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_DEVICES): vol.All(cv.ensure_list, [
{
vol.Required('module'): cv.positive_int,
vol.Required('channel'): cv.positive_int,
vol.Required(CONF_NAME): cv.string
}
])
})
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up Lights."""
velbus = hass.data[DOMAIN]
add_devices(VelbusLight(light, velbus) for light in config[CONF_DEVICES])
class VelbusLight(Light):
"""Representation of a Velbus Light."""
def __init__(self, light, velbus):
"""Initialize a Velbus light."""
self._velbus = velbus
self._name = light[CONF_NAME]
self._module = light['module']
self._channel = light['channel']
self._state = False
@asyncio.coroutine
def async_added_to_hass(self):
"""Add listener for Velbus messages on bus."""
def _init_velbus():
"""Initialize Velbus on startup."""
self._velbus.subscribe(self._on_message)
self.get_status()
yield from self.hass.async_add_job(_init_velbus)
def _on_message(self, message):
import velbus
if isinstance(message, velbus.RelayStatusMessage) and \
message.address == self._module and \
message.channel == self._channel:
self._state = message.is_on()
self.schedule_update_ha_state()
@property
def name(self):
"""Return the display name of this light."""
return self._name
@property
def should_poll(self):
"""Disable polling."""
return False
@property
def is_on(self):
"""Return true if the light is on."""
return self._state
def turn_on(self, **kwargs):
"""Instruct the light to turn on."""
import velbus
message = velbus.SwitchRelayOnMessage()
message.set_defaults(self._module)
message.relay_channels = [self._channel]
self._velbus.send(message)
def turn_off(self, **kwargs):
"""Instruct the light to turn off."""
import velbus
message = velbus.SwitchRelayOffMessage()
message.set_defaults(self._module)
message.relay_channels = [self._channel]
self._velbus.send(message)
def get_status(self):
"""Retrieve current status."""
import velbus
message = velbus.ModuleStatusRequestMessage()
message.set_defaults(self._module)
message.channels = [self._channel]
self._velbus.send(message)

View File

@ -10,5 +10,5 @@ DOMAIN = 'map'
async def async_setup(hass, config):
"""Register the built-in map panel."""
await hass.components.frontend.async_register_built_in_panel(
'map', 'map', 'mdi:account-location')
'map', 'map', 'hass:account-location')
return True

View File

@ -14,7 +14,7 @@ from homeassistant.components.media_player import (
SERVICE_PLAY_MEDIA)
from homeassistant.helpers import config_validation as cv
REQUIREMENTS = ['youtube_dl==2018.07.29']
REQUIREMENTS = ['youtube_dl==2018.08.04']
_LOGGER = logging.getLogger(__name__)

View File

@ -0,0 +1,400 @@
# -*- coding: utf-8 -*-
"""
Support for DLNA DMR (Device Media Renderer).
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/media_player.dlna_dmr/
"""
import asyncio
import functools
import logging
from datetime import datetime
import aiohttp
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.components.media_player import (
SUPPORT_PLAY, SUPPORT_PAUSE, SUPPORT_STOP,
SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET,
SUPPORT_PLAY_MEDIA,
SUPPORT_PREVIOUS_TRACK, SUPPORT_NEXT_TRACK,
MediaPlayerDevice,
PLATFORM_SCHEMA)
from homeassistant.const import (
EVENT_HOMEASSISTANT_STOP,
CONF_URL, CONF_NAME,
STATE_OFF, STATE_ON, STATE_IDLE, STATE_PLAYING, STATE_PAUSED)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import PlatformNotReady
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.util import get_local_ip
DLNA_DMR_DATA = 'dlna_dmr'
REQUIREMENTS = [
'async-upnp-client==0.12.3',
]
DEFAULT_NAME = 'DLNA Digital Media Renderer'
DEFAULT_LISTEN_PORT = 8301
CONF_LISTEN_IP = 'listen_ip'
CONF_LISTEN_PORT = 'listen_port'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_URL): cv.string,
vol.Optional(CONF_LISTEN_IP): cv.string,
vol.Optional(CONF_LISTEN_PORT, default=DEFAULT_LISTEN_PORT): cv.port,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
})
HOME_ASSISTANT_UPNP_CLASS_MAPPING = {
'music': 'object.item.audioItem',
'tvshow': 'object.item.videoItem',
'video': 'object.item.videoItem',
'episode': 'object.item.videoItem',
'channel': 'object.item.videoItem',
'playlist': 'object.item.playlist',
}
HOME_ASSISTANT_UPNP_MIME_TYPE_MAPPING = {
'music': 'audio/*',
'tvshow': 'video/*',
'video': 'video/*',
'episode': 'video/*',
'channel': 'video/*',
'playlist': 'playlist/*',
}
_LOGGER = logging.getLogger(__name__)
def catch_request_errors():
"""Catch asyncio.TimeoutError, aiohttp.ClientError errors."""
def call_wrapper(func):
"""Call wrapper for decorator."""
@functools.wraps(func)
def wrapper(self, *args, **kwargs):
"""Catch asyncio.TimeoutError, aiohttp.ClientError errors."""
try:
return func(self, *args, **kwargs)
except (asyncio.TimeoutError, aiohttp.ClientError):
_LOGGER.error("Error during call %s", func.__name__)
return wrapper
return call_wrapper
async def async_start_event_handler(hass, server_host, server_port, requester):
"""Register notify view."""
hass_data = hass.data[DLNA_DMR_DATA]
if 'event_handler' in hass_data:
return hass_data['event_handler']
# start event handler
from async_upnp_client.aiohttp import AiohttpNotifyServer
server = AiohttpNotifyServer(requester,
server_port,
server_host,
hass.loop)
await server.start_server()
_LOGGER.info('UPNP/DLNA event handler listening on: %s',
server.callback_url)
hass_data['notify_server'] = server
hass_data['event_handler'] = server.event_handler
# register for graceful shutdown
async def async_stop_server(event):
"""Stop server."""
_LOGGER.debug('Stopping UPNP/DLNA event handler')
await server.stop_server()
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, async_stop_server)
return hass_data['event_handler']
async def async_setup_platform(hass: HomeAssistant,
config,
async_add_devices,
discovery_info=None):
"""Set up DLNA DMR platform."""
if config.get(CONF_URL) is not None:
url = config[CONF_URL]
name = config.get(CONF_NAME)
elif discovery_info is not None:
url = discovery_info['ssdp_description']
name = discovery_info.get('name')
if DLNA_DMR_DATA not in hass.data:
hass.data[DLNA_DMR_DATA] = {}
if 'lock' not in hass.data[DLNA_DMR_DATA]:
hass.data[DLNA_DMR_DATA]['lock'] = asyncio.Lock()
# build upnp/aiohttp requester
from async_upnp_client.aiohttp import AiohttpSessionRequester
session = async_get_clientsession(hass)
requester = AiohttpSessionRequester(session, True)
# ensure event handler has been started
with await hass.data[DLNA_DMR_DATA]['lock']:
server_host = config.get(CONF_LISTEN_IP)
if server_host is None:
server_host = get_local_ip()
server_port = config.get(CONF_LISTEN_PORT, DEFAULT_LISTEN_PORT)
event_handler = await async_start_event_handler(hass,
server_host,
server_port,
requester)
# create upnp device
from async_upnp_client import UpnpFactory
factory = UpnpFactory(requester, disable_state_variable_validation=True)
try:
upnp_device = await factory.async_create_device(url)
except (asyncio.TimeoutError, aiohttp.ClientError):
raise PlatformNotReady()
# wrap with DmrDevice
from async_upnp_client.dlna import DmrDevice
dlna_device = DmrDevice(upnp_device, event_handler)
# create our own device
device = DlnaDmrDevice(dlna_device, name)
_LOGGER.debug("Adding device: %s", device)
async_add_devices([device], True)
class DlnaDmrDevice(MediaPlayerDevice):
"""Representation of a DLNA DMR device."""
def __init__(self, dmr_device, name=None):
"""Initializer."""
self._device = dmr_device
self._name = name
self._available = False
self._subscription_renew_time = None
async def async_added_to_hass(self):
"""Callback when added."""
self._device.on_event = self._on_event
# register unsubscribe on stop
bus = self.hass.bus
bus.async_listen_once(EVENT_HOMEASSISTANT_STOP,
self._async_on_hass_stop)
@property
def available(self):
"""Device is available."""
return self._available
async def _async_on_hass_stop(self, event):
"""Event handler on HASS stop."""
with await self.hass.data[DLNA_DMR_DATA]['lock']:
await self._device.async_unsubscribe_services()
async def async_update(self):
"""Retrieve the latest data."""
was_available = self._available
try:
await self._device.async_update()
self._available = True
except (asyncio.TimeoutError, aiohttp.ClientError):
self._available = False
_LOGGER.debug("Device unavailable")
return
# do we need to (re-)subscribe?
now = datetime.now()
should_renew = self._subscription_renew_time and \
now >= self._subscription_renew_time
if should_renew or \
not was_available and self._available:
try:
timeout = await self._device.async_subscribe_services()
self._subscription_renew_time = datetime.now() + timeout / 2
except (asyncio.TimeoutError, aiohttp.ClientError):
self._available = False
_LOGGER.debug("Could not (re)subscribe")
def _on_event(self, service, state_variables):
"""State variable(s) changed, let home-assistant know."""
self.schedule_update_ha_state()
@property
def supported_features(self):
"""Flag media player features that are supported."""
supported_features = 0
if self._device.has_volume_level:
supported_features |= SUPPORT_VOLUME_SET
if self._device.has_volume_mute:
supported_features |= SUPPORT_VOLUME_MUTE
if self._device.has_play:
supported_features |= SUPPORT_PLAY
if self._device.has_pause:
supported_features |= SUPPORT_PAUSE
if self._device.has_stop:
supported_features |= SUPPORT_STOP
if self._device.has_previous:
supported_features |= SUPPORT_PREVIOUS_TRACK
if self._device.has_next:
supported_features |= SUPPORT_NEXT_TRACK
if self._device.has_play_media:
supported_features |= SUPPORT_PLAY_MEDIA
return supported_features
@property
def volume_level(self):
"""Volume level of the media player (0..1)."""
return self._device.volume_level
@catch_request_errors()
async def async_set_volume_level(self, volume):
"""Set volume level, range 0..1."""
await self._device.async_set_volume_level(volume)
@property
def is_volume_muted(self):
"""Boolean if volume is currently muted."""
return self._device.is_volume_muted
@catch_request_errors()
async def async_mute_volume(self, mute):
"""Mute the volume."""
desired_mute = bool(mute)
await self._device.async_mute_volume(desired_mute)
@catch_request_errors()
async def async_media_pause(self):
"""Send pause command."""
if not self._device.can_pause:
_LOGGER.debug('Cannot do Pause')
return
await self._device.async_pause()
@catch_request_errors()
async def async_media_play(self):
"""Send play command."""
if not self._device.can_play:
_LOGGER.debug('Cannot do Play')
return
await self._device.async_play()
@catch_request_errors()
async def async_media_stop(self):
"""Send stop command."""
if not self._device.can_stop:
_LOGGER.debug('Cannot do Stop')
return
await self._device.async_stop()
@catch_request_errors()
async def async_play_media(self, media_type, media_id, **kwargs):
"""Play a piece of media."""
title = "Home Assistant"
mime_type = HOME_ASSISTANT_UPNP_MIME_TYPE_MAPPING[media_type]
upnp_class = HOME_ASSISTANT_UPNP_CLASS_MAPPING[media_type]
# stop current playing media
if self._device.can_stop:
await self.async_media_stop()
# queue media
await self._device.async_set_transport_uri(media_id,
title,
mime_type,
upnp_class)
await self._device.async_wait_for_can_play()
# if already playing, no need to call Play
from async_upnp_client import dlna
if self._device.state == dlna.STATE_PLAYING:
return
# play it
await self.async_media_play()
@catch_request_errors()
async def async_media_previous_track(self):
"""Send previous track command."""
if not self._device.can_previous:
_LOGGER.debug('Cannot do Previous')
return
await self._device.async_previous()
@catch_request_errors()
async def async_media_next_track(self):
"""Send next track command."""
if not self._device.can_next:
_LOGGER.debug('Cannot do Next')
return
await self._device.async_next()
@property
def media_title(self):
"""Title of current playing media."""
return self._device.media_title
@property
def media_image_url(self):
"""Image url of current playing media."""
return self._device.media_image_url
@property
def state(self):
"""State of the player."""
if not self._available:
return STATE_OFF
from async_upnp_client import dlna
if self._device.state is None:
return STATE_ON
if self._device.state == dlna.STATE_PLAYING:
return STATE_PLAYING
if self._device.state == dlna.STATE_PAUSED:
return STATE_PAUSED
return STATE_IDLE
@property
def media_duration(self):
"""Duration of current playing media in seconds."""
return self._device.media_duration
@property
def media_position(self):
"""Position of current playing media in seconds."""
return self._device.media_position
@property
def media_position_updated_at(self):
"""When was the position of the current playing media valid.
Returns value from homeassistant.util.dt.utcnow().
"""
return self._device.media_position_updated_at
@property
def name(self) -> str:
"""Return the name of the device."""
if self._name:
return self._name
return self._device.name
@property
def unique_id(self) -> str:
"""Return an unique ID."""
return self._device.udn

View File

@ -160,6 +160,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
if DATA_KODI not in hass.data:
hass.data[DATA_KODI] = dict()
unique_id = None
# Is this a manual configuration?
if discovery_info is None:
name = config.get(CONF_NAME)
@ -175,6 +176,9 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
tcp_port = DEFAULT_TCP_PORT
encryption = DEFAULT_PROXY_SSL
websocket = DEFAULT_ENABLE_WEBSOCKET
properties = discovery_info.get('properties')
if properties is not None:
unique_id = properties.get('uuid', None)
# Only add a device once, so discovered devices do not override manual
# config.
@ -182,6 +186,14 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
if ip_addr in hass.data[DATA_KODI]:
return
# If we got an unique id, check that it does not exist already.
# This is necessary as netdisco does not deterministally return the same
# advertisement when the service is offered over multiple IP addresses.
if unique_id is not None:
for device in hass.data[DATA_KODI].values():
if device.unique_id == unique_id:
return
entity = KodiDevice(
hass,
name=name,
@ -190,7 +202,8 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
password=config.get(CONF_PASSWORD),
turn_on_action=config.get(CONF_TURN_ON_ACTION),
turn_off_action=config.get(CONF_TURN_OFF_ACTION),
timeout=config.get(CONF_TIMEOUT), websocket=websocket)
timeout=config.get(CONF_TIMEOUT), websocket=websocket,
unique_id=unique_id)
hass.data[DATA_KODI][ip_addr] = entity
async_add_devices([entity], update_before_add=True)
@ -260,12 +273,14 @@ class KodiDevice(MediaPlayerDevice):
def __init__(self, hass, name, host, port, tcp_port, encryption=False,
username=None, password=None,
turn_on_action=None, turn_off_action=None,
timeout=DEFAULT_TIMEOUT, websocket=True):
timeout=DEFAULT_TIMEOUT, websocket=True,
unique_id=None):
"""Initialize the Kodi device."""
import jsonrpc_async
import jsonrpc_websocket
self.hass = hass
self._name = name
self._unique_id = unique_id
kwargs = {
'timeout': timeout,
@ -384,6 +399,11 @@ class KodiDevice(MediaPlayerDevice):
_LOGGER.debug("Unable to fetch kodi data", exc_info=True)
return None
@property
def unique_id(self):
"""Return the unique id of the device."""
return self._unique_id
@property
def state(self):
"""Return the state of the device."""

View File

@ -25,7 +25,7 @@ from homeassistant.const import (
)
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['pymediaroom==0.6.3']
REQUIREMENTS = ['pymediaroom==0.6.4']
_LOGGER = logging.getLogger(__name__)

View File

@ -0,0 +1,157 @@
"""
Support for controlling projector via the PJLink protocol.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/media_player.pjlink/
"""
import logging
import voluptuous as vol
from homeassistant.components.media_player import (
SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE,
SUPPORT_SELECT_SOURCE, PLATFORM_SCHEMA, MediaPlayerDevice)
from homeassistant.const import (
STATE_OFF, STATE_ON, CONF_HOST,
CONF_NAME, CONF_PASSWORD, CONF_PORT)
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['pypjlink2==1.2.0']
_LOGGER = logging.getLogger(__name__)
CONF_ENCODING = 'encoding'
DEFAULT_PORT = 4352
DEFAULT_ENCODING = 'utf-8'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
vol.Optional(CONF_NAME): cv.string,
vol.Optional(CONF_ENCODING, default=DEFAULT_ENCODING): cv.string,
vol.Optional(CONF_PASSWORD): cv.string,
})
SUPPORT_PJLINK = SUPPORT_VOLUME_MUTE | \
SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_SELECT_SOURCE
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the PJLink platform."""
host = config.get(CONF_HOST)
port = config.get(CONF_PORT)
name = config.get(CONF_NAME)
encoding = config.get(CONF_ENCODING)
password = config.get(CONF_PASSWORD)
if 'pjlink' not in hass.data:
hass.data['pjlink'] = {}
hass_data = hass.data['pjlink']
device_label = "{}:{}".format(host, port)
if device_label in hass_data:
return
device = PjLinkDevice(host, port, name, encoding, password)
hass_data[device_label] = device
add_devices([device], True)
def format_input_source(input_source_name, input_source_number):
"""Format input source for display in UI."""
return "{} {}".format(input_source_name, input_source_number)
class PjLinkDevice(MediaPlayerDevice):
"""Representation of a PJLink device."""
def __init__(self, host, port, name, encoding, password):
"""Iinitialize the PJLink device."""
self._host = host
self._port = port
self._name = name
self._password = password
self._encoding = encoding
self._muted = False
self._pwstate = STATE_OFF
self._current_source = None
with self.projector() as projector:
if not self._name:
self._name = projector.get_name()
inputs = projector.get_inputs()
self._source_name_mapping = \
{format_input_source(*x): x for x in inputs}
self._source_list = sorted(self._source_name_mapping.keys())
def projector(self):
"""Create PJLink Projector instance."""
from pypjlink import Projector
projector = Projector.from_address(self._host, self._port,
self._encoding)
projector.authenticate(self._password)
return projector
def update(self):
"""Get the latest state from the device."""
with self.projector() as projector:
pwstate = projector.get_power()
if pwstate == 'off':
self._pwstate = STATE_OFF
else:
self._pwstate = STATE_ON
self._muted = projector.get_mute()[1]
self._current_source = \
format_input_source(*projector.get_input())
@property
def name(self):
"""Return the name of the device."""
return self._name
@property
def state(self):
"""Return the state of the device."""
return self._pwstate
@property
def is_volume_muted(self):
"""Return boolean indicating mute status."""
return self._muted
@property
def source(self):
"""Return current input source."""
return self._current_source
@property
def source_list(self):
"""Return all available input sources."""
return self._source_list
@property
def supported_features(self):
"""Return projector supported features."""
return SUPPORT_PJLINK
def turn_off(self):
"""Turn projector off."""
with self.projector() as projector:
projector.set_power('off')
def turn_on(self):
"""Turn projector on."""
with self.projector() as projector:
projector.set_power('on')
def mute_volume(self, mute):
"""Mute (true) of unmute (false) media player."""
with self.projector() as projector:
from pypjlink import MUTE_AUDIO
projector.set_mute(MUTE_AUDIO, mute)
def select_source(self, source):
"""Set the input source."""
source = self._source_name_mapping[source]
with self.projector() as projector:
projector.set_input(*source)

View File

@ -32,7 +32,8 @@ from homeassistant.util.async_ import (
from homeassistant.const import (
EVENT_HOMEASSISTANT_STOP, CONF_VALUE_TEMPLATE, CONF_USERNAME,
CONF_PASSWORD, CONF_PORT, CONF_PROTOCOL, CONF_PAYLOAD)
from homeassistant.components.mqtt.server import HBMQTT_CONFIG_SCHEMA
from .server import HBMQTT_CONFIG_SCHEMA
REQUIREMENTS = ['paho-mqtt==1.3.1']
@ -306,7 +307,8 @@ async def _async_setup_server(hass: HomeAssistantType,
return None
success, broker_config = \
await server.async_start(hass, conf.get(CONF_EMBEDDED))
await server.async_start(
hass, conf.get(CONF_PASSWORD), conf.get(CONF_EMBEDDED))
if not success:
return None
@ -349,6 +351,16 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool:
if CONF_EMBEDDED not in conf and CONF_BROKER in conf:
broker_config = None
else:
if (conf.get(CONF_PASSWORD) is None and
config.get('http') is not None and
config['http'].get('api_password') is not None):
_LOGGER.error(
"Starting from release 0.76, the embedded MQTT broker does not"
" use api_password as default password anymore. Please set"
" password configuration. See https://home-assistant.io/docs/"
"mqtt/broker#embedded-broker for details")
return False
broker_config = await _async_setup_server(hass, config)
if CONF_BROKER in conf:

View File

@ -27,27 +27,29 @@ HBMQTT_CONFIG_SCHEMA = vol.Any(None, vol.Schema({
})
}, extra=vol.ALLOW_EXTRA))
_LOGGER = logging.getLogger(__name__)
@asyncio.coroutine
def async_start(hass, server_config):
def async_start(hass, password, server_config):
"""Initialize MQTT Server.
This method is a coroutine.
"""
from hbmqtt.broker import Broker, BrokerException
passwd = tempfile.NamedTemporaryFile()
try:
passwd = tempfile.NamedTemporaryFile()
if server_config is None:
server_config, client_config = generate_config(hass, passwd)
server_config, client_config = generate_config(
hass, passwd, password)
else:
client_config = None
broker = Broker(server_config, hass.loop)
yield from broker.start()
except BrokerException:
logging.getLogger(__name__).exception("Error initializing MQTT server")
_LOGGER.exception("Error initializing MQTT server")
return False, None
finally:
passwd.close()
@ -63,9 +65,10 @@ def async_start(hass, server_config):
return True, client_config
def generate_config(hass, passwd):
def generate_config(hass, passwd, password):
"""Generate a configuration based on current Home Assistant instance."""
from homeassistant.components.mqtt import PROTOCOL_311
from . import PROTOCOL_311
config = {
'listeners': {
'default': {
@ -79,29 +82,26 @@ def generate_config(hass, passwd):
},
},
'auth': {
'allow-anonymous': hass.config.api.api_password is None
'allow-anonymous': password is None
},
'plugins': ['auth_anonymous'],
}
if hass.config.api.api_password:
if password:
username = 'homeassistant'
password = hass.config.api.api_password
# Encrypt with what hbmqtt uses to verify
from passlib.apps import custom_app_context
passwd.write(
'homeassistant:{}\n'.format(
custom_app_context.encrypt(
hass.config.api.api_password)).encode('utf-8'))
custom_app_context.encrypt(password)).encode('utf-8'))
passwd.flush()
config['auth']['password-file'] = passwd.name
config['plugins'].append('auth_file')
else:
username = None
password = None
client_config = ('localhost', 1883, username, password, None, PROTOCOL_311)

View File

@ -22,7 +22,7 @@ from .const import (
from .device import get_mysensors_devices
from .gateway import get_mysensors_gateway, setup_gateways, finish_setup
REQUIREMENTS = ['pymysensors==0.16.0']
REQUIREMENTS = ['pymysensors==0.17.0']
_LOGGER = logging.getLogger(__name__)

View File

@ -186,12 +186,16 @@ def _discover_mysensors_platform(hass, platform, new_devices):
async def _gw_start(hass, gateway):
"""Start the gateway."""
# Don't use hass.async_create_task to avoid holding up setup indefinitely.
connect_task = hass.loop.create_task(gateway.start())
@callback
def gw_stop(event):
"""Trigger to stop the gateway."""
hass.async_add_job(gateway.stop())
if not connect_task.done():
connect_task.cancel()
await gateway.start()
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, gw_stop)
if gateway.device == 'mqtt':
# Gatways connected via mqtt doesn't send gateway ready message.

View File

@ -2,6 +2,8 @@
"config": {
"abort": {
"already_setup": "Sie k\u00f6nnen nur ein einziges Nest-Konto konfigurieren.",
"authorize_url_fail": "Unbekannter Fehler beim Erstellen der Authorisierungs-URL",
"authorize_url_timeout": "Zeit\u00fcberschreitung beim Erstellen der Authorisierungs-URL",
"no_flows": "Sie m\u00fcssen Nest konfigurieren, bevor Sie sich authentifizieren k\u00f6nnen. [Bitte lesen Sie die Anweisungen] (https://www.home-assistant.io/components/nest/)."
},
"error": {

View File

@ -4,19 +4,21 @@ Support for Nest devices.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/nest/
"""
from concurrent.futures import ThreadPoolExecutor
import logging
import socket
from datetime import datetime, timedelta
import threading
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.const import (
CONF_STRUCTURE, CONF_FILENAME, CONF_BINARY_SENSORS, CONF_SENSORS,
CONF_MONITORED_CONDITIONS,
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP)
from homeassistant.core import callback
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.dispatcher import async_dispatcher_send, \
from homeassistant.helpers.dispatcher import dispatcher_send, \
async_dispatcher_connect
from homeassistant.helpers.entity import Entity
@ -70,24 +72,25 @@ CONFIG_SCHEMA = vol.Schema({
}, extra=vol.ALLOW_EXTRA)
async def async_nest_update_event_broker(hass, nest):
def nest_update_event_broker(hass, nest):
"""
Dispatch SIGNAL_NEST_UPDATE to devices when nest stream API received data.
nest.update_event.wait will block the thread in most of time,
so specific an executor to save default thread pool.
Runs in its own thread.
"""
_LOGGER.debug("listening nest.update_event")
with ThreadPoolExecutor(max_workers=1) as executor:
while True:
await hass.loop.run_in_executor(executor, nest.update_event.wait)
if hass.is_running:
nest.update_event.clear()
_LOGGER.debug("dispatching nest data update")
async_dispatcher_send(hass, SIGNAL_NEST_UPDATE)
else:
_LOGGER.debug("stop listening nest.update_event")
return
while hass.is_running:
nest.update_event.wait()
if not hass.is_running:
break
nest.update_event.clear()
_LOGGER.debug("dispatching nest data update")
dispatcher_send(hass, SIGNAL_NEST_UPDATE)
_LOGGER.debug("stop listening nest.update_event")
async def async_setup(hass, config):
@ -103,7 +106,8 @@ async def async_setup(hass, config):
access_token_cache_file = hass.config.path(filename)
hass.async_add_job(hass.config_entries.flow.async_init(
DOMAIN, source='import', data={
DOMAIN, context={'source': config_entries.SOURCE_IMPORT},
data={
'nest_conf_path': access_token_cache_file,
}
))
@ -165,16 +169,21 @@ async def async_setup_entry(hass, entry):
hass.services.async_register(
DOMAIN, 'set_mode', set_mode, schema=AWAY_SCHEMA)
@callback
def start_up(event):
"""Start Nest update event listener."""
hass.async_add_job(async_nest_update_event_broker, hass, nest)
threading.Thread(
name='Nest update listener',
target=nest_update_event_broker,
args=(hass, nest)
).start()
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, start_up)
@callback
def shut_down(event):
"""Stop Nest update event listener."""
if nest:
nest.update_event.set()
nest.update_event.set()
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, shut_down)

View File

@ -58,6 +58,10 @@ class NestFlowHandler(data_entry_flow.FlowHandler):
"""Initialize the Nest config flow."""
self.flow_impl = None
async def async_step_user(self, user_input=None):
"""Handle a flow initialized by the user."""
return await self.async_step_init(user_input)
async def async_step_init(self, user_input=None):
"""Handle a flow start."""
flows = self.hass.data.get(DATA_FLOW_IMPL, {})

View File

@ -26,7 +26,7 @@ from homeassistant.const import (
from homeassistant.helpers import config_validation as cv
from homeassistant.util import ensure_unique_string
REQUIREMENTS = ['pywebpush==1.6.0', 'PyJWT==1.6.0']
REQUIREMENTS = ['pywebpush==1.6.0']
DEPENDENCIES = ['frontend']

View File

@ -13,7 +13,7 @@ from homeassistant.components.notify import (
from homeassistant.const import CONF_ACCESS_TOKEN
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['Mastodon.py==1.3.0']
REQUIREMENTS = ['Mastodon.py==1.3.1']
_LOGGER = logging.getLogger(__name__)

View File

@ -1,104 +0,0 @@
"""
Telstra API platform for notify component.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/notify.telstra/
"""
import logging
from aiohttp.hdrs import CONTENT_TYPE, AUTHORIZATION
import requests
import voluptuous as vol
from homeassistant.components.notify import (
ATTR_TITLE, PLATFORM_SCHEMA, BaseNotificationService)
from homeassistant.const import CONTENT_TYPE_JSON
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
CONF_CONSUMER_KEY = 'consumer_key'
CONF_CONSUMER_SECRET = 'consumer_secret'
CONF_PHONE_NUMBER = 'phone_number'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_CONSUMER_KEY): cv.string,
vol.Required(CONF_CONSUMER_SECRET): cv.string,
vol.Required(CONF_PHONE_NUMBER): cv.string,
})
def get_service(hass, config, discovery_info=None):
"""Get the Telstra SMS API notification service."""
consumer_key = config.get(CONF_CONSUMER_KEY)
consumer_secret = config.get(CONF_CONSUMER_SECRET)
phone_number = config.get(CONF_PHONE_NUMBER)
if _authenticate(consumer_key, consumer_secret) is False:
_LOGGER.exception("Error obtaining authorization from Telstra API")
return None
return TelstraNotificationService(
consumer_key, consumer_secret, phone_number)
class TelstraNotificationService(BaseNotificationService):
"""Implementation of a notification service for the Telstra SMS API."""
def __init__(self, consumer_key, consumer_secret, phone_number):
"""Initialize the service."""
self._consumer_key = consumer_key
self._consumer_secret = consumer_secret
self._phone_number = phone_number
def send_message(self, message="", **kwargs):
"""Send a message to a user."""
title = kwargs.get(ATTR_TITLE)
# Retrieve authorization first
token_response = _authenticate(
self._consumer_key, self._consumer_secret)
if token_response is False:
_LOGGER.exception("Error obtaining authorization from Telstra API")
return
# Send the SMS
if title:
text = '{} {}'.format(title, message)
else:
text = message
message_data = {
'to': self._phone_number,
'body': text,
}
message_resource = 'https://api.telstra.com/v1/sms/messages'
message_headers = {
CONTENT_TYPE: CONTENT_TYPE_JSON,
AUTHORIZATION: 'Bearer {}'.format(token_response['access_token']),
}
message_response = requests.post(
message_resource, headers=message_headers, json=message_data,
timeout=10)
if message_response.status_code != 202:
_LOGGER.exception("Failed to send SMS. Status code: %d",
message_response.status_code)
def _authenticate(consumer_key, consumer_secret):
"""Authenticate with the Telstra API."""
token_data = {
'client_id': consumer_key,
'client_secret': consumer_secret,
'grant_type': 'client_credentials',
'scope': 'SMS'
}
token_resource = 'https://api.telstra.com/v1/oauth/token'
token_response = requests.get(
token_resource, params=token_data, timeout=10).json()
if 'error' in token_response:
return False
return token_response

View File

@ -2,9 +2,10 @@
from homeassistant.core import callback
from homeassistant.loader import bind_hass
from .const import STEPS, STEP_USER, DOMAIN
from .const import DOMAIN, STEP_USER, STEPS
DEPENDENCIES = ['http']
STORAGE_KEY = DOMAIN
STORAGE_VERSION = 1
@ -21,7 +22,7 @@ def async_is_onboarded(hass):
async def async_setup(hass, config):
"""Set up the onboard component."""
"""Set up the onboarding component."""
store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY)
data = await store.async_load()

View File

@ -3,21 +3,21 @@ import asyncio
import voluptuous as vol
from homeassistant.core import callback
from homeassistant.components.http.view import HomeAssistantView
from homeassistant.components.http.data_validator import RequestDataValidator
from homeassistant.components.http.view import HomeAssistantView
from homeassistant.core import callback
from .const import DOMAIN, STEPS, STEP_USER
from .const import DOMAIN, STEP_USER, STEPS
async def async_setup(hass, data, store):
"""Setup onboarding."""
"""Set up the onboarding view."""
hass.http.register_view(OnboardingView(data, store))
hass.http.register_view(UserOnboardingView(data, store))
class OnboardingView(HomeAssistantView):
"""Returns the onboarding status."""
"""Return the onboarding status."""
requires_auth = False
url = '/api/onboarding'

View File

@ -0,0 +1,182 @@
"""
Support for data from openuv.io.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/openuv/
"""
import logging
from datetime import timedelta
import voluptuous as vol
from homeassistant.const import (
ATTR_ATTRIBUTION, CONF_API_KEY, CONF_BINARY_SENSORS, CONF_ELEVATION,
CONF_LATITUDE, CONF_LONGITUDE, CONF_MONITORED_CONDITIONS,
CONF_SCAN_INTERVAL, CONF_SENSORS)
from homeassistant.helpers import (
aiohttp_client, config_validation as cv, discovery)
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.event import async_track_time_interval
REQUIREMENTS = ['pyopenuv==1.0.1']
_LOGGER = logging.getLogger(__name__)
DOMAIN = 'openuv'
DATA_PROTECTION_WINDOW = 'protection_window'
DATA_UV = 'uv'
DEFAULT_ATTRIBUTION = 'Data provided by OpenUV'
DEFAULT_SCAN_INTERVAL = timedelta(minutes=30)
NOTIFICATION_ID = 'openuv_notification'
NOTIFICATION_TITLE = 'OpenUV Component Setup'
TOPIC_UPDATE = '{0}_data_update'.format(DOMAIN)
TYPE_CURRENT_OZONE_LEVEL = 'current_ozone_level'
TYPE_CURRENT_UV_INDEX = 'current_uv_index'
TYPE_MAX_UV_INDEX = 'max_uv_index'
TYPE_PROTECTION_WINDOW = 'uv_protection_window'
TYPE_SAFE_EXPOSURE_TIME_1 = 'safe_exposure_time_type_1'
TYPE_SAFE_EXPOSURE_TIME_2 = 'safe_exposure_time_type_2'
TYPE_SAFE_EXPOSURE_TIME_3 = 'safe_exposure_time_type_3'
TYPE_SAFE_EXPOSURE_TIME_4 = 'safe_exposure_time_type_4'
TYPE_SAFE_EXPOSURE_TIME_5 = 'safe_exposure_time_type_5'
TYPE_SAFE_EXPOSURE_TIME_6 = 'safe_exposure_time_type_6'
BINARY_SENSORS = {
TYPE_PROTECTION_WINDOW: ('Protection Window', 'mdi:sunglasses')
}
BINARY_SENSOR_SCHEMA = vol.Schema({
vol.Optional(CONF_MONITORED_CONDITIONS, default=list(BINARY_SENSORS)):
vol.All(cv.ensure_list, [vol.In(BINARY_SENSORS)])
})
SENSORS = {
TYPE_CURRENT_OZONE_LEVEL: (
'Current Ozone Level', 'mdi:vector-triangle', 'du'),
TYPE_CURRENT_UV_INDEX: ('Current UV Index', 'mdi:weather-sunny', 'index'),
TYPE_MAX_UV_INDEX: ('Max UV Index', 'mdi:weather-sunny', 'index'),
TYPE_SAFE_EXPOSURE_TIME_1: (
'Skin Type 1 Safe Exposure Time', 'mdi:timer', 'minutes'),
TYPE_SAFE_EXPOSURE_TIME_2: (
'Skin Type 2 Safe Exposure Time', 'mdi:timer', 'minutes'),
TYPE_SAFE_EXPOSURE_TIME_3: (
'Skin Type 3 Safe Exposure Time', 'mdi:timer', 'minutes'),
TYPE_SAFE_EXPOSURE_TIME_4: (
'Skin Type 4 Safe Exposure Time', 'mdi:timer', 'minutes'),
TYPE_SAFE_EXPOSURE_TIME_5: (
'Skin Type 5 Safe Exposure Time', 'mdi:timer', 'minutes'),
TYPE_SAFE_EXPOSURE_TIME_6: (
'Skin Type 6 Safe Exposure Time', 'mdi:timer', 'minutes'),
}
SENSOR_SCHEMA = vol.Schema({
vol.Optional(CONF_MONITORED_CONDITIONS, default=list(SENSORS)):
vol.All(cv.ensure_list, [vol.In(SENSORS)])
})
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Required(CONF_API_KEY): cv.string,
vol.Optional(CONF_ELEVATION): float,
vol.Optional(CONF_LATITUDE): cv.latitude,
vol.Optional(CONF_LONGITUDE): cv.longitude,
vol.Optional(CONF_BINARY_SENSORS, default={}): BINARY_SENSOR_SCHEMA,
vol.Optional(CONF_SENSORS, default={}): SENSOR_SCHEMA,
vol.Optional(CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL):
cv.time_period,
})
}, extra=vol.ALLOW_EXTRA)
async def async_setup(hass, config):
"""Set up the OpenUV component."""
from pyopenuv import Client
from pyopenuv.errors import OpenUvError
conf = config[DOMAIN]
api_key = conf[CONF_API_KEY]
elevation = conf.get(CONF_ELEVATION, hass.config.elevation)
latitude = conf.get(CONF_LATITUDE, hass.config.latitude)
longitude = conf.get(CONF_LONGITUDE, hass.config.longitude)
try:
websession = aiohttp_client.async_get_clientsession(hass)
openuv = OpenUV(
Client(
api_key, latitude, longitude, websession, altitude=elevation),
conf[CONF_BINARY_SENSORS][CONF_MONITORED_CONDITIONS] +
conf[CONF_SENSORS][CONF_MONITORED_CONDITIONS])
await openuv.async_update()
hass.data[DOMAIN] = openuv
except OpenUvError as err:
_LOGGER.error('An error occurred: %s', str(err))
hass.components.persistent_notification.create(
'Error: {0}<br />'
'You will need to restart hass after fixing.'
''.format(err),
title=NOTIFICATION_TITLE,
notification_id=NOTIFICATION_ID)
return False
for component, schema in [
('binary_sensor', conf[CONF_BINARY_SENSORS]),
('sensor', conf[CONF_SENSORS]),
]:
hass.async_create_task(
discovery.async_load_platform(
hass, component, DOMAIN, schema, config))
async def refresh_sensors(event_time):
"""Refresh OpenUV data."""
_LOGGER.debug('Refreshing OpenUV data')
await openuv.async_update()
async_dispatcher_send(hass, TOPIC_UPDATE)
async_track_time_interval(hass, refresh_sensors, conf[CONF_SCAN_INTERVAL])
return True
class OpenUV:
"""Define a generic OpenUV object."""
def __init__(self, client, monitored_conditions):
"""Initialize."""
self._monitored_conditions = monitored_conditions
self.client = client
self.data = {}
async def async_update(self):
"""Update sensor/binary sensor data."""
if TYPE_PROTECTION_WINDOW in self._monitored_conditions:
data = await self.client.uv_protection_window()
self.data[DATA_PROTECTION_WINDOW] = data
if any(c in self._monitored_conditions for c in SENSORS):
data = await self.client.uv_index()
self.data[DATA_UV] = data
class OpenUvEntity(Entity):
"""Define a generic OpenUV entity."""
def __init__(self, openuv):
"""Initialize."""
self._attrs = {ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION}
self._name = None
self.openuv = openuv
@property
def device_state_attributes(self):
"""Return the state attributes."""
return self._attrs
@property
def name(self):
"""Return the name of the entity."""
return self._name

View File

@ -6,10 +6,11 @@ https://home-assistant.io/components/persistent_notification/
"""
import asyncio
import logging
from typing import Awaitable
import voluptuous as vol
from homeassistant.core import callback
from homeassistant.core import callback, HomeAssistant
from homeassistant.exceptions import TemplateError
from homeassistant.loader import bind_hass
from homeassistant.helpers import config_validation as cv
@ -58,7 +59,8 @@ def dismiss(hass, notification_id):
@callback
@bind_hass
def async_create(hass, message, title=None, notification_id=None):
def async_create(hass: HomeAssistant, message: str, title: str = None,
notification_id: str = None) -> None:
"""Generate a notification."""
data = {
key: value for key, value in [
@ -68,7 +70,8 @@ def async_create(hass, message, title=None, notification_id=None):
] if value is not None
}
hass.async_add_job(hass.services.async_call(DOMAIN, SERVICE_CREATE, data))
hass.async_create_task(
hass.services.async_call(DOMAIN, SERVICE_CREATE, data))
@callback
@ -81,7 +84,7 @@ def async_dismiss(hass, notification_id):
@asyncio.coroutine
def async_setup(hass, config):
def async_setup(hass: HomeAssistant, config: dict) -> Awaitable[bool]:
"""Set up the persistent notification component."""
@callback
def create_service(call):

View File

@ -114,6 +114,27 @@ def _drop_index(engine, table_name, index_name):
"critical operation.", index_name, table_name)
def _add_columns(engine, table_name, columns_def):
"""Add columns to a table."""
from sqlalchemy import text
from sqlalchemy.exc import SQLAlchemyError
columns_def = ['ADD COLUMN {}'.format(col_def) for col_def in columns_def]
try:
engine.execute(text("ALTER TABLE {table} {columns_def}".format(
table=table_name,
columns_def=', '.join(columns_def))))
return
except SQLAlchemyError:
pass
for column_def in columns_def:
engine.execute(text("ALTER TABLE {table} {column_def}".format(
table=table_name,
column_def=column_def)))
def _apply_update(engine, new_version, old_version):
"""Perform operations to bring schema up to date."""
if new_version == 1:
@ -146,6 +167,19 @@ def _apply_update(engine, new_version, old_version):
elif new_version == 5:
# Create supporting index for States.event_id foreign key
_create_index(engine, "states", "ix_states_event_id")
elif new_version == 6:
_add_columns(engine, "events", [
'context_id CHARACTER(36)',
'context_user_id CHARACTER(36)',
])
_create_index(engine, "events", "ix_events_context_id")
_create_index(engine, "events", "ix_events_context_user_id")
_add_columns(engine, "states", [
'context_id CHARACTER(36)',
'context_user_id CHARACTER(36)',
])
_create_index(engine, "states", "ix_states_context_id")
_create_index(engine, "states", "ix_states_context_user_id")
else:
raise ValueError("No schema migration defined for version {}"
.format(new_version))

View File

@ -9,14 +9,15 @@ from sqlalchemy import (
from sqlalchemy.ext.declarative import declarative_base
import homeassistant.util.dt as dt_util
from homeassistant.core import Event, EventOrigin, State, split_entity_id
from homeassistant.core import (
Context, Event, EventOrigin, State, split_entity_id)
from homeassistant.remote import JSONEncoder
# SQLAlchemy Schema
# pylint: disable=invalid-name
Base = declarative_base()
SCHEMA_VERSION = 5
SCHEMA_VERSION = 6
_LOGGER = logging.getLogger(__name__)
@ -31,6 +32,8 @@ class Events(Base): # type: ignore
origin = Column(String(32))
time_fired = Column(DateTime(timezone=True), index=True)
created = Column(DateTime(timezone=True), default=datetime.utcnow)
context_id = Column(String(36), index=True)
context_user_id = Column(String(36), index=True)
@staticmethod
def from_event(event):
@ -38,16 +41,23 @@ class Events(Base): # type: ignore
return Events(event_type=event.event_type,
event_data=json.dumps(event.data, cls=JSONEncoder),
origin=str(event.origin),
time_fired=event.time_fired)
time_fired=event.time_fired,
context_id=event.context.id,
context_user_id=event.context.user_id)
def to_native(self):
"""Convert to a natve HA Event."""
context = Context(
id=self.context_id,
user_id=self.context_user_id
)
try:
return Event(
self.event_type,
json.loads(self.event_data),
EventOrigin(self.origin),
_process_timestamp(self.time_fired)
_process_timestamp(self.time_fired),
context=context,
)
except ValueError:
# When json.loads fails
@ -69,6 +79,8 @@ class States(Base): # type: ignore
last_updated = Column(DateTime(timezone=True), default=datetime.utcnow,
index=True)
created = Column(DateTime(timezone=True), default=datetime.utcnow)
context_id = Column(String(36), index=True)
context_user_id = Column(String(36), index=True)
__table_args__ = (
# Used for fetching the state of entities at a specific time
@ -82,7 +94,11 @@ class States(Base): # type: ignore
entity_id = event.data['entity_id']
state = event.data.get('new_state')
dbstate = States(entity_id=entity_id)
dbstate = States(
entity_id=entity_id,
context_id=event.context.id,
context_user_id=event.context.user_id,
)
# State got deleted
if state is None:
@ -103,12 +119,17 @@ class States(Base): # type: ignore
def to_native(self):
"""Convert to an HA state object."""
context = Context(
id=self.context_id,
user_id=self.context_user_id
)
try:
return State(
self.entity_id, self.state,
json.loads(self.attributes),
_process_timestamp(self.last_changed),
_process_timestamp(self.last_updated)
_process_timestamp(self.last_updated),
context=context,
)
except ValueError:
# When json.loads fails

View File

@ -0,0 +1,6 @@
{
"state": {
"first_quarter": "\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u0623\u0648\u0644",
"full_moon": "\u0627\u0644\u0642\u0645\u0631 \u0627\u0644\u0643\u0627\u0645\u0644"
}
}

View File

@ -0,0 +1,12 @@
{
"state": {
"first_quarter": "Quart creixent",
"full_moon": "Lluna plena",
"last_quarter": "Quart minvant",
"new_moon": "Lluna nova",
"waning_crescent": "Lluna vella minvant",
"waning_gibbous": "Gibosa minvant",
"waxing_crescent": "Lluna nova visible",
"waxing_gibbous": "Gibosa creixent"
}
}

View File

@ -0,0 +1,12 @@
{
"state": {
"first_quarter": "Erstes Viertel",
"full_moon": "Vollmond",
"last_quarter": "Letztes Viertel",
"new_moon": "Neumond",
"waning_crescent": "Abnehmende Sichel",
"waning_gibbous": "Drittes Viertel",
"waxing_crescent": " Zunehmende Sichel",
"waxing_gibbous": "Zweites Viertel"
}
}

View File

@ -0,0 +1,12 @@
{
"state": {
"first_quarter": "First quarter",
"full_moon": "Full moon",
"last_quarter": "Last quarter",
"new_moon": "New moon",
"waning_crescent": "Waning crescent",
"waning_gibbous": "Waning gibbous",
"waxing_crescent": "Waxing crescent",
"waxing_gibbous": "Waxing gibbous"
}
}

View File

@ -0,0 +1,12 @@
{
"state": {
"first_quarter": "Cuarto creciente",
"full_moon": "Luna llena",
"last_quarter": "Cuarto menguante",
"new_moon": "Luna nueva",
"waning_crescent": "Luna menguante",
"waning_gibbous": "Luna menguante gibosa",
"waxing_crescent": "Luna creciente",
"waxing_gibbous": "Luna creciente gibosa"
}
}

View File

@ -0,0 +1,12 @@
{
"state": {
"first_quarter": "Premier quartier",
"full_moon": "Pleine lune",
"last_quarter": "Dernier quartier",
"new_moon": "Nouvelle lune",
"waning_crescent": "Dernier croissant",
"waning_gibbous": "Gibbeuse d\u00e9croissante",
"waxing_crescent": "Premier croissant",
"waxing_gibbous": "Gibbeuse croissante"
}
}

View File

@ -0,0 +1,12 @@
{
"state": {
"first_quarter": "\ubc18\ub2ec(\ucc28\uc624\ub974\ub294)",
"full_moon": "\ubcf4\ub984\ub2ec",
"last_quarter": "\ubc18\ub2ec(\uc904\uc5b4\ub4dc\ub294)",
"new_moon": "\uc0ad\uc6d4",
"waning_crescent": "\uadf8\ubbd0\ub2ec",
"waning_gibbous": "\ud558\ud604\ub2ec",
"waxing_crescent": "\ucd08\uc2b9\ub2ec",
"waxing_gibbous": "\uc0c1\ud604\ub2ec"
}
}

View File

@ -0,0 +1,12 @@
{
"state": {
"first_quarter": "Eerste kwartier",
"full_moon": "Volle maan",
"last_quarter": "Laatste kwartier",
"new_moon": "Nieuwe maan",
"waning_crescent": "Krimpende, sikkelvormige maan",
"waning_gibbous": "Krimpende, vooruitspringende maan",
"waxing_crescent": "Wassende, sikkelvormige maan",
"waxing_gibbous": "Wassende, vooruitspringende maan"
}
}

View File

@ -0,0 +1,12 @@
{
"state": {
"first_quarter": "F\u00f8rste kvartdel",
"full_moon": "Fullm\u00e5ne",
"last_quarter": "Siste kvartdel",
"new_moon": "Nym\u00e5ne",
"waning_crescent": "Minkende halvm\u00e5ne",
"waning_gibbous": "Minkende trekvartm\u00e5ne",
"waxing_crescent": "Voksende halvm\u00e5ne",
"waxing_gibbous": "Voksende trekvartm\u00e5ne"
}
}

View File

@ -0,0 +1,12 @@
{
"state": {
"first_quarter": "\u041f\u0435\u0440\u0432\u0430\u044f \u0447\u0435\u0442\u0432\u0435\u0440\u0442\u044c",
"full_moon": "\u041f\u043e\u043b\u043d\u043e\u043b\u0443\u043d\u0438\u0435",
"last_quarter": "\u041f\u043e\u0441\u043b\u0435\u0434\u043d\u044f\u044f \u0447\u0435\u0442\u0432\u0435\u0440\u0442\u044c",
"new_moon": "\u041d\u043e\u0432\u043e\u043b\u0443\u043d\u0438\u0435",
"waning_crescent": "\u0421\u0442\u0430\u0440\u0430\u044f \u043b\u0443\u043d\u0430",
"waning_gibbous": "\u0423\u0431\u044b\u0432\u0430\u044e\u0449\u0430\u044f \u043b\u0443\u043d\u0430",
"waxing_crescent": "\u041c\u043e\u043b\u043e\u0434\u0430\u044f \u043b\u0443\u043d\u0430",
"waxing_gibbous": "\u041f\u0440\u0438\u0431\u044b\u0432\u0430\u044e\u0449\u0430\u044f \u043b\u0443\u043d\u0430"
}
}

View File

@ -0,0 +1,12 @@
{
"state": {
"first_quarter": "Prvi krajec",
"full_moon": "Polna luna",
"last_quarter": "Zadnji krajec",
"new_moon": "Mlaj",
"waning_crescent": "Zadnji izbo\u010dec",
"waning_gibbous": "Zadnji srpec",
"waxing_crescent": " Prvi izbo\u010dec",
"waxing_gibbous": "Prvi srpec"
}
}

View File

@ -0,0 +1,12 @@
{
"state": {
"first_quarter": "\u4e0a\u5f26\u6708",
"full_moon": "\u6ee1\u6708",
"last_quarter": "\u4e0b\u5f26\u6708",
"new_moon": "\u65b0\u6708",
"waning_crescent": "\u6b8b\u6708",
"waning_gibbous": "\u4e8f\u51f8\u6708",
"waxing_crescent": "\u5ce8\u7709\u6708",
"waxing_gibbous": "\u76c8\u51f8\u6708"
}
}

View File

@ -0,0 +1,12 @@
{
"state": {
"first_quarter": "\u4e0a\u5f26\u6708",
"full_moon": "\u6eff\u6708",
"last_quarter": "\u4e0b\u5f26\u6708",
"new_moon": "\u65b0\u6708",
"waning_crescent": "\u6b98\u6708",
"waning_gibbous": "\u8667\u51f8\u6708",
"waxing_crescent": "\u86fe\u7709\u6708",
"waxing_gibbous": "\u76c8\u51f8\u6708"
}
}

View File

@ -22,7 +22,6 @@ _LOGGER = logging.getLogger(__name__)
ATTR_CLOSE = 'close'
ATTR_HIGH = 'high'
ATTR_LOW = 'low'
ATTR_VOLUME = 'volume'
CONF_ATTRIBUTION = "Stock market information provided by Alpha Vantage"
CONF_FOREIGN_EXCHANGE = 'foreign_exchange'
@ -148,7 +147,6 @@ class AlphaVantageSensor(Entity):
ATTR_CLOSE: self.values['4. close'],
ATTR_HIGH: self.values['2. high'],
ATTR_LOW: self.values['3. low'],
ATTR_VOLUME: self.values['5. volume'],
}
@property

View File

@ -0,0 +1,107 @@
"""
Support for Enphase Envoy solar energy monitor.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/sensor.enphase_envoy/
"""
import logging
import voluptuous as vol
from homeassistant.helpers.entity import Entity
from homeassistant.components.sensor import PLATFORM_SCHEMA
import homeassistant.helpers.config_validation as cv
from homeassistant.const import (CONF_IP_ADDRESS, CONF_MONITORED_CONDITIONS)
REQUIREMENTS = ['envoy_reader==0.1']
_LOGGER = logging.getLogger(__name__)
SENSORS = {
"production": ("Envoy Current Energy Production", 'W'),
"daily_production": ("Envoy Today's Energy Production", "Wh"),
"7_days_production": ("Envoy Last Seven Days Energy Production", "Wh"),
"lifetime_production": ("Envoy Lifetime Energy Production", "Wh"),
"consumption": ("Envoy Current Energy Consumption", "W"),
"daily_consumption": ("Envoy Today's Energy Consumption", "Wh"),
"7_days_consumption": ("Envoy Last Seven Days Energy Consumption", "Wh"),
"lifetime_consumption": ("Envoy Lifetime Energy Consumption", "Wh")
}
ICON = 'mdi:flash'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_IP_ADDRESS): cv.string,
vol.Optional(CONF_MONITORED_CONDITIONS, default=list(SENSORS)):
vol.All(cv.ensure_list, [vol.In(list(SENSORS))])})
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Enphase Envoy sensor."""
ip_address = config[CONF_IP_ADDRESS]
monitored_conditions = config[CONF_MONITORED_CONDITIONS]
# Iterate through the list of sensors
for condition in monitored_conditions:
add_devices([Envoy(ip_address, condition, SENSORS[condition][0],
SENSORS[condition][1])], True)
class Envoy(Entity):
"""Implementation of the Enphase Envoy sensors."""
def __init__(self, ip_address, sensor_type, name, unit):
"""Initialize the sensor."""
self._ip_address = ip_address
self._name = name
self._unit_of_measurement = unit
self._type = sensor_type
self._state = None
@property
def name(self):
"""Return the name of the sensor."""
return self._name
@property
def state(self):
"""Return the state of the sensor."""
return self._state
@property
def unit_of_measurement(self):
"""Return the unit of measurement of this entity, if any."""
return self._unit_of_measurement
@property
def icon(self):
"""Icon to use in the frontend, if any."""
return ICON
def update(self):
"""Get the energy production data from the Enphase Envoy."""
import envoy_reader
if self._type == "production":
self._state = int(envoy_reader.production(self._ip_address))
elif self._type == "daily_production":
self._state = int(envoy_reader.daily_production(self._ip_address))
elif self._type == "7_days_production":
self._state = int(envoy_reader.seven_days_production(
self._ip_address))
elif self._type == "lifetime_production":
self._state = int(envoy_reader.lifetime_production(
self._ip_address))
elif self._type == "consumption":
self._state = int(envoy_reader.consumption(self._ip_address))
elif self._type == "daily_consumption":
self._state = int(envoy_reader.daily_consumption(
self._ip_address))
elif self._type == "7_days_consumption":
self._state = int(envoy_reader.seven_days_consumption(
self._ip_address))
elif self._type == "lifetime_consumption":
self._state = int(envoy_reader.lifetime_consumption(
self._ip_address))

View File

@ -164,7 +164,7 @@ class IrishRailTransportData:
ATTR_TRAIN_TYPE: train.get('type')}
self.info.append(train_data)
if not self.info or not self.info:
if not self.info:
self.info = self._empty_train_data()
def _empty_train_data(self):

Some files were not shown because too many files have changed in this diff Show More