mirror of
https://github.com/home-assistant/core.git
synced 2026-01-08 00:28:31 +01:00
Compare commits
104 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9e21765173 | ||
|
|
c0830f1c20 | ||
|
|
985f96662e | ||
|
|
e0229b799d | ||
|
|
69934a9598 | ||
|
|
f24773933c | ||
|
|
e17e080639 | ||
|
|
055e35b297 | ||
|
|
81604a9326 | ||
|
|
a0e9f9f218 | ||
|
|
0ab3e7a92a | ||
|
|
9512bb9587 | ||
|
|
da916d7b27 | ||
|
|
b370b6a4e4 | ||
|
|
1911168855 | ||
|
|
f98629b895 | ||
|
|
dc01b17260 | ||
|
|
ef61c0c3a4 | ||
|
|
664eae72d1 | ||
|
|
86658f310d | ||
|
|
a29f867908 | ||
|
|
28de2d6f75 | ||
|
|
37d98474d5 | ||
|
|
5116f02290 | ||
|
|
2233d7ca98 | ||
|
|
f58425dd3c | ||
|
|
39d19f2183 | ||
|
|
99c4c65f69 | ||
|
|
61901496ec | ||
|
|
0ab65f1ac5 | ||
|
|
debdc707e9 | ||
|
|
fcc918a146 | ||
|
|
b6bc0097b8 | ||
|
|
d556edae31 | ||
|
|
1fb2ea70c2 | ||
|
|
4cbcb4c3a2 | ||
|
|
d071df0dec | ||
|
|
f09f153014 | ||
|
|
1d8678c431 | ||
|
|
51c30980df | ||
|
|
cb20c9b1ea | ||
|
|
a7db2ebbe1 | ||
|
|
61721478f3 | ||
|
|
47fa928425 | ||
|
|
10a7accd00 | ||
|
|
527585ff9c | ||
|
|
2f15a40e97 | ||
|
|
ccef9a3e43 | ||
|
|
479dfd1710 | ||
|
|
34ad4bd32d | ||
|
|
6031801206 | ||
|
|
9cfe0db3c8 | ||
|
|
8ef2cfa364 | ||
|
|
12e69202f8 | ||
|
|
e4b2ae29bd | ||
|
|
ac4674fdb0 | ||
|
|
f86702e8ab | ||
|
|
9a84f8b763 | ||
|
|
6a32b9bf87 | ||
|
|
b152becbe0 | ||
|
|
c41aa12d1d | ||
|
|
8a81ee3b4f | ||
|
|
5e1836f3a2 | ||
|
|
9ea3be4dc1 | ||
|
|
bce47eb9a4 | ||
|
|
018bd8544c | ||
|
|
bfb9f2a00b | ||
|
|
c7a8f1143c | ||
|
|
dbe44c076e | ||
|
|
3246b49a45 | ||
|
|
0c7d46927e | ||
|
|
f6935b5d27 | ||
|
|
91e8680fc5 | ||
|
|
6f2000f5e2 | ||
|
|
ee180c51cf | ||
|
|
b63312ff2e | ||
|
|
59f8a73676 | ||
|
|
affd4e7df3 | ||
|
|
38928c4c0e | ||
|
|
48af5116b3 | ||
|
|
eb5f6efb43 | ||
|
|
7972d6a0c6 | ||
|
|
bdea9e1333 | ||
|
|
2f8d66ef2b | ||
|
|
589b23b7e2 | ||
|
|
2e5131bb21 | ||
|
|
2ff5b4ce95 | ||
|
|
f8a478946e | ||
|
|
623f6c841b | ||
|
|
0b6f2f5b91 | ||
|
|
3445dc1f00 | ||
|
|
a11c2a0bd8 | ||
|
|
95da41aa15 | ||
|
|
27401f4975 | ||
|
|
d902a9f279 | ||
|
|
03847e6c41 | ||
|
|
a4f9602405 | ||
|
|
5f214ffa98 | ||
|
|
8ee3b535ef | ||
|
|
951372491c | ||
|
|
eeb79476de | ||
|
|
1b2d0e7a6f | ||
|
|
3208ad27ac | ||
|
|
cf87b76b0c |
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
import asyncio
|
||||
import logging
|
||||
from collections import OrderedDict
|
||||
from typing import List, Awaitable
|
||||
|
||||
from homeassistant import data_entry_flow
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.core import callback, HomeAssistant
|
||||
|
||||
from . import models
|
||||
from . import auth_store
|
||||
@@ -13,7 +14,9 @@ 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:
|
||||
@@ -208,7 +211,7 @@ class AuthManager:
|
||||
|
||||
return tkn
|
||||
|
||||
async def _async_create_login_flow(self, handler, *, source, data):
|
||||
async def _async_create_login_flow(self, handler, *, context, data):
|
||||
"""Create a login flow."""
|
||||
auth_provider = self._providers[handler]
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -68,8 +68,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 +95,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 +142,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)
|
||||
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 +170,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 +201,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')
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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
|
||||
|
||||
103
homeassistant/components/binary_sensor/openuv.py
Normal file
103
homeassistant/components/binary_sensor/openuv.py
Normal 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'])),
|
||||
})
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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__)
|
||||
|
||||
|
||||
@@ -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['source'] != config_entries.SOURCE_USER])
|
||||
|
||||
|
||||
class ConfigManagerFlowResourceView(FlowManagerResourceView):
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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__)
|
||||
|
||||
|
||||
@@ -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]
|
||||
|
||||
87
homeassistant/components/device_tracker/ritassist.py
Normal file
87
homeassistant/components/device_tracker/ritassist.py
Normal 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')
|
||||
@@ -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__)
|
||||
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -85,6 +85,7 @@ SERVICE_HANDLERS = {
|
||||
'volumio': ('media_player', 'volumio'),
|
||||
'nanoleaf_aurora': ('light', 'nanoleaf_aurora'),
|
||||
'freebox': ('device_tracker', 'freebox'),
|
||||
'dlna_dmr': ('media_player', 'dlna_dmr'),
|
||||
}
|
||||
|
||||
OPTIONAL_SERVICE_HANDLERS = {
|
||||
@@ -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
|
||||
|
||||
@@ -16,7 +16,7 @@ from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.discovery import async_load_platform
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
|
||||
REQUIREMENTS = ['pyenvisalink==2.2']
|
||||
REQUIREMENTS = ['pyenvisalink==2.3']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -111,20 +111,24 @@ def async_setup(hass, config):
|
||||
def login_fail_callback(data):
|
||||
"""Handle when the evl rejects our login."""
|
||||
_LOGGER.error("The Envisalink rejected your credentials")
|
||||
sync_connect.set_result(False)
|
||||
if not sync_connect.done():
|
||||
sync_connect.set_result(False)
|
||||
|
||||
@callback
|
||||
def connection_fail_callback(data):
|
||||
"""Network failure callback."""
|
||||
_LOGGER.error("Could not establish a connection with the Envisalink")
|
||||
sync_connect.set_result(False)
|
||||
if not sync_connect.done():
|
||||
sync_connect.set_result(False)
|
||||
|
||||
@callback
|
||||
def connection_success_callback(data):
|
||||
"""Handle a successful connection."""
|
||||
_LOGGER.info("Established a connection with the Envisalink")
|
||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_envisalink)
|
||||
sync_connect.set_result(True)
|
||||
if not sync_connect.done():
|
||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP,
|
||||
stop_envisalink)
|
||||
sync_connect.set_result(True)
|
||||
|
||||
@callback
|
||||
def zones_updated_callback(data):
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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==20180813.0']
|
||||
|
||||
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."""
|
||||
|
||||
|
||||
@@ -175,10 +175,13 @@ def async_setup(hass, config):
|
||||
if data is None:
|
||||
data = {}
|
||||
|
||||
refresh_token = None
|
||||
if 'hassio_user' in data:
|
||||
user = yield from hass.auth.async_get_user(data['hassio_user'])
|
||||
refresh_token = list(user.refresh_tokens.values())[0]
|
||||
else:
|
||||
if user:
|
||||
refresh_token = list(user.refresh_tokens.values())[0]
|
||||
|
||||
if refresh_token is None:
|
||||
user = yield from hass.auth.async_create_system_user('Hass.io')
|
||||
refresh_token = yield from hass.auth.async_create_refresh_token(user)
|
||||
data['hassio_user'] = user.id
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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 = {}
|
||||
|
||||
@@ -66,8 +66,8 @@ 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]),
|
||||
@@ -96,8 +96,8 @@ 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]
|
||||
|
||||
@@ -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],
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
@@ -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__)
|
||||
|
||||
|
||||
400
homeassistant/components/media_player/dlna_dmr.py
Normal file
400
homeassistant/components/media_player/dlna_dmr.py
Normal 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.2',
|
||||
]
|
||||
|
||||
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['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
|
||||
@@ -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."""
|
||||
|
||||
@@ -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__)
|
||||
|
||||
|
||||
157
homeassistant/components/media_player/pjlink.py
Normal file
157
homeassistant/components/media_player/pjlink.py
Normal 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)
|
||||
@@ -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__)
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ from datetime import datetime, timedelta
|
||||
|
||||
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,
|
||||
@@ -103,7 +104,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,
|
||||
}
|
||||
))
|
||||
|
||||
@@ -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, {})
|
||||
|
||||
@@ -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__)
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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'
|
||||
|
||||
182
homeassistant/components/openuv.py
Normal file
182
homeassistant/components/openuv.py
Normal 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
|
||||
@@ -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):
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
107
homeassistant/components/sensor/enphase_envoy.py
Normal file
107
homeassistant/components/sensor/enphase_envoy.py
Normal 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))
|
||||
@@ -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):
|
||||
|
||||
@@ -13,7 +13,7 @@ from homeassistant.const import CONF_API_KEY
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
REQUIREMENTS = ['pylast==2.3.0']
|
||||
REQUIREMENTS = ['pylast==2.4.0']
|
||||
|
||||
ATTR_LAST_PLAYED = 'last_played'
|
||||
ATTR_PLAY_COUNT = 'play_count'
|
||||
|
||||
@@ -124,7 +124,6 @@ class MinMaxSensor(Entity):
|
||||
self.states = {}
|
||||
|
||||
@callback
|
||||
# pylint: disable=invalid-name
|
||||
def async_min_max_sensor_state_listener(entity, old_state, new_state):
|
||||
"""Handle the sensor state changes."""
|
||||
if new_state.state is None or new_state.state in STATE_UNKNOWN:
|
||||
|
||||
@@ -13,22 +13,27 @@ import voluptuous as vol
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.sensor import PLATFORM_SCHEMA
|
||||
from homeassistant.const import (
|
||||
CONF_NAME, CONF_LATITUDE, CONF_LONGITUDE, CONF_RADIUS,
|
||||
ATTR_ATTRIBUTION, ATTR_LATITUDE, ATTR_LONGITUDE,
|
||||
LENGTH_KILOMETERS, LENGTH_METERS)
|
||||
CONF_NAME, CONF_LATITUDE, CONF_LONGITUDE,
|
||||
CONF_RADIUS, ATTR_ATTRIBUTION, ATTR_LATITUDE,
|
||||
ATTR_LONGITUDE, LENGTH_KILOMETERS, LENGTH_METERS)
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.util import distance as util_distance
|
||||
from homeassistant.util import location as util_location
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_ALTITUDE = 'altitude'
|
||||
|
||||
ATTR_CALLSIGN = 'callsign'
|
||||
ATTR_ALTITUDE = 'altitude'
|
||||
ATTR_ON_GROUND = 'on_ground'
|
||||
ATTR_SENSOR = 'sensor'
|
||||
ATTR_STATES = 'states'
|
||||
|
||||
DOMAIN = 'opensky'
|
||||
|
||||
DEFAULT_ALTITUDE = 0
|
||||
|
||||
EVENT_OPENSKY_ENTRY = '{}_entry'.format(DOMAIN)
|
||||
EVENT_OPENSKY_EXIT = '{}_exit'.format(DOMAIN)
|
||||
SCAN_INTERVAL = timedelta(seconds=12) # opensky public limit is 10 seconds
|
||||
@@ -38,7 +43,7 @@ OPENSKY_ATTRIBUTION = "Information provided by the OpenSky Network "\
|
||||
OPENSKY_API_URL = 'https://opensky-network.org/api/states/all'
|
||||
OPENSKY_API_FIELDS = [
|
||||
'icao24', ATTR_CALLSIGN, 'origin_country', 'time_position',
|
||||
'time_velocity', ATTR_LONGITUDE, ATTR_LATITUDE, 'altitude',
|
||||
'time_velocity', ATTR_LONGITUDE, ATTR_LATITUDE, ATTR_ALTITUDE,
|
||||
ATTR_ON_GROUND, 'velocity', 'heading', 'vertical_rate', 'sensors']
|
||||
|
||||
|
||||
@@ -46,7 +51,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_RADIUS): vol.Coerce(float),
|
||||
vol.Optional(CONF_NAME): cv.string,
|
||||
vol.Inclusive(CONF_LATITUDE, 'coordinates'): cv.latitude,
|
||||
vol.Inclusive(CONF_LONGITUDE, 'coordinates'): cv.longitude
|
||||
vol.Inclusive(CONF_LONGITUDE, 'coordinates'): cv.longitude,
|
||||
vol.Optional(CONF_ALTITUDE, default=DEFAULT_ALTITUDE): vol.Coerce(float)
|
||||
})
|
||||
|
||||
|
||||
@@ -56,19 +62,20 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
longitude = config.get(CONF_LONGITUDE, hass.config.longitude)
|
||||
add_devices([OpenSkySensor(
|
||||
hass, config.get(CONF_NAME, DOMAIN), latitude, longitude,
|
||||
config.get(CONF_RADIUS))], True)
|
||||
config.get(CONF_RADIUS), config.get(CONF_ALTITUDE))], True)
|
||||
|
||||
|
||||
class OpenSkySensor(Entity):
|
||||
"""Open Sky Network Sensor."""
|
||||
|
||||
def __init__(self, hass, name, latitude, longitude, radius):
|
||||
def __init__(self, hass, name, latitude, longitude, radius, altitude):
|
||||
"""Initialize the sensor."""
|
||||
self._session = requests.Session()
|
||||
self._latitude = latitude
|
||||
self._longitude = longitude
|
||||
self._radius = util_distance.convert(
|
||||
radius, LENGTH_KILOMETERS, LENGTH_METERS)
|
||||
self._altitude = altitude
|
||||
self._state = 0
|
||||
self._hass = hass
|
||||
self._name = name
|
||||
@@ -84,11 +91,18 @@ class OpenSkySensor(Entity):
|
||||
"""Return the state of the sensor."""
|
||||
return self._state
|
||||
|
||||
def _handle_boundary(self, callsigns, event):
|
||||
def _handle_boundary(self, flights, event, metadata):
|
||||
"""Handle flights crossing region boundary."""
|
||||
for callsign in callsigns:
|
||||
for flight in flights:
|
||||
if flight in metadata:
|
||||
altitude = metadata[flight].get(ATTR_ALTITUDE)
|
||||
else:
|
||||
# Assume Flight has landed if missing.
|
||||
altitude = 0
|
||||
|
||||
data = {
|
||||
ATTR_CALLSIGN: callsign,
|
||||
ATTR_CALLSIGN: flight,
|
||||
ATTR_ALTITUDE: altitude,
|
||||
ATTR_SENSOR: self._name,
|
||||
}
|
||||
self._hass.bus.fire(event, data)
|
||||
@@ -96,30 +110,37 @@ class OpenSkySensor(Entity):
|
||||
def update(self):
|
||||
"""Update device state."""
|
||||
currently_tracked = set()
|
||||
flight_metadata = {}
|
||||
states = self._session.get(OPENSKY_API_URL).json().get(ATTR_STATES)
|
||||
for state in states:
|
||||
data = dict(zip(OPENSKY_API_FIELDS, state))
|
||||
flight = dict(zip(OPENSKY_API_FIELDS, state))
|
||||
callsign = flight[ATTR_CALLSIGN].strip()
|
||||
if callsign != '':
|
||||
flight_metadata[callsign] = flight
|
||||
else:
|
||||
continue
|
||||
missing_location = (
|
||||
data.get(ATTR_LONGITUDE) is None or
|
||||
data.get(ATTR_LATITUDE) is None)
|
||||
flight.get(ATTR_LONGITUDE) is None or
|
||||
flight.get(ATTR_LATITUDE) is None)
|
||||
if missing_location:
|
||||
continue
|
||||
if data.get(ATTR_ON_GROUND):
|
||||
if flight.get(ATTR_ON_GROUND):
|
||||
continue
|
||||
distance = util_location.distance(
|
||||
self._latitude, self._longitude,
|
||||
data.get(ATTR_LATITUDE), data.get(ATTR_LONGITUDE))
|
||||
flight.get(ATTR_LATITUDE), flight.get(ATTR_LONGITUDE))
|
||||
if distance is None or distance > self._radius:
|
||||
continue
|
||||
callsign = data[ATTR_CALLSIGN].strip()
|
||||
if callsign == '':
|
||||
altitude = flight.get(ATTR_ALTITUDE)
|
||||
if altitude > self._altitude and self._altitude != 0:
|
||||
continue
|
||||
currently_tracked.add(callsign)
|
||||
if self._previously_tracked is not None:
|
||||
entries = currently_tracked - self._previously_tracked
|
||||
exits = self._previously_tracked - currently_tracked
|
||||
self._handle_boundary(entries, EVENT_OPENSKY_ENTRY)
|
||||
self._handle_boundary(exits, EVENT_OPENSKY_EXIT)
|
||||
self._handle_boundary(entries, EVENT_OPENSKY_ENTRY,
|
||||
flight_metadata)
|
||||
self._handle_boundary(exits, EVENT_OPENSKY_EXIT, flight_metadata)
|
||||
self._state = len(currently_tracked)
|
||||
self._previously_tracked = currently_tracked
|
||||
|
||||
|
||||
121
homeassistant/components/sensor/openuv.py
Normal file
121
homeassistant/components/sensor/openuv.py
Normal file
@@ -0,0 +1,121 @@
|
||||
"""
|
||||
This platform provides sensors for OpenUV data.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/sensor.openuv/
|
||||
"""
|
||||
import logging
|
||||
|
||||
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 (
|
||||
DATA_UV, DOMAIN, SENSORS, TOPIC_UPDATE, TYPE_CURRENT_OZONE_LEVEL,
|
||||
TYPE_CURRENT_UV_INDEX, TYPE_MAX_UV_INDEX, TYPE_SAFE_EXPOSURE_TIME_1,
|
||||
TYPE_SAFE_EXPOSURE_TIME_2, TYPE_SAFE_EXPOSURE_TIME_3,
|
||||
TYPE_SAFE_EXPOSURE_TIME_4, TYPE_SAFE_EXPOSURE_TIME_5,
|
||||
TYPE_SAFE_EXPOSURE_TIME_6, OpenUvEntity)
|
||||
from homeassistant.util.dt import as_local, parse_datetime
|
||||
|
||||
DEPENDENCIES = ['openuv']
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTR_MAX_UV_TIME = 'time'
|
||||
|
||||
EXPOSURE_TYPE_MAP = {
|
||||
TYPE_SAFE_EXPOSURE_TIME_1: 'st1',
|
||||
TYPE_SAFE_EXPOSURE_TIME_2: 'st2',
|
||||
TYPE_SAFE_EXPOSURE_TIME_3: 'st3',
|
||||
TYPE_SAFE_EXPOSURE_TIME_4: 'st4',
|
||||
TYPE_SAFE_EXPOSURE_TIME_5: 'st5',
|
||||
TYPE_SAFE_EXPOSURE_TIME_6: 'st6'
|
||||
}
|
||||
|
||||
|
||||
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]
|
||||
|
||||
sensors = []
|
||||
for sensor_type in discovery_info[CONF_MONITORED_CONDITIONS]:
|
||||
name, icon, unit = SENSORS[sensor_type]
|
||||
sensors.append(OpenUvSensor(openuv, sensor_type, name, icon, unit))
|
||||
|
||||
async_add_devices(sensors, True)
|
||||
|
||||
|
||||
class OpenUvSensor(OpenUvEntity):
|
||||
"""Define a binary sensor for OpenUV."""
|
||||
|
||||
def __init__(self, openuv, sensor_type, name, icon, unit):
|
||||
"""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
|
||||
self._unit = unit
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
"""Return the icon."""
|
||||
return self._icon
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Disable polling."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the status of the sensor."""
|
||||
return self._state
|
||||
|
||||
@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)
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
"""Return the unit the value is expressed in."""
|
||||
return self._unit
|
||||
|
||||
@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_UV]['result']
|
||||
if self._sensor_type == TYPE_CURRENT_OZONE_LEVEL:
|
||||
self._state = data['ozone']
|
||||
elif self._sensor_type == TYPE_CURRENT_UV_INDEX:
|
||||
self._state = data['uv']
|
||||
elif self._sensor_type == TYPE_MAX_UV_INDEX:
|
||||
self._state = data['uv_max']
|
||||
self._attrs.update({
|
||||
ATTR_MAX_UV_TIME: as_local(
|
||||
parse_datetime(data['uv_max_time']))
|
||||
})
|
||||
elif self._sensor_type in (TYPE_SAFE_EXPOSURE_TIME_1,
|
||||
TYPE_SAFE_EXPOSURE_TIME_2,
|
||||
TYPE_SAFE_EXPOSURE_TIME_3,
|
||||
TYPE_SAFE_EXPOSURE_TIME_4,
|
||||
TYPE_SAFE_EXPOSURE_TIME_5,
|
||||
TYPE_SAFE_EXPOSURE_TIME_6):
|
||||
self._state = data['safe_exposure_time'][EXPOSURE_TYPE_MAP[
|
||||
self._sensor_type]]
|
||||
202
homeassistant/components/sensor/rmvtransport.py
Normal file
202
homeassistant/components/sensor/rmvtransport.py
Normal file
@@ -0,0 +1,202 @@
|
||||
"""
|
||||
Support for real-time departure information for Rhein-Main public transport.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/sensor.rmvtransport/
|
||||
"""
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.components.sensor import PLATFORM_SCHEMA
|
||||
from homeassistant.const import (CONF_NAME, ATTR_ATTRIBUTION)
|
||||
|
||||
REQUIREMENTS = ['PyRMVtransport==0.0.7']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_NEXT_DEPARTURE = 'next_departure'
|
||||
|
||||
CONF_STATION = 'station'
|
||||
CONF_DESTINATIONS = 'destinations'
|
||||
CONF_DIRECTIONS = 'directions'
|
||||
CONF_LINES = 'lines'
|
||||
CONF_PRODUCTS = 'products'
|
||||
CONF_TIME_OFFSET = 'time_offset'
|
||||
CONF_MAX_JOURNEYS = 'max_journeys'
|
||||
|
||||
DEFAULT_NAME = 'RMV Journey'
|
||||
|
||||
VALID_PRODUCTS = ['U-Bahn', 'Tram', 'Bus', 'S', 'RB', 'RE', 'EC', 'IC', 'ICE']
|
||||
|
||||
ICONS = {
|
||||
'U-Bahn': 'mdi:subway',
|
||||
'Tram': 'mdi:tram',
|
||||
'Bus': 'mdi:bus',
|
||||
'S': 'mdi:train',
|
||||
'RB': 'mdi:train',
|
||||
'RE': 'mdi:train',
|
||||
'EC': 'mdi:train',
|
||||
'IC': 'mdi:train',
|
||||
'ICE': 'mdi:train',
|
||||
'SEV': 'mdi:checkbox-blank-circle-outline',
|
||||
None: 'mdi:clock'
|
||||
}
|
||||
ATTRIBUTION = "Data provided by opendata.rmv.de"
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_NEXT_DEPARTURE): [{
|
||||
vol.Required(CONF_STATION): cv.string,
|
||||
vol.Optional(CONF_DESTINATIONS, default=[]):
|
||||
vol.All(cv.ensure_list, [cv.string]),
|
||||
vol.Optional(CONF_DIRECTIONS, default=[]):
|
||||
vol.All(cv.ensure_list, [cv.string]),
|
||||
vol.Optional(CONF_LINES, default=[]):
|
||||
vol.All(cv.ensure_list, [cv.positive_int, cv.string]),
|
||||
vol.Optional(CONF_PRODUCTS, default=VALID_PRODUCTS):
|
||||
vol.All(cv.ensure_list, [vol.In(VALID_PRODUCTS)]),
|
||||
vol.Optional(CONF_TIME_OFFSET, default=0): cv.positive_int,
|
||||
vol.Optional(CONF_MAX_JOURNEYS, default=5): cv.positive_int,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string}]
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Set up the RMV departure sensor."""
|
||||
sensors = []
|
||||
for next_departure in config.get(CONF_NEXT_DEPARTURE):
|
||||
sensors.append(
|
||||
RMVDepartureSensor(
|
||||
next_departure[CONF_STATION],
|
||||
next_departure.get(CONF_DESTINATIONS),
|
||||
next_departure.get(CONF_DIRECTIONS),
|
||||
next_departure.get(CONF_LINES),
|
||||
next_departure.get(CONF_PRODUCTS),
|
||||
next_departure.get(CONF_TIME_OFFSET),
|
||||
next_departure.get(CONF_MAX_JOURNEYS),
|
||||
next_departure.get(CONF_NAME)))
|
||||
add_entities(sensors, True)
|
||||
|
||||
|
||||
class RMVDepartureSensor(Entity):
|
||||
"""Implementation of an RMV departure sensor."""
|
||||
|
||||
def __init__(self, station, destinations, directions,
|
||||
lines, products, time_offset, max_journeys, name):
|
||||
"""Initialize the sensor."""
|
||||
self._station = station
|
||||
self._name = name
|
||||
self._state = None
|
||||
self.data = RMVDepartureData(station, destinations, directions, lines,
|
||||
products, time_offset, max_journeys)
|
||||
self._icon = ICONS[None]
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the sensor."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
"""Return True if entity is available."""
|
||||
return self._state is not None
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the next departure time."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
try:
|
||||
return {
|
||||
'next_departures': [val for val in self.data.departures[1:]],
|
||||
'direction': self.data.departures[0].get('direction'),
|
||||
'line': self.data.departures[0].get('line'),
|
||||
'minutes': self.data.departures[0].get('minutes'),
|
||||
'departure_time':
|
||||
self.data.departures[0].get('departure_time'),
|
||||
'product': self.data.departures[0].get('product'),
|
||||
}
|
||||
except IndexError:
|
||||
return {}
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
"""Icon to use in the frontend, if any."""
|
||||
return self._icon
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
"""Return the unit this state is expressed in."""
|
||||
return "min"
|
||||
|
||||
def update(self):
|
||||
"""Get the latest data and update the state."""
|
||||
self.data.update()
|
||||
if not self.data.departures:
|
||||
self._state = None
|
||||
self._icon = ICONS[None]
|
||||
return
|
||||
if self._name == DEFAULT_NAME:
|
||||
self._name = self.data.station
|
||||
self._station = self.data.station
|
||||
self._state = self.data.departures[0].get('minutes')
|
||||
self._icon = ICONS[self.data.departures[0].get('product')]
|
||||
|
||||
|
||||
class RMVDepartureData:
|
||||
"""Pull data from the opendata.rmv.de web page."""
|
||||
|
||||
def __init__(self, station_id, destinations, directions,
|
||||
lines, products, time_offset, max_journeys):
|
||||
"""Initialize the sensor."""
|
||||
import RMVtransport
|
||||
self.station = None
|
||||
self._station_id = station_id
|
||||
self._destinations = destinations
|
||||
self._directions = directions
|
||||
self._lines = lines
|
||||
self._products = products
|
||||
self._time_offset = time_offset
|
||||
self._max_journeys = max_journeys
|
||||
self.rmv = RMVtransport.RMVtransport()
|
||||
self.departures = []
|
||||
|
||||
def update(self):
|
||||
"""Update the connection data."""
|
||||
try:
|
||||
_data = self.rmv.get_departures(self._station_id,
|
||||
products=self._products,
|
||||
maxJourneys=50)
|
||||
except ValueError:
|
||||
self.departures = []
|
||||
_LOGGER.warning("Returned data not understood")
|
||||
return
|
||||
self.station = _data.get('station')
|
||||
_deps = []
|
||||
for journey in _data['journeys']:
|
||||
# find the first departure meeting the criteria
|
||||
_nextdep = {ATTR_ATTRIBUTION: ATTRIBUTION}
|
||||
if self._destinations:
|
||||
dest_found = False
|
||||
for dest in self._destinations:
|
||||
if dest in journey['stops']:
|
||||
dest_found = True
|
||||
_nextdep['destination'] = dest
|
||||
if not dest_found:
|
||||
continue
|
||||
elif self._lines and journey['number'] not in self._lines:
|
||||
continue
|
||||
elif journey['minutes'] < self._time_offset:
|
||||
continue
|
||||
for attr in ['direction', 'departure_time', 'product', 'minutes']:
|
||||
_nextdep[attr] = journey.get(attr, '')
|
||||
_nextdep['line'] = journey.get('number', '')
|
||||
_deps.append(_nextdep)
|
||||
if len(_deps) > self._max_journeys:
|
||||
break
|
||||
self.departures = _deps
|
||||
@@ -14,7 +14,7 @@ from homeassistant.components.sensor import PLATFORM_SCHEMA
|
||||
from homeassistant.const import ATTR_ATTRIBUTION, CONF_API_KEY, CONF_NAME
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
REQUIREMENTS = ['shodan==1.8.1']
|
||||
REQUIREMENTS = ['shodan==1.9.0']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ from homeassistant.const import (
|
||||
CONF_HOST, CONF_NAME, CONF_PORT, CONF_UNIT_OF_MEASUREMENT, STATE_UNKNOWN,
|
||||
CONF_VALUE_TEMPLATE)
|
||||
|
||||
REQUIREMENTS = ['pysnmp==4.4.4']
|
||||
REQUIREMENTS = ['pysnmp==4.4.5']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -158,8 +158,12 @@ class SonarrSensor(Entity):
|
||||
)
|
||||
elif self.type == 'series':
|
||||
for show in self.data:
|
||||
attributes[show['title']] = '{}/{} Episodes'.format(
|
||||
show['episodeFileCount'], show['episodeCount'])
|
||||
if 'episodeFileCount' not in show \
|
||||
or 'episodeCount' not in show:
|
||||
attributes[show['title']] = 'N/A'
|
||||
else:
|
||||
attributes[show['title']] = '{}/{} Episodes'.format(
|
||||
show['episodeFileCount'], show['episodeCount'])
|
||||
elif self.type == 'status':
|
||||
attributes = self.data
|
||||
return attributes
|
||||
|
||||
@@ -97,7 +97,6 @@ class StatisticsSensor(Entity):
|
||||
hass.async_add_job(self._initialize_from_database)
|
||||
|
||||
@callback
|
||||
# pylint: disable=invalid-name
|
||||
def async_stats_sensor_state_listener(entity, old_state, new_state):
|
||||
"""Handle the sensor state changes."""
|
||||
self._unit_of_measurement = new_state.attributes.get(
|
||||
|
||||
@@ -31,8 +31,10 @@ CONF_DESTINATION = 'destination'
|
||||
CONF_ORIGIN = 'origin'
|
||||
CONF_INCL_FILTER = 'incl_filter'
|
||||
CONF_EXCL_FILTER = 'excl_filter'
|
||||
CONF_REALTIME = 'realtime'
|
||||
|
||||
DEFAULT_NAME = 'Waze Travel Time'
|
||||
DEFAULT_REALTIME = True
|
||||
|
||||
ICON = 'mdi:car'
|
||||
|
||||
@@ -49,6 +51,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_INCL_FILTER): cv.string,
|
||||
vol.Optional(CONF_EXCL_FILTER): cv.string,
|
||||
vol.Optional(CONF_REALTIME, default=DEFAULT_REALTIME): cv.boolean,
|
||||
})
|
||||
|
||||
|
||||
@@ -60,9 +63,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
region = config.get(CONF_REGION)
|
||||
incl_filter = config.get(CONF_INCL_FILTER)
|
||||
excl_filter = config.get(CONF_EXCL_FILTER)
|
||||
realtime = config.get(CONF_REALTIME)
|
||||
|
||||
sensor = WazeTravelTime(name, origin, destination, region,
|
||||
incl_filter, excl_filter)
|
||||
incl_filter, excl_filter, realtime)
|
||||
|
||||
add_devices([sensor])
|
||||
|
||||
@@ -80,12 +84,13 @@ class WazeTravelTime(Entity):
|
||||
"""Representation of a Waze travel time sensor."""
|
||||
|
||||
def __init__(self, name, origin, destination, region,
|
||||
incl_filter, excl_filter):
|
||||
incl_filter, excl_filter, realtime):
|
||||
"""Initialize the Waze travel time sensor."""
|
||||
self._name = name
|
||||
self._region = region
|
||||
self._incl_filter = incl_filter
|
||||
self._excl_filter = excl_filter
|
||||
self._realtime = realtime
|
||||
self._state = None
|
||||
self._origin_entity_id = None
|
||||
self._destination_entity_id = None
|
||||
@@ -197,7 +202,7 @@ class WazeTravelTime(Entity):
|
||||
try:
|
||||
params = WazeRouteCalculator.WazeRouteCalculator(
|
||||
self._origin, self._destination, self._region)
|
||||
routes = params.calc_all_routes_info()
|
||||
routes = params.calc_all_routes_info(real_time=self._realtime)
|
||||
|
||||
if self._incl_filter is not None:
|
||||
routes = {k: v for k, v in routes.items() if
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""Component to embed Sonos."""
|
||||
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
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
all_switches = []
|
||||
|
||||
for setting in switches:
|
||||
all_switches.append(AmcrestSwitch(setting, camera))
|
||||
all_switches.append(AmcrestSwitch(setting, camera, name))
|
||||
|
||||
async_add_devices(all_switches, True)
|
||||
|
||||
@@ -38,11 +38,11 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
class AmcrestSwitch(ToggleEntity):
|
||||
"""Representation of an Amcrest IP camera switch."""
|
||||
|
||||
def __init__(self, setting, camera):
|
||||
def __init__(self, setting, camera, name):
|
||||
"""Initialize the Amcrest switch."""
|
||||
self._setting = setting
|
||||
self._camera = camera
|
||||
self._name = SWITCHES[setting][0]
|
||||
self._name = '{} {}'.format(SWITCHES[setting][0], name)
|
||||
self._icon = SWITCHES[setting][1]
|
||||
self._state = None
|
||||
|
||||
|
||||
118
homeassistant/components/switch/deconz.py
Normal file
118
homeassistant/components/switch/deconz.py
Normal file
@@ -0,0 +1,118 @@
|
||||
"""
|
||||
Support for deCONZ switches.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/switch.deconz/
|
||||
"""
|
||||
from homeassistant.components.deconz.const import (
|
||||
DOMAIN as DATA_DECONZ, DATA_DECONZ_ID, DATA_DECONZ_UNSUB,
|
||||
POWER_PLUGS, SIRENS)
|
||||
from homeassistant.components.switch import SwitchDevice
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
|
||||
DEPENDENCIES = ['deconz']
|
||||
|
||||
|
||||
async def async_setup_platform(hass, config, async_add_devices,
|
||||
discovery_info=None):
|
||||
"""Old way of setting up deCONZ switches."""
|
||||
pass
|
||||
|
||||
|
||||
async def async_setup_entry(hass, config_entry, async_add_devices):
|
||||
"""Set up switches for deCONZ component.
|
||||
|
||||
Switches are based same device class as lights in deCONZ.
|
||||
"""
|
||||
@callback
|
||||
def async_add_switch(lights):
|
||||
"""Add switch from deCONZ."""
|
||||
entities = []
|
||||
for light in lights:
|
||||
if light.type in POWER_PLUGS:
|
||||
entities.append(DeconzPowerPlug(light))
|
||||
elif light.type in SIRENS:
|
||||
entities.append(DeconzSiren(light))
|
||||
async_add_devices(entities, True)
|
||||
|
||||
hass.data[DATA_DECONZ_UNSUB].append(
|
||||
async_dispatcher_connect(hass, 'deconz_new_light', async_add_switch))
|
||||
|
||||
async_add_switch(hass.data[DATA_DECONZ].lights.values())
|
||||
|
||||
|
||||
class DeconzSwitch(SwitchDevice):
|
||||
"""Representation of a deCONZ switch."""
|
||||
|
||||
def __init__(self, switch):
|
||||
"""Set up switch and add update callback to get data from websocket."""
|
||||
self._switch = switch
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Subscribe to switches events."""
|
||||
self._switch.register_async_callback(self.async_update_callback)
|
||||
self.hass.data[DATA_DECONZ_ID][self.entity_id] = self._switch.deconz_id
|
||||
|
||||
@callback
|
||||
def async_update_callback(self, reason):
|
||||
"""Update the switch's state."""
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the switch."""
|
||||
return self._switch.name
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return a unique identifier for this switch."""
|
||||
return self._switch.uniqueid
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
"""Return True if light is available."""
|
||||
return self._switch.reachable
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""No polling needed."""
|
||||
return False
|
||||
|
||||
|
||||
class DeconzPowerPlug(DeconzSwitch):
|
||||
"""Representation of power plugs from deCONZ."""
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if switch is on."""
|
||||
return self._switch.state
|
||||
|
||||
async def async_turn_on(self, **kwargs):
|
||||
"""Turn on switch."""
|
||||
data = {'on': True}
|
||||
await self._switch.async_set_state(data)
|
||||
|
||||
async def async_turn_off(self, **kwargs):
|
||||
"""Turn off switch."""
|
||||
data = {'on': False}
|
||||
await self._switch.async_set_state(data)
|
||||
|
||||
|
||||
class DeconzSiren(DeconzSwitch):
|
||||
"""Representation of sirens from deCONZ."""
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if switch is on."""
|
||||
return self._switch.alert == 'lselect'
|
||||
|
||||
async def async_turn_on(self, **kwargs):
|
||||
"""Turn on switch."""
|
||||
data = {'alert': 'lselect'}
|
||||
await self._switch.async_set_state(data)
|
||||
|
||||
async def async_turn_off(self, **kwargs):
|
||||
"""Turn off switch."""
|
||||
data = {'alert': 'none'}
|
||||
await self._switch.async_set_state(data)
|
||||
@@ -13,7 +13,7 @@ from homeassistant.const import (
|
||||
CONF_HOST, CONF_NAME, CONF_PORT, CONF_PAYLOAD_ON, CONF_PAYLOAD_OFF)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['pysnmp==4.4.4']
|
||||
REQUIREMENTS = ['pysnmp==4.4.5']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -4,108 +4,42 @@ Support for Velbus switches.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/switch.velbus/
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import CONF_NAME, CONF_DEVICES
|
||||
from homeassistant.components.switch import SwitchDevice, PLATFORM_SCHEMA
|
||||
from homeassistant.components.velbus import DOMAIN
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.switch import SwitchDevice
|
||||
from homeassistant.components.velbus import (
|
||||
DOMAIN as VELBUS_DOMAIN, VelbusEntity)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SWITCH_SCHEMA = {
|
||||
vol.Required('module'): cv.positive_int,
|
||||
vol.Required('channel'): cv.positive_int,
|
||||
vol.Required(CONF_NAME): cv.string
|
||||
}
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_DEVICES):
|
||||
vol.All(cv.ensure_list, [SWITCH_SCHEMA])
|
||||
})
|
||||
|
||||
DEPENDENCIES = ['velbus']
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the Switch."""
|
||||
velbus = hass.data[DOMAIN]
|
||||
devices = []
|
||||
|
||||
for switch in config[CONF_DEVICES]:
|
||||
devices.append(VelbusSwitch(switch, velbus))
|
||||
add_devices(devices)
|
||||
return True
|
||||
async def async_setup_platform(hass, config, async_add_devices,
|
||||
discovery_info=None):
|
||||
"""Set up the Velbus Switch platform."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
switches = []
|
||||
for switch in discovery_info:
|
||||
module = hass.data[VELBUS_DOMAIN].get_module(switch[0])
|
||||
channel = switch[1]
|
||||
switches.append(VelbusSwitch(module, channel))
|
||||
async_add_devices(switches)
|
||||
|
||||
|
||||
class VelbusSwitch(SwitchDevice):
|
||||
class VelbusSwitch(VelbusEntity, SwitchDevice):
|
||||
"""Representation of a switch."""
|
||||
|
||||
def __init__(self, switch, velbus):
|
||||
"""Initialize a Velbus switch."""
|
||||
self._velbus = velbus
|
||||
self._name = switch[CONF_NAME]
|
||||
self._module = switch['module']
|
||||
self._channel = switch['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 switch."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Disable polling."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if the switch is on."""
|
||||
return self._state
|
||||
return self._module.is_on(self._channel)
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
"""Instruct the switch to turn on."""
|
||||
import velbus
|
||||
message = velbus.SwitchRelayOnMessage()
|
||||
message.set_defaults(self._module)
|
||||
message.relay_channels = [self._channel]
|
||||
self._velbus.send(message)
|
||||
self._module.turn_on(self._channel)
|
||||
|
||||
def turn_off(self, **kwargs):
|
||||
"""Instruct the switch 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)
|
||||
self._module.turn_off(self._channel)
|
||||
|
||||
@@ -50,6 +50,8 @@ TAHOMA_TYPES = {
|
||||
'io:WindowOpenerVeluxIOComponent': 'cover',
|
||||
'io:LightIOSystemSensor': 'sensor',
|
||||
'rts:GarageDoor4TRTSComponent': 'switch',
|
||||
'io:VerticalExteriorAwningIOComponent': 'cover',
|
||||
'io:HorizontalAwningIOComponent': 'cover',
|
||||
'rtds:RTDSSmokeSensor': 'smoke',
|
||||
}
|
||||
|
||||
|
||||
@@ -14,12 +14,12 @@ import voluptuous as vol
|
||||
from homeassistant.components import group
|
||||
from homeassistant.const import (
|
||||
ATTR_BATTERY_LEVEL, ATTR_COMMAND, ATTR_ENTITY_ID, SERVICE_TOGGLE,
|
||||
SERVICE_TURN_OFF, SERVICE_TURN_ON, STATE_ON)
|
||||
SERVICE_TURN_OFF, SERVICE_TURN_ON, STATE_ON, STATE_PAUSED, STATE_IDLE)
|
||||
from homeassistant.loader import bind_hass
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers.entity import ToggleEntity
|
||||
from homeassistant.helpers.entity import (ToggleEntity, Entity)
|
||||
from homeassistant.helpers.icon import icon_for_battery_level
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -45,6 +45,8 @@ SERVICE_RETURN_TO_BASE = 'return_to_base'
|
||||
SERVICE_SEND_COMMAND = 'send_command'
|
||||
SERVICE_SET_FAN_SPEED = 'set_fan_speed'
|
||||
SERVICE_START_PAUSE = 'start_pause'
|
||||
SERVICE_START = 'start'
|
||||
SERVICE_PAUSE = 'pause'
|
||||
SERVICE_STOP = 'stop'
|
||||
|
||||
VACUUM_SERVICE_SCHEMA = vol.Schema({
|
||||
@@ -65,6 +67,8 @@ SERVICE_TO_METHOD = {
|
||||
SERVICE_TURN_OFF: {'method': 'async_turn_off'},
|
||||
SERVICE_TOGGLE: {'method': 'async_toggle'},
|
||||
SERVICE_START_PAUSE: {'method': 'async_start_pause'},
|
||||
SERVICE_START: {'method': 'async_start'},
|
||||
SERVICE_PAUSE: {'method': 'async_pause'},
|
||||
SERVICE_RETURN_TO_BASE: {'method': 'async_return_to_base'},
|
||||
SERVICE_CLEAN_SPOT: {'method': 'async_clean_spot'},
|
||||
SERVICE_LOCATE: {'method': 'async_locate'},
|
||||
@@ -75,6 +79,13 @@ SERVICE_TO_METHOD = {
|
||||
'schema': VACUUM_SEND_COMMAND_SERVICE_SCHEMA},
|
||||
}
|
||||
|
||||
STATE_CLEANING = 'cleaning'
|
||||
STATE_DOCKED = 'docked'
|
||||
STATE_IDLE = STATE_IDLE
|
||||
STATE_PAUSED = STATE_PAUSED
|
||||
STATE_RETURNING = 'returning'
|
||||
STATE_ERROR = 'error'
|
||||
|
||||
DEFAULT_NAME = 'Vacuum cleaner robot'
|
||||
|
||||
SUPPORT_TURN_ON = 1
|
||||
@@ -89,6 +100,8 @@ SUPPORT_SEND_COMMAND = 256
|
||||
SUPPORT_LOCATE = 512
|
||||
SUPPORT_CLEAN_SPOT = 1024
|
||||
SUPPORT_MAP = 2048
|
||||
SUPPORT_STATE = 4096
|
||||
SUPPORT_START = 8192
|
||||
|
||||
|
||||
@bind_hass
|
||||
@@ -147,6 +160,20 @@ def start_pause(hass, entity_id=None):
|
||||
hass.services.call(DOMAIN, SERVICE_START_PAUSE, data)
|
||||
|
||||
|
||||
@bind_hass
|
||||
def start(hass, entity_id=None):
|
||||
"""Tell all or specified vacuum to start or resume the current task."""
|
||||
data = {ATTR_ENTITY_ID: entity_id} if entity_id else None
|
||||
hass.services.call(DOMAIN, SERVICE_START, data)
|
||||
|
||||
|
||||
@bind_hass
|
||||
def pause(hass, entity_id=None):
|
||||
"""Tell all or the specified vacuum to pause the current task."""
|
||||
data = {ATTR_ENTITY_ID: entity_id} if entity_id else None
|
||||
hass.services.call(DOMAIN, SERVICE_PAUSE, data)
|
||||
|
||||
|
||||
@bind_hass
|
||||
def stop(hass, entity_id=None):
|
||||
"""Stop all or specified vacuum."""
|
||||
@@ -208,33 +235,22 @@ def async_setup(hass, config):
|
||||
return True
|
||||
|
||||
|
||||
class VacuumDevice(ToggleEntity):
|
||||
"""Representation of a vacuum cleaner robot."""
|
||||
class _BaseVacuum(Entity):
|
||||
"""Representation of a base vacuum.
|
||||
|
||||
Contains common properties and functions for all vacuum devices.
|
||||
"""
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Flag vacuum cleaner features that are supported."""
|
||||
raise NotImplementedError()
|
||||
|
||||
@property
|
||||
def status(self):
|
||||
"""Return the status of the vacuum cleaner."""
|
||||
return None
|
||||
|
||||
@property
|
||||
def battery_level(self):
|
||||
"""Return the battery level of the vacuum cleaner."""
|
||||
return None
|
||||
|
||||
@property
|
||||
def battery_icon(self):
|
||||
"""Return the battery icon for the vacuum cleaner."""
|
||||
charging = False
|
||||
if self.status is not None:
|
||||
charging = 'charg' in self.status.lower()
|
||||
return icon_for_battery_level(
|
||||
battery_level=self.battery_level, charging=charging)
|
||||
|
||||
@property
|
||||
def fan_speed(self):
|
||||
"""Return the fan speed of the vacuum cleaner."""
|
||||
@@ -245,6 +261,94 @@ class VacuumDevice(ToggleEntity):
|
||||
"""Get the list of available fan speed steps of the vacuum cleaner."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def stop(self, **kwargs):
|
||||
"""Stop the vacuum cleaner."""
|
||||
raise NotImplementedError()
|
||||
|
||||
async def async_stop(self, **kwargs):
|
||||
"""Stop the vacuum cleaner.
|
||||
|
||||
This method must be run in the event loop.
|
||||
"""
|
||||
await self.hass.async_add_executor_job(partial(self.stop, **kwargs))
|
||||
|
||||
def return_to_base(self, **kwargs):
|
||||
"""Set the vacuum cleaner to return to the dock."""
|
||||
raise NotImplementedError()
|
||||
|
||||
async def async_return_to_base(self, **kwargs):
|
||||
"""Set the vacuum cleaner to return to the dock.
|
||||
|
||||
This method must be run in the event loop.
|
||||
"""
|
||||
await self.hass.async_add_executor_job(
|
||||
partial(self.return_to_base, **kwargs))
|
||||
|
||||
def clean_spot(self, **kwargs):
|
||||
"""Perform a spot clean-up."""
|
||||
raise NotImplementedError()
|
||||
|
||||
async def async_clean_spot(self, **kwargs):
|
||||
"""Perform a spot clean-up.
|
||||
|
||||
This method must be run in the event loop.
|
||||
"""
|
||||
await self.hass.async_add_executor_job(
|
||||
partial(self.clean_spot, **kwargs))
|
||||
|
||||
def locate(self, **kwargs):
|
||||
"""Locate the vacuum cleaner."""
|
||||
raise NotImplementedError()
|
||||
|
||||
async def async_locate(self, **kwargs):
|
||||
"""Locate the vacuum cleaner.
|
||||
|
||||
This method must be run in the event loop.
|
||||
"""
|
||||
await self.hass.async_add_executor_job(partial(self.locate, **kwargs))
|
||||
|
||||
def set_fan_speed(self, fan_speed, **kwargs):
|
||||
"""Set fan speed."""
|
||||
raise NotImplementedError()
|
||||
|
||||
async def async_set_fan_speed(self, fan_speed, **kwargs):
|
||||
"""Set fan speed.
|
||||
|
||||
This method must be run in the event loop.
|
||||
"""
|
||||
await self.hass.async_add_executor_job(
|
||||
partial(self.set_fan_speed, fan_speed, **kwargs))
|
||||
|
||||
def send_command(self, command, params=None, **kwargs):
|
||||
"""Send a command to a vacuum cleaner."""
|
||||
raise NotImplementedError()
|
||||
|
||||
async def async_send_command(self, command, params=None, **kwargs):
|
||||
"""Send a command to a vacuum cleaner.
|
||||
|
||||
This method must be run in the event loop.
|
||||
"""
|
||||
await self.hass.async_add_executor_job(
|
||||
partial(self.send_command, command, params=params, **kwargs))
|
||||
|
||||
|
||||
class VacuumDevice(_BaseVacuum, ToggleEntity):
|
||||
"""Representation of a vacuum cleaner robot."""
|
||||
|
||||
@property
|
||||
def status(self):
|
||||
"""Return the status of the vacuum cleaner."""
|
||||
return None
|
||||
|
||||
@property
|
||||
def battery_icon(self):
|
||||
"""Return the battery icon for the vacuum cleaner."""
|
||||
charging = False
|
||||
if self.status is not None:
|
||||
charging = 'charg' in self.status.lower()
|
||||
return icon_for_battery_level(
|
||||
battery_level=self.battery_level, charging=charging)
|
||||
|
||||
@property
|
||||
def state_attributes(self):
|
||||
"""Return the state attributes of the vacuum cleaner."""
|
||||
@@ -267,100 +371,88 @@ class VacuumDevice(ToggleEntity):
|
||||
"""Turn the vacuum on and start cleaning."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def async_turn_on(self, **kwargs):
|
||||
async def async_turn_on(self, **kwargs):
|
||||
"""Turn the vacuum on and start cleaning.
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
This method must be run in the event loop.
|
||||
"""
|
||||
return self.hass.async_add_job(partial(self.turn_on, **kwargs))
|
||||
await self.hass.async_add_executor_job(
|
||||
partial(self.turn_on, **kwargs))
|
||||
|
||||
def turn_off(self, **kwargs):
|
||||
"""Turn the vacuum off stopping the cleaning and returning home."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def async_turn_off(self, **kwargs):
|
||||
async def async_turn_off(self, **kwargs):
|
||||
"""Turn the vacuum off stopping the cleaning and returning home.
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
This method must be run in the event loop.
|
||||
"""
|
||||
return self.hass.async_add_job(partial(self.turn_off, **kwargs))
|
||||
|
||||
def return_to_base(self, **kwargs):
|
||||
"""Set the vacuum cleaner to return to the dock."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def async_return_to_base(self, **kwargs):
|
||||
"""Set the vacuum cleaner to return to the dock.
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
return self.hass.async_add_job(partial(self.return_to_base, **kwargs))
|
||||
|
||||
def stop(self, **kwargs):
|
||||
"""Stop the vacuum cleaner."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def async_stop(self, **kwargs):
|
||||
"""Stop the vacuum cleaner.
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
return self.hass.async_add_job(partial(self.stop, **kwargs))
|
||||
|
||||
def clean_spot(self, **kwargs):
|
||||
"""Perform a spot clean-up."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def async_clean_spot(self, **kwargs):
|
||||
"""Perform a spot clean-up.
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
return self.hass.async_add_job(partial(self.clean_spot, **kwargs))
|
||||
|
||||
def locate(self, **kwargs):
|
||||
"""Locate the vacuum cleaner."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def async_locate(self, **kwargs):
|
||||
"""Locate the vacuum cleaner.
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
return self.hass.async_add_job(partial(self.locate, **kwargs))
|
||||
|
||||
def set_fan_speed(self, fan_speed, **kwargs):
|
||||
"""Set fan speed."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def async_set_fan_speed(self, fan_speed, **kwargs):
|
||||
"""Set fan speed.
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
return self.hass.async_add_job(
|
||||
partial(self.set_fan_speed, fan_speed, **kwargs))
|
||||
await self.hass.async_add_executor_job(
|
||||
partial(self.turn_off, **kwargs))
|
||||
|
||||
def start_pause(self, **kwargs):
|
||||
"""Start, pause or resume the cleaning task."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def async_start_pause(self, **kwargs):
|
||||
async def async_start_pause(self, **kwargs):
|
||||
"""Start, pause or resume the cleaning task.
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
This method must be run in the event loop.
|
||||
"""
|
||||
return self.hass.async_add_job(
|
||||
await self.hass.async_add_executor_job(
|
||||
partial(self.start_pause, **kwargs))
|
||||
|
||||
def send_command(self, command, params=None, **kwargs):
|
||||
"""Send a command to a vacuum cleaner."""
|
||||
|
||||
class StateVacuumDevice(_BaseVacuum):
|
||||
"""Representation of a vacuum cleaner robot that supports states."""
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the vacuum cleaner."""
|
||||
return None
|
||||
|
||||
@property
|
||||
def battery_icon(self):
|
||||
"""Return the battery icon for the vacuum cleaner."""
|
||||
charging = bool(self.state == STATE_DOCKED)
|
||||
|
||||
return icon_for_battery_level(
|
||||
battery_level=self.battery_level, charging=charging)
|
||||
|
||||
@property
|
||||
def state_attributes(self):
|
||||
"""Return the state attributes of the vacuum cleaner."""
|
||||
data = {}
|
||||
|
||||
if self.battery_level is not None:
|
||||
data[ATTR_BATTERY_LEVEL] = self.battery_level
|
||||
data[ATTR_BATTERY_ICON] = self.battery_icon
|
||||
|
||||
if self.fan_speed is not None:
|
||||
data[ATTR_FAN_SPEED] = self.fan_speed
|
||||
data[ATTR_FAN_SPEED_LIST] = self.fan_speed_list
|
||||
|
||||
return data
|
||||
|
||||
def start(self):
|
||||
"""Start or resume the cleaning task."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def async_send_command(self, command, params=None, **kwargs):
|
||||
"""Send a command to a vacuum cleaner.
|
||||
async def async_start(self):
|
||||
"""Start or resume the cleaning task.
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
This method must be run in the event loop.
|
||||
"""
|
||||
return self.hass.async_add_job(
|
||||
partial(self.send_command, command, params=params, **kwargs))
|
||||
await self.hass.async_add_executor_job(self.start)
|
||||
|
||||
def pause(self):
|
||||
"""Pause the cleaning task."""
|
||||
raise NotImplementedError()
|
||||
|
||||
async def async_pause(self):
|
||||
"""Pause the cleaning task.
|
||||
|
||||
This method must be run in the event loop.
|
||||
"""
|
||||
await self.hass.async_add_executor_job(self.pause)
|
||||
|
||||
@@ -10,7 +10,9 @@ from homeassistant.components.vacuum import (
|
||||
ATTR_CLEANED_AREA, SUPPORT_BATTERY, SUPPORT_CLEAN_SPOT,
|
||||
SUPPORT_FAN_SPEED, SUPPORT_LOCATE, SUPPORT_PAUSE, SUPPORT_RETURN_HOME,
|
||||
SUPPORT_SEND_COMMAND, SUPPORT_STATUS, SUPPORT_STOP, SUPPORT_TURN_OFF,
|
||||
SUPPORT_TURN_ON, VacuumDevice)
|
||||
SUPPORT_TURN_ON, SUPPORT_STATE, SUPPORT_START, STATE_CLEANING,
|
||||
STATE_DOCKED, STATE_IDLE, STATE_PAUSED, STATE_RETURNING, VacuumDevice,
|
||||
StateVacuumDevice)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -28,12 +30,17 @@ SUPPORT_ALL_SERVICES = SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_PAUSE | \
|
||||
SUPPORT_LOCATE | SUPPORT_STATUS | SUPPORT_BATTERY | \
|
||||
SUPPORT_CLEAN_SPOT
|
||||
|
||||
SUPPORT_STATE_SERVICES = SUPPORT_STATE | SUPPORT_PAUSE | SUPPORT_STOP | \
|
||||
SUPPORT_RETURN_HOME | SUPPORT_FAN_SPEED | \
|
||||
SUPPORT_BATTERY | SUPPORT_CLEAN_SPOT | SUPPORT_START
|
||||
|
||||
FAN_SPEEDS = ['min', 'medium', 'high', 'max']
|
||||
DEMO_VACUUM_COMPLETE = '0_Ground_floor'
|
||||
DEMO_VACUUM_MOST = '1_First_floor'
|
||||
DEMO_VACUUM_BASIC = '2_Second_floor'
|
||||
DEMO_VACUUM_MINIMAL = '3_Third_floor'
|
||||
DEMO_VACUUM_NONE = '4_Fourth_floor'
|
||||
DEMO_VACUUM_STATE = '5_Fifth_floor'
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
@@ -44,6 +51,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
DemoVacuum(DEMO_VACUUM_BASIC, SUPPORT_BASIC_SERVICES),
|
||||
DemoVacuum(DEMO_VACUUM_MINIMAL, SUPPORT_MINIMAL_SERVICES),
|
||||
DemoVacuum(DEMO_VACUUM_NONE, 0),
|
||||
StateDemoVacuum(DEMO_VACUUM_STATE),
|
||||
])
|
||||
|
||||
|
||||
@@ -204,3 +212,125 @@ class DemoVacuum(VacuumDevice):
|
||||
self._status = 'Executing {}({})'.format(command, params)
|
||||
self._state = True
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
|
||||
class StateDemoVacuum(StateVacuumDevice):
|
||||
"""Representation of a demo vacuum supporting states."""
|
||||
|
||||
def __init__(self, name):
|
||||
"""Initialize the vacuum."""
|
||||
self._name = name
|
||||
self._supported_features = SUPPORT_STATE_SERVICES
|
||||
self._state = STATE_DOCKED
|
||||
self._fan_speed = FAN_SPEEDS[1]
|
||||
self._cleaned_area = 0
|
||||
self._battery_level = 100
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the vacuum."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""No polling needed for a demo vacuum."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Flag supported features."""
|
||||
return self._supported_features
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the current state of the vacuum."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def battery_level(self):
|
||||
"""Return the current battery level of the vacuum."""
|
||||
if self.supported_features & SUPPORT_BATTERY == 0:
|
||||
return
|
||||
|
||||
return max(0, min(100, self._battery_level))
|
||||
|
||||
@property
|
||||
def fan_speed(self):
|
||||
"""Return the current fan speed of the vacuum."""
|
||||
if self.supported_features & SUPPORT_FAN_SPEED == 0:
|
||||
return
|
||||
|
||||
return self._fan_speed
|
||||
|
||||
@property
|
||||
def fan_speed_list(self):
|
||||
"""Return the list of supported fan speeds."""
|
||||
if self.supported_features & SUPPORT_FAN_SPEED == 0:
|
||||
return
|
||||
return FAN_SPEEDS
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return device state attributes."""
|
||||
return {ATTR_CLEANED_AREA: round(self._cleaned_area, 2)}
|
||||
|
||||
def start(self):
|
||||
"""Start or resume the cleaning task."""
|
||||
if self.supported_features & SUPPORT_START == 0:
|
||||
return
|
||||
|
||||
if self._state != STATE_CLEANING:
|
||||
self._state = STATE_CLEANING
|
||||
self._cleaned_area += 1.32
|
||||
self._battery_level -= 1
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def pause(self):
|
||||
"""Pause the cleaning task."""
|
||||
if self.supported_features & SUPPORT_PAUSE == 0:
|
||||
return
|
||||
|
||||
if self._state == STATE_CLEANING:
|
||||
self._state = STATE_PAUSED
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def stop(self, **kwargs):
|
||||
"""Stop the cleaning task, do not return to dock."""
|
||||
if self.supported_features & SUPPORT_STOP == 0:
|
||||
return
|
||||
|
||||
self._state = STATE_IDLE
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def return_to_base(self, **kwargs):
|
||||
"""Return dock to charging base."""
|
||||
if self.supported_features & SUPPORT_RETURN_HOME == 0:
|
||||
return
|
||||
|
||||
self._state = STATE_RETURNING
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
self.hass.loop.call_later(30, self.__set_state_to_dock)
|
||||
|
||||
def clean_spot(self, **kwargs):
|
||||
"""Perform a spot clean-up."""
|
||||
if self.supported_features & SUPPORT_CLEAN_SPOT == 0:
|
||||
return
|
||||
|
||||
self._state = STATE_CLEANING
|
||||
self._cleaned_area += 1.32
|
||||
self._battery_level -= 1
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def set_fan_speed(self, fan_speed, **kwargs):
|
||||
"""Set the vacuum's fan speed."""
|
||||
if self.supported_features & SUPPORT_FAN_SPEED == 0:
|
||||
return
|
||||
|
||||
if fan_speed in self.fan_speed_list:
|
||||
self._fan_speed = fan_speed
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def __set_state_to_dock(self):
|
||||
self._state = STATE_DOCKED
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
@@ -35,6 +35,20 @@ start_pause:
|
||||
description: Name of the vacuum entity.
|
||||
example: 'vacuum.xiaomi_vacuum_cleaner'
|
||||
|
||||
start:
|
||||
description: Start or resume the cleaning task.
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name of the vacuum entity.
|
||||
example: 'vacuum.xiaomi_vacuum_cleaner'
|
||||
|
||||
pause:
|
||||
description: Pause the cleaning task.
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name of the vacuum entity.
|
||||
example: 'vacuum.xiaomi_vacuum_cleaner'
|
||||
|
||||
return_to_base:
|
||||
description: Tell the vacuum cleaner to return to its dock.
|
||||
fields:
|
||||
|
||||
@@ -9,8 +9,10 @@ import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP, CONF_PORT
|
||||
from homeassistant.helpers.discovery import load_platform
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
REQUIREMENTS = ['python-velbus==2.0.11']
|
||||
REQUIREMENTS = ['python-velbus==2.0.17']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -26,18 +28,76 @@ CONFIG_SCHEMA = vol.Schema({
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
async def async_setup(hass, config):
|
||||
"""Set up the Velbus platform."""
|
||||
import velbus
|
||||
port = config[DOMAIN].get(CONF_PORT)
|
||||
connection = velbus.VelbusUSBConnection(port)
|
||||
controller = velbus.Controller(connection)
|
||||
controller = velbus.Controller(port)
|
||||
|
||||
hass.data[DOMAIN] = controller
|
||||
|
||||
def stop_velbus(event):
|
||||
"""Disconnect from serial port."""
|
||||
_LOGGER.debug("Shutting down ")
|
||||
connection.stop()
|
||||
controller.stop()
|
||||
|
||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_velbus)
|
||||
|
||||
def callback():
|
||||
modules = controller.get_modules()
|
||||
discovery_info = {
|
||||
'switch': [],
|
||||
'binary_sensor': []
|
||||
}
|
||||
for module in modules:
|
||||
for channel in range(1, module.number_of_channels() + 1):
|
||||
for category in discovery_info:
|
||||
if category in module.get_categories(channel):
|
||||
discovery_info[category].append((
|
||||
module.get_module_address(),
|
||||
channel
|
||||
))
|
||||
load_platform(hass, 'switch', DOMAIN,
|
||||
discovery_info['switch'], config)
|
||||
load_platform(hass, 'binary_sensor', DOMAIN,
|
||||
discovery_info['binary_sensor'], config)
|
||||
|
||||
controller.scan(callback)
|
||||
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_velbus)
|
||||
return True
|
||||
|
||||
|
||||
class VelbusEntity(Entity):
|
||||
"""Representation of a Velbus entity."""
|
||||
|
||||
def __init__(self, module, channel):
|
||||
"""Initialize a Velbus entity."""
|
||||
self._module = module
|
||||
self._channel = channel
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Get unique ID."""
|
||||
serial = 0
|
||||
if self._module.serial == 0:
|
||||
serial = self._module.get_module_address()
|
||||
else:
|
||||
serial = self._module.serial
|
||||
return "{}-{}".format(serial, self._channel)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the display name of this entity."""
|
||||
return self._module.get_name(self._channel)
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Disable polling."""
|
||||
return False
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Add listener for state changes."""
|
||||
self._module.on_status_update(self._channel, self._on_update)
|
||||
|
||||
def _on_update(self, state):
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
@@ -131,7 +131,7 @@ class OpenWeatherMapWeather(WeatherEntity):
|
||||
@property
|
||||
def wind_speed(self):
|
||||
"""Return the wind speed."""
|
||||
return self.data.get_wind().get('speed')
|
||||
return round(self.data.get_wind().get('speed') * 3.6, 2)
|
||||
|
||||
@property
|
||||
def wind_bearing(self):
|
||||
@@ -173,7 +173,10 @@ class OpenWeatherMapWeather(WeatherEntity):
|
||||
ATTR_FORECAST_TEMP:
|
||||
entry.get_temperature('celsius').get('temp'),
|
||||
ATTR_FORECAST_PRECIPITATION:
|
||||
entry.get_rain().get('3h'),
|
||||
(round(entry.get_rain().get('3h'), 1)
|
||||
if entry.get_rain().get('3h') is not None
|
||||
and (round(entry.get_rain().get('3h'), 1) > 0)
|
||||
else None),
|
||||
ATTR_FORECAST_CONDITION:
|
||||
[k for k, v in CONDITION_CLASSES.items()
|
||||
if entry.get_weather_code() in v][0]
|
||||
|
||||
@@ -519,8 +519,12 @@ def handle_call_service(hass, connection, msg):
|
||||
"""
|
||||
async def call_service_helper(msg):
|
||||
"""Call a service and fire complete message."""
|
||||
blocking = True
|
||||
if (msg['domain'] == 'homeassistant' and
|
||||
msg['service'] in ['restart', 'stop']):
|
||||
blocking = False
|
||||
await hass.services.async_call(
|
||||
msg['domain'], msg['service'], msg.get('service_data'), True,
|
||||
msg['domain'], msg['service'], msg.get('service_data'), blocking,
|
||||
connection.context(msg))
|
||||
connection.send_message_outside(result_message(msg['id']))
|
||||
|
||||
|
||||
@@ -29,6 +29,10 @@ class ZoneFlowHandler(data_entry_flow.FlowHandler):
|
||||
"""Initialize zone configuration flow."""
|
||||
pass
|
||||
|
||||
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 = {}
|
||||
|
||||
@@ -35,7 +35,7 @@ from .discovery_schemas import DISCOVERY_SCHEMAS
|
||||
from .util import (check_node_schema, check_value_schema, node_name,
|
||||
check_has_unique_id, is_node_parsed)
|
||||
|
||||
REQUIREMENTS = ['pydispatcher==2.0.5', 'python_openzwave==0.4.3']
|
||||
REQUIREMENTS = ['pydispatcher==2.0.5', 'python_openzwave==0.4.9']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -175,6 +175,7 @@ DISCOVERY_SCHEMAS = [
|
||||
{const.DISC_COMPONENT: 'lock',
|
||||
const.DISC_GENERIC_DEVICE_CLASS: [const.GENERIC_TYPE_ENTRY_CONTROL],
|
||||
const.DISC_SPECIFIC_DEVICE_CLASS: [
|
||||
const.SPECIFIC_TYPE_DOOR_LOCK,
|
||||
const.SPECIFIC_TYPE_ADVANCED_DOOR_LOCK,
|
||||
const.SPECIFIC_TYPE_SECURE_KEYPAD_DOOR_LOCK,
|
||||
const.SPECIFIC_TYPE_SECURE_LOCKBOX],
|
||||
|
||||
@@ -24,20 +24,24 @@ Before instantiating the handler, Home Assistant will make sure to load all
|
||||
dependencies and install the requirements of the component.
|
||||
|
||||
At a minimum, each config flow will have to define a version number and the
|
||||
'init' step.
|
||||
'user' step.
|
||||
|
||||
@config_entries.HANDLERS.register(DOMAIN)
|
||||
class ExampleConfigFlow(config_entries.FlowHandler):
|
||||
class ExampleConfigFlow(data_entry_flow.FlowHandler):
|
||||
|
||||
VERSION = 1
|
||||
|
||||
async def async_step_init(self, user_input=None):
|
||||
async def async_step_user(self, user_input=None):
|
||||
…
|
||||
|
||||
The 'init' step is the first step of a flow and is called when a user
|
||||
The 'user' step is the first step of a flow and is called when a user
|
||||
starts a new flow. Each step has three different possible results: "Show Form",
|
||||
"Abort" and "Create Entry".
|
||||
|
||||
> Note: prior 0.76, the default step is 'init' step, some config flows still
|
||||
keep 'init' step to avoid break localization. All new config flow should use
|
||||
'user' step.
|
||||
|
||||
### Show Form
|
||||
|
||||
This will show a form to the user to fill in. You define the current step,
|
||||
@@ -50,7 +54,7 @@ a title, a description and the schema of the data that needs to be returned.
|
||||
data_schema[vol.Required('password')] = str
|
||||
|
||||
return self.async_show_form(
|
||||
step_id='init',
|
||||
step_id='user',
|
||||
title='Account Info',
|
||||
data_schema=vol.Schema(data_schema)
|
||||
)
|
||||
@@ -97,10 +101,10 @@ Assistant, a success message is shown to the user and the flow is finished.
|
||||
You might want to initialize a config flow programmatically. For example, if
|
||||
we discover a device on the network that requires user interaction to finish
|
||||
setup. To do so, pass a source parameter and optional user input to the init
|
||||
step:
|
||||
method:
|
||||
|
||||
await hass.config_entries.flow.async_init(
|
||||
'hue', source='discovery', data=discovery_info)
|
||||
'hue', context={'source': 'discovery'}, data=discovery_info)
|
||||
|
||||
The config flow handler will need to add a step to support the source. The step
|
||||
should follow the same return values as a normal step.
|
||||
@@ -113,7 +117,7 @@ the flow from the config panel.
|
||||
|
||||
import logging
|
||||
import uuid
|
||||
from typing import Set, Optional # noqa pylint: disable=unused-import
|
||||
from typing import Set, Optional, List # noqa pylint: disable=unused-import
|
||||
|
||||
from homeassistant import data_entry_flow
|
||||
from homeassistant.core import callback, HomeAssistant
|
||||
@@ -123,6 +127,11 @@ from homeassistant.util.decorator import Registry
|
||||
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SOURCE_USER = 'user'
|
||||
SOURCE_DISCOVERY = 'discovery'
|
||||
SOURCE_IMPORT = 'import'
|
||||
|
||||
HANDLERS = Registry()
|
||||
# Components that have config flows. In future we will auto-generate this list.
|
||||
FLOWS = [
|
||||
@@ -151,8 +160,8 @@ ENTRY_STATE_FAILED_UNLOAD = 'failed_unload'
|
||||
|
||||
DISCOVERY_NOTIFICATION_ID = 'config_entry_discovery'
|
||||
DISCOVERY_SOURCES = (
|
||||
data_entry_flow.SOURCE_DISCOVERY,
|
||||
data_entry_flow.SOURCE_IMPORT,
|
||||
SOURCE_DISCOVERY,
|
||||
SOURCE_IMPORT,
|
||||
)
|
||||
|
||||
EVENT_FLOW_DISCOVERED = 'config_entry_discovered'
|
||||
@@ -270,19 +279,19 @@ class ConfigEntries:
|
||||
An instance of this object is available via `hass.config_entries`.
|
||||
"""
|
||||
|
||||
def __init__(self, hass, hass_config):
|
||||
def __init__(self, hass: HomeAssistant, hass_config: dict) -> None:
|
||||
"""Initialize the entry manager."""
|
||||
self.hass = hass
|
||||
self.flow = data_entry_flow.FlowManager(
|
||||
hass, self._async_create_flow, self._async_finish_flow)
|
||||
self._hass_config = hass_config
|
||||
self._entries = None
|
||||
self._entries = [] # type: List[ConfigEntry]
|
||||
self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY)
|
||||
|
||||
@callback
|
||||
def async_domains(self):
|
||||
def async_domains(self) -> List[str]:
|
||||
"""Return domains for which we have entries."""
|
||||
seen = set() # type: Set[ConfigEntry]
|
||||
seen = set() # type: Set[str]
|
||||
result = []
|
||||
|
||||
for entry in self._entries:
|
||||
@@ -293,7 +302,7 @@ class ConfigEntries:
|
||||
return result
|
||||
|
||||
@callback
|
||||
def async_entries(self, domain=None):
|
||||
def async_entries(self, domain: str = None) -> List[ConfigEntry]:
|
||||
"""Return all entries or entries for a specific domain."""
|
||||
if domain is None:
|
||||
return list(self._entries)
|
||||
@@ -319,7 +328,7 @@ class ConfigEntries:
|
||||
'require_restart': not unloaded
|
||||
}
|
||||
|
||||
async def async_load(self):
|
||||
async def async_load(self) -> None:
|
||||
"""Handle loading the config."""
|
||||
# Migrating for config entries stored before 0.73
|
||||
config = await self.hass.helpers.storage.async_migrator(
|
||||
@@ -374,12 +383,15 @@ class ConfigEntries:
|
||||
if result['type'] != data_entry_flow.RESULT_TYPE_CREATE_ENTRY:
|
||||
return None
|
||||
|
||||
source = result['source']
|
||||
if source is None:
|
||||
source = SOURCE_USER
|
||||
entry = ConfigEntry(
|
||||
version=result['version'],
|
||||
domain=result['handler'],
|
||||
title=result['title'],
|
||||
data=result['data'],
|
||||
source=result['source'],
|
||||
source=source,
|
||||
)
|
||||
self._entries.append(entry)
|
||||
await self._async_schedule_save()
|
||||
@@ -399,17 +411,22 @@ class ConfigEntries:
|
||||
|
||||
return entry
|
||||
|
||||
async def _async_create_flow(self, handler, *, source, data):
|
||||
async def _async_create_flow(self, handler_key, *, context, data):
|
||||
"""Create a flow for specified handler.
|
||||
|
||||
Handler key is the domain of the component that we want to setup.
|
||||
"""
|
||||
component = getattr(self.hass.components, handler)
|
||||
handler = HANDLERS.get(handler)
|
||||
component = getattr(self.hass.components, handler_key)
|
||||
handler = HANDLERS.get(handler_key)
|
||||
|
||||
if handler is None:
|
||||
raise data_entry_flow.UnknownHandler
|
||||
|
||||
if context is not None:
|
||||
source = context.get('source', SOURCE_USER)
|
||||
else:
|
||||
source = SOURCE_USER
|
||||
|
||||
# Make sure requirements and dependencies of component are resolved
|
||||
await async_process_deps_reqs(
|
||||
self.hass, self._hass_config, handler, component)
|
||||
@@ -424,7 +441,10 @@ class ConfigEntries:
|
||||
notification_id=DISCOVERY_NOTIFICATION_ID
|
||||
)
|
||||
|
||||
return handler()
|
||||
flow = handler()
|
||||
flow.source = source
|
||||
flow.init_step = source
|
||||
return flow
|
||||
|
||||
async def _async_schedule_save(self):
|
||||
"""Save the entity registry to a file."""
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# coding: utf-8
|
||||
"""Constants used by Home Assistant components."""
|
||||
MAJOR_VERSION = 0
|
||||
MINOR_VERSION = 75
|
||||
PATCH_VERSION = '1'
|
||||
MINOR_VERSION = 76
|
||||
PATCH_VERSION = '0b1'
|
||||
__short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION)
|
||||
__version__ = '{}.{}'.format(__short_version__, PATCH_VERSION)
|
||||
REQUIRED_PYTHON_VER = (3, 5, 3)
|
||||
|
||||
@@ -423,7 +423,8 @@ class Event:
|
||||
self.event_type == other.event_type and
|
||||
self.data == other.data and
|
||||
self.origin == other.origin and
|
||||
self.time_fired == other.time_fired)
|
||||
self.time_fired == other.time_fired and
|
||||
self.context == other.context)
|
||||
|
||||
|
||||
class EventBus:
|
||||
@@ -695,7 +696,8 @@ class State:
|
||||
return (self.__class__ == other.__class__ and # type: ignore
|
||||
self.entity_id == other.entity_id and
|
||||
self.state == other.state and
|
||||
self.attributes == other.attributes)
|
||||
self.attributes == other.attributes and
|
||||
self.context == other.context)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""Return the representation of the states."""
|
||||
|
||||
@@ -2,16 +2,12 @@
|
||||
import logging
|
||||
import uuid
|
||||
import voluptuous as vol
|
||||
from typing import Dict, Any, Callable, List, Optional # noqa pylint: disable=unused-import
|
||||
from typing import Dict, Any, Callable, Hashable, List, Optional # noqa pylint: disable=unused-import
|
||||
from .core import callback, HomeAssistant
|
||||
from .exceptions import HomeAssistantError
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SOURCE_USER = 'user'
|
||||
SOURCE_DISCOVERY = 'discovery'
|
||||
SOURCE_IMPORT = 'import'
|
||||
|
||||
RESULT_TYPE_FORM = 'form'
|
||||
RESULT_TYPE_CREATE_ENTRY = 'create_entry'
|
||||
RESULT_TYPE_ABORT = 'abort'
|
||||
@@ -53,22 +49,17 @@ class FlowManager:
|
||||
'source': flow.source,
|
||||
} for flow in self._progress.values()]
|
||||
|
||||
async def async_init(self, handler: Callable, *, source: str = SOURCE_USER,
|
||||
data: str = None) -> Any:
|
||||
async def async_init(self, handler: Hashable, *, context: Dict = None,
|
||||
data: Any = None) -> Any:
|
||||
"""Start a configuration flow."""
|
||||
flow = await self._async_create_flow(handler, source=source, data=data)
|
||||
flow = await self._async_create_flow(
|
||||
handler, context=context, data=data)
|
||||
flow.hass = self.hass
|
||||
flow.handler = handler
|
||||
flow.flow_id = uuid.uuid4().hex
|
||||
flow.source = source
|
||||
self._progress[flow.flow_id] = flow
|
||||
|
||||
if source == SOURCE_USER:
|
||||
step = 'init'
|
||||
else:
|
||||
step = source
|
||||
|
||||
return await self._async_handle_step(flow, step, data)
|
||||
return await self._async_handle_step(flow, flow.init_step, data)
|
||||
|
||||
async def async_configure(
|
||||
self, flow_id: str, user_input: str = None) -> Any:
|
||||
@@ -131,9 +122,12 @@ class FlowHandler:
|
||||
flow_id = None
|
||||
hass = None
|
||||
handler = None
|
||||
source = SOURCE_USER
|
||||
source = None
|
||||
cur_step = None
|
||||
|
||||
# Set by _async_create_flow callback
|
||||
init_step = 'init'
|
||||
|
||||
# Set by developer
|
||||
VERSION = 1
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ class DiscoveryFlowHandler(data_entry_flow.FlowHandler):
|
||||
self._title = title
|
||||
self._discovery_function = discovery_function
|
||||
|
||||
async def async_step_init(self, user_input=None):
|
||||
async def async_step_user(self, user_input=None):
|
||||
"""Handle a flow initialized by the user."""
|
||||
if self._async_current_entries():
|
||||
return self.async_abort(
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import data_entry_flow
|
||||
from homeassistant import data_entry_flow, config_entries
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
from homeassistant.components.http.data_validator import RequestDataValidator
|
||||
|
||||
@@ -53,7 +53,8 @@ class FlowManagerIndexView(_BaseFlowManagerView):
|
||||
handler = data['handler']
|
||||
|
||||
try:
|
||||
result = await self._flow_mgr.async_init(handler)
|
||||
result = await self._flow_mgr.async_init(
|
||||
handler, context={'source': config_entries.SOURCE_USER})
|
||||
except data_entry_flow.UnknownHandler:
|
||||
return self.json_message('Invalid handler specified', 404)
|
||||
except data_entry_flow.UnknownStep:
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
from collections import OrderedDict
|
||||
import fnmatch
|
||||
import re
|
||||
from typing import Dict
|
||||
|
||||
from homeassistant.core import split_entity_id
|
||||
|
||||
@@ -9,7 +10,8 @@ from homeassistant.core import split_entity_id
|
||||
class EntityValues:
|
||||
"""Class to store entity id based values."""
|
||||
|
||||
def __init__(self, exact=None, domain=None, glob=None):
|
||||
def __init__(self, exact: Dict = None, domain: Dict = None,
|
||||
glob: Dict = None) -> None:
|
||||
"""Initialize an EntityConfigDict."""
|
||||
self._cache = {}
|
||||
self._exact = exact
|
||||
|
||||
@@ -3,7 +3,7 @@ import logging
|
||||
import signal
|
||||
import sys
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.core import callback, HomeAssistant
|
||||
from homeassistant.const import RESTART_EXIT_CODE
|
||||
from homeassistant.loader import bind_hass
|
||||
|
||||
@@ -12,13 +12,13 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@callback
|
||||
@bind_hass
|
||||
def async_register_signal_handling(hass):
|
||||
def async_register_signal_handling(hass: HomeAssistant) -> None:
|
||||
"""Register system signal handler for core."""
|
||||
if sys.platform != 'win32':
|
||||
@callback
|
||||
def async_signal_handle(exit_code):
|
||||
"""Wrap signal handling."""
|
||||
hass.async_add_job(hass.async_stop(exit_code))
|
||||
hass.async_create_task(hass.async_stop(exit_code))
|
||||
|
||||
try:
|
||||
hass.loop.add_signal_handler(
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user