mirror of
https://github.com/home-assistant/core.git
synced 2025-07-21 12:17:07 +00:00
commit
c5cac04e54
14
.coveragerc
14
.coveragerc
@ -29,7 +29,7 @@ omit =
|
|||||||
homeassistant/components/arduino.py
|
homeassistant/components/arduino.py
|
||||||
homeassistant/components/*/arduino.py
|
homeassistant/components/*/arduino.py
|
||||||
|
|
||||||
homeassistant/components/bmw_connected_drive.py
|
homeassistant/components/bmw_connected_drive/*.py
|
||||||
homeassistant/components/*/bmw_connected_drive.py
|
homeassistant/components/*/bmw_connected_drive.py
|
||||||
|
|
||||||
homeassistant/components/android_ip_webcam.py
|
homeassistant/components/android_ip_webcam.py
|
||||||
@ -166,6 +166,9 @@ omit =
|
|||||||
homeassistant/components/mailgun.py
|
homeassistant/components/mailgun.py
|
||||||
homeassistant/components/*/mailgun.py
|
homeassistant/components/*/mailgun.py
|
||||||
|
|
||||||
|
homeassistant/components/matrix.py
|
||||||
|
homeassistant/components/*/matrix.py
|
||||||
|
|
||||||
homeassistant/components/maxcube.py
|
homeassistant/components/maxcube.py
|
||||||
homeassistant/components/*/maxcube.py
|
homeassistant/components/*/maxcube.py
|
||||||
|
|
||||||
@ -208,6 +211,9 @@ omit =
|
|||||||
homeassistant/components/raincloud.py
|
homeassistant/components/raincloud.py
|
||||||
homeassistant/components/*/raincloud.py
|
homeassistant/components/*/raincloud.py
|
||||||
|
|
||||||
|
homeassistant/components/rainmachine.py
|
||||||
|
homeassistant/components/*/rainmachine.py
|
||||||
|
|
||||||
homeassistant/components/raspihats.py
|
homeassistant/components/raspihats.py
|
||||||
homeassistant/components/*/raspihats.py
|
homeassistant/components/*/raspihats.py
|
||||||
|
|
||||||
@ -516,7 +522,6 @@ omit =
|
|||||||
homeassistant/components/notify/lannouncer.py
|
homeassistant/components/notify/lannouncer.py
|
||||||
homeassistant/components/notify/llamalab_automate.py
|
homeassistant/components/notify/llamalab_automate.py
|
||||||
homeassistant/components/notify/mastodon.py
|
homeassistant/components/notify/mastodon.py
|
||||||
homeassistant/components/notify/matrix.py
|
|
||||||
homeassistant/components/notify/message_bird.py
|
homeassistant/components/notify/message_bird.py
|
||||||
homeassistant/components/notify/mycroft.py
|
homeassistant/components/notify/mycroft.py
|
||||||
homeassistant/components/notify/nfandroidtv.py
|
homeassistant/components/notify/nfandroidtv.py
|
||||||
@ -574,6 +579,7 @@ omit =
|
|||||||
homeassistant/components/sensor/discogs.py
|
homeassistant/components/sensor/discogs.py
|
||||||
homeassistant/components/sensor/dnsip.py
|
homeassistant/components/sensor/dnsip.py
|
||||||
homeassistant/components/sensor/dovado.py
|
homeassistant/components/sensor/dovado.py
|
||||||
|
homeassistant/components/sensor/domain_expiry.py
|
||||||
homeassistant/components/sensor/dte_energy_bridge.py
|
homeassistant/components/sensor/dte_energy_bridge.py
|
||||||
homeassistant/components/sensor/dublin_bus_transport.py
|
homeassistant/components/sensor/dublin_bus_transport.py
|
||||||
homeassistant/components/sensor/dwd_weather_warnings.py
|
homeassistant/components/sensor/dwd_weather_warnings.py
|
||||||
@ -615,6 +621,7 @@ omit =
|
|||||||
homeassistant/components/sensor/lyft.py
|
homeassistant/components/sensor/lyft.py
|
||||||
homeassistant/components/sensor/metoffice.py
|
homeassistant/components/sensor/metoffice.py
|
||||||
homeassistant/components/sensor/miflora.py
|
homeassistant/components/sensor/miflora.py
|
||||||
|
homeassistant/components/sensor/mitemp_bt.py
|
||||||
homeassistant/components/sensor/modem_callerid.py
|
homeassistant/components/sensor/modem_callerid.py
|
||||||
homeassistant/components/sensor/mopar.py
|
homeassistant/components/sensor/mopar.py
|
||||||
homeassistant/components/sensor/mqtt_room.py
|
homeassistant/components/sensor/mqtt_room.py
|
||||||
@ -635,6 +642,7 @@ omit =
|
|||||||
homeassistant/components/sensor/plex.py
|
homeassistant/components/sensor/plex.py
|
||||||
homeassistant/components/sensor/pocketcasts.py
|
homeassistant/components/sensor/pocketcasts.py
|
||||||
homeassistant/components/sensor/pollen.py
|
homeassistant/components/sensor/pollen.py
|
||||||
|
homeassistant/components/sensor/postnl.py
|
||||||
homeassistant/components/sensor/pushbullet.py
|
homeassistant/components/sensor/pushbullet.py
|
||||||
homeassistant/components/sensor/pvoutput.py
|
homeassistant/components/sensor/pvoutput.py
|
||||||
homeassistant/components/sensor/pyload.py
|
homeassistant/components/sensor/pyload.py
|
||||||
@ -656,6 +664,7 @@ omit =
|
|||||||
homeassistant/components/sensor/sma.py
|
homeassistant/components/sensor/sma.py
|
||||||
homeassistant/components/sensor/snmp.py
|
homeassistant/components/sensor/snmp.py
|
||||||
homeassistant/components/sensor/sochain.py
|
homeassistant/components/sensor/sochain.py
|
||||||
|
homeassistant/components/sensor/socialblade.py
|
||||||
homeassistant/components/sensor/sonarr.py
|
homeassistant/components/sensor/sonarr.py
|
||||||
homeassistant/components/sensor/speedtest.py
|
homeassistant/components/sensor/speedtest.py
|
||||||
homeassistant/components/sensor/spotcrime.py
|
homeassistant/components/sensor/spotcrime.py
|
||||||
@ -710,7 +719,6 @@ omit =
|
|||||||
homeassistant/components/switch/orvibo.py
|
homeassistant/components/switch/orvibo.py
|
||||||
homeassistant/components/switch/pulseaudio_loopback.py
|
homeassistant/components/switch/pulseaudio_loopback.py
|
||||||
homeassistant/components/switch/rainbird.py
|
homeassistant/components/switch/rainbird.py
|
||||||
homeassistant/components/switch/rainmachine.py
|
|
||||||
homeassistant/components/switch/rest.py
|
homeassistant/components/switch/rest.py
|
||||||
homeassistant/components/switch/rpi_rf.py
|
homeassistant/components/switch/rpi_rf.py
|
||||||
homeassistant/components/switch/snmp.py
|
homeassistant/components/switch/snmp.py
|
||||||
|
50
.github/ISSUE_TEMPLATE/Bug_report.md
vendored
Normal file
50
.github/ISSUE_TEMPLATE/Bug_report.md
vendored
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
---
|
||||||
|
name: Bug report
|
||||||
|
about: Create a report to help us improve
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- READ THIS FIRST:
|
||||||
|
- If you need additional help with this template please refer to https://www.home-assistant.io/help/reporting_issues/
|
||||||
|
- Make sure you are running the latest version of Home Assistant before reporting an issue: https://github.com/home-assistant/home-assistant/releases
|
||||||
|
- Do not report issues for components if you are using custom components: files in <config-dir>/custom_components
|
||||||
|
- This is for bugs only. Feature and enhancement requests should go in our community forum: https://community.home-assistant.io/c/feature-requests
|
||||||
|
- Provide as many details as possible. Paste logs, configuration sample and code into the backticks. Do not delete any text from this template!
|
||||||
|
-->
|
||||||
|
|
||||||
|
**Home Assistant release with the issue:**
|
||||||
|
<!--
|
||||||
|
- Frontend -> Developer tools -> Info
|
||||||
|
- Or use this command: hass --version
|
||||||
|
-->
|
||||||
|
|
||||||
|
|
||||||
|
**Last working Home Assistant release (if known):**
|
||||||
|
|
||||||
|
|
||||||
|
**Operating environment (Hass.io/Docker/Windows/etc.):**
|
||||||
|
<!--
|
||||||
|
Please provide details about your environment.
|
||||||
|
-->
|
||||||
|
|
||||||
|
**Component/platform:**
|
||||||
|
<!--
|
||||||
|
Please add the link to the documentation at https://www.home-assistant.io/components/ of the component/platform in question.
|
||||||
|
-->
|
||||||
|
|
||||||
|
|
||||||
|
**Description of problem:**
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
**Problem-relevant `configuration.yaml` entries and (fill out even if it seems unimportant):**
|
||||||
|
```yaml
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
**Traceback (if applicable):**
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
**Additional information:**
|
@ -54,8 +54,11 @@ homeassistant/components/device_tracker/tile.py @bachya
|
|||||||
homeassistant/components/history_graph.py @andrey-git
|
homeassistant/components/history_graph.py @andrey-git
|
||||||
homeassistant/components/light/tplink.py @rytilahti
|
homeassistant/components/light/tplink.py @rytilahti
|
||||||
homeassistant/components/light/yeelight.py @rytilahti
|
homeassistant/components/light/yeelight.py @rytilahti
|
||||||
|
homeassistant/components/lock/nello.py @pschmitt
|
||||||
|
homeassistant/components/lock/nuki.py @pschmitt
|
||||||
homeassistant/components/media_player/emby.py @mezz64
|
homeassistant/components/media_player/emby.py @mezz64
|
||||||
homeassistant/components/media_player/kodi.py @armills
|
homeassistant/components/media_player/kodi.py @armills
|
||||||
|
homeassistant/components/media_player/liveboxplaytv.py @pschmitt
|
||||||
homeassistant/components/media_player/mediaroom.py @dgomes
|
homeassistant/components/media_player/mediaroom.py @dgomes
|
||||||
homeassistant/components/media_player/monoprice.py @etsinko
|
homeassistant/components/media_player/monoprice.py @etsinko
|
||||||
homeassistant/components/media_player/sonos.py @amelchio
|
homeassistant/components/media_player/sonos.py @amelchio
|
||||||
@ -77,6 +80,7 @@ homeassistant/components/sensor/upnp.py @dgomes
|
|||||||
homeassistant/components/sensor/waqi.py @andrey-git
|
homeassistant/components/sensor/waqi.py @andrey-git
|
||||||
homeassistant/components/switch/rainmachine.py @bachya
|
homeassistant/components/switch/rainmachine.py @bachya
|
||||||
homeassistant/components/switch/tplink.py @rytilahti
|
homeassistant/components/switch/tplink.py @rytilahti
|
||||||
|
homeassistant/components/vacuum/roomba.py @pschmitt
|
||||||
homeassistant/components/xiaomi_aqara.py @danielhiversen @syssi
|
homeassistant/components/xiaomi_aqara.py @danielhiversen @syssi
|
||||||
|
|
||||||
homeassistant/components/*/axis.py @kane610
|
homeassistant/components/*/axis.py @kane610
|
||||||
@ -90,6 +94,8 @@ homeassistant/components/*/hive.py @Rendili @KJonline
|
|||||||
homeassistant/components/homekit/* @cdce8p
|
homeassistant/components/homekit/* @cdce8p
|
||||||
homeassistant/components/knx.py @Julius2342
|
homeassistant/components/knx.py @Julius2342
|
||||||
homeassistant/components/*/knx.py @Julius2342
|
homeassistant/components/*/knx.py @Julius2342
|
||||||
|
homeassistant/components/matrix.py @tinloaf
|
||||||
|
homeassistant/components/*/matrix.py @tinloaf
|
||||||
homeassistant/components/qwikswitch.py @kellerza
|
homeassistant/components/qwikswitch.py @kellerza
|
||||||
homeassistant/components/*/qwikswitch.py @kellerza
|
homeassistant/components/*/qwikswitch.py @kellerza
|
||||||
homeassistant/components/*/rfxtrx.py @danielhiversen
|
homeassistant/components/*/rfxtrx.py @danielhiversen
|
||||||
|
505
homeassistant/auth.py
Normal file
505
homeassistant/auth.py
Normal file
@ -0,0 +1,505 @@
|
|||||||
|
"""Provide an authentication layer for Home Assistant."""
|
||||||
|
import asyncio
|
||||||
|
import binascii
|
||||||
|
from collections import OrderedDict
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
import os
|
||||||
|
import importlib
|
||||||
|
import logging
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
import attr
|
||||||
|
import voluptuous as vol
|
||||||
|
from voluptuous.humanize import humanize_error
|
||||||
|
|
||||||
|
from homeassistant import data_entry_flow, requirements
|
||||||
|
from homeassistant.core import callback
|
||||||
|
from homeassistant.const import CONF_TYPE, CONF_NAME, CONF_ID
|
||||||
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
|
from homeassistant.util.decorator import Registry
|
||||||
|
from homeassistant.util import dt as dt_util
|
||||||
|
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
AUTH_PROVIDERS = Registry()
|
||||||
|
|
||||||
|
AUTH_PROVIDER_SCHEMA = vol.Schema({
|
||||||
|
vol.Required(CONF_TYPE): str,
|
||||||
|
vol.Optional(CONF_NAME): str,
|
||||||
|
# Specify ID if you have two auth providers for same type.
|
||||||
|
vol.Optional(CONF_ID): str,
|
||||||
|
}, extra=vol.ALLOW_EXTRA)
|
||||||
|
|
||||||
|
ACCESS_TOKEN_EXPIRATION = timedelta(minutes=30)
|
||||||
|
DATA_REQS = 'auth_reqs_processed'
|
||||||
|
|
||||||
|
|
||||||
|
class AuthError(HomeAssistantError):
|
||||||
|
"""Generic authentication error."""
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidUser(AuthError):
|
||||||
|
"""Raised when an invalid user has been specified."""
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidPassword(AuthError):
|
||||||
|
"""Raised when an invalid password has been supplied."""
|
||||||
|
|
||||||
|
|
||||||
|
class UnknownError(AuthError):
|
||||||
|
"""When an unknown error occurs."""
|
||||||
|
|
||||||
|
|
||||||
|
def generate_secret(entropy=32):
|
||||||
|
"""Generate a secret.
|
||||||
|
|
||||||
|
Backport of secrets.token_hex from Python 3.6
|
||||||
|
|
||||||
|
Event loop friendly.
|
||||||
|
"""
|
||||||
|
return binascii.hexlify(os.urandom(entropy)).decode('ascii')
|
||||||
|
|
||||||
|
|
||||||
|
class AuthProvider:
|
||||||
|
"""Provider of user authentication."""
|
||||||
|
|
||||||
|
DEFAULT_TITLE = 'Unnamed auth provider'
|
||||||
|
|
||||||
|
initialized = False
|
||||||
|
|
||||||
|
def __init__(self, store, config):
|
||||||
|
"""Initialize an auth provider."""
|
||||||
|
self.store = store
|
||||||
|
self.config = config
|
||||||
|
|
||||||
|
@property
|
||||||
|
def id(self): # pylint: disable=invalid-name
|
||||||
|
"""Return id of the auth provider.
|
||||||
|
|
||||||
|
Optional, can be None.
|
||||||
|
"""
|
||||||
|
return self.config.get(CONF_ID)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def type(self):
|
||||||
|
"""Return type of the provider."""
|
||||||
|
return self.config[CONF_TYPE]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
"""Return the name of the auth provider."""
|
||||||
|
return self.config.get(CONF_NAME, self.DEFAULT_TITLE)
|
||||||
|
|
||||||
|
async def async_credentials(self):
|
||||||
|
"""Return all credentials of this provider."""
|
||||||
|
return await self.store.credentials_for_provider(self.type, self.id)
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_create_credentials(self, data):
|
||||||
|
"""Create credentials."""
|
||||||
|
return Credentials(
|
||||||
|
auth_provider_type=self.type,
|
||||||
|
auth_provider_id=self.id,
|
||||||
|
data=data,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Implement by extending class
|
||||||
|
|
||||||
|
async def async_initialize(self):
|
||||||
|
"""Initialize the auth provider.
|
||||||
|
|
||||||
|
Optional.
|
||||||
|
"""
|
||||||
|
|
||||||
|
async def async_credential_flow(self):
|
||||||
|
"""Return the data flow for logging in with auth provider."""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
async def async_get_or_create_credentials(self, flow_result):
|
||||||
|
"""Get credentials based on the flow result."""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
async def async_user_meta_for_credentials(self, credentials):
|
||||||
|
"""Return extra user metadata for credentials.
|
||||||
|
|
||||||
|
Will be used to populate info when creating a new user.
|
||||||
|
"""
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
@attr.s(slots=True)
|
||||||
|
class User:
|
||||||
|
"""A user."""
|
||||||
|
|
||||||
|
id = attr.ib(type=str, default=attr.Factory(lambda: uuid.uuid4().hex))
|
||||||
|
is_owner = attr.ib(type=bool, default=False)
|
||||||
|
is_active = attr.ib(type=bool, default=False)
|
||||||
|
name = attr.ib(type=str, default=None)
|
||||||
|
# For persisting and see if saved?
|
||||||
|
# store = attr.ib(type=AuthStore, default=None)
|
||||||
|
|
||||||
|
# List of credentials of a user.
|
||||||
|
credentials = attr.ib(type=list, default=attr.Factory(list))
|
||||||
|
|
||||||
|
# Tokens associated with a user.
|
||||||
|
refresh_tokens = attr.ib(type=dict, default=attr.Factory(dict))
|
||||||
|
|
||||||
|
def as_dict(self):
|
||||||
|
"""Convert user object to a dictionary."""
|
||||||
|
return {
|
||||||
|
'id': self.id,
|
||||||
|
'is_owner': self.is_owner,
|
||||||
|
'is_active': self.is_active,
|
||||||
|
'name': self.name,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@attr.s(slots=True)
|
||||||
|
class RefreshToken:
|
||||||
|
"""RefreshToken for a user to grant new access tokens."""
|
||||||
|
|
||||||
|
user = attr.ib(type=User)
|
||||||
|
client_id = attr.ib(type=str)
|
||||||
|
id = attr.ib(type=str, default=attr.Factory(lambda: uuid.uuid4().hex))
|
||||||
|
created_at = attr.ib(type=datetime, default=attr.Factory(dt_util.utcnow))
|
||||||
|
access_token_expiration = attr.ib(type=timedelta,
|
||||||
|
default=ACCESS_TOKEN_EXPIRATION)
|
||||||
|
token = attr.ib(type=str,
|
||||||
|
default=attr.Factory(lambda: generate_secret(64)))
|
||||||
|
access_tokens = attr.ib(type=list, default=attr.Factory(list))
|
||||||
|
|
||||||
|
|
||||||
|
@attr.s(slots=True)
|
||||||
|
class AccessToken:
|
||||||
|
"""Access token to access the API.
|
||||||
|
|
||||||
|
These will only ever be stored in memory and not be persisted.
|
||||||
|
"""
|
||||||
|
|
||||||
|
refresh_token = attr.ib(type=RefreshToken)
|
||||||
|
created_at = attr.ib(type=datetime, default=attr.Factory(dt_util.utcnow))
|
||||||
|
token = attr.ib(type=str,
|
||||||
|
default=attr.Factory(generate_secret))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def expires(self):
|
||||||
|
"""Return datetime when this token expires."""
|
||||||
|
return self.created_at + self.refresh_token.access_token_expiration
|
||||||
|
|
||||||
|
|
||||||
|
@attr.s(slots=True)
|
||||||
|
class Credentials:
|
||||||
|
"""Credentials for a user on an auth provider."""
|
||||||
|
|
||||||
|
auth_provider_type = attr.ib(type=str)
|
||||||
|
auth_provider_id = attr.ib(type=str)
|
||||||
|
|
||||||
|
# Allow the auth provider to store data to represent their auth.
|
||||||
|
data = attr.ib(type=dict)
|
||||||
|
|
||||||
|
id = attr.ib(type=str, default=attr.Factory(lambda: uuid.uuid4().hex))
|
||||||
|
is_new = attr.ib(type=bool, default=True)
|
||||||
|
|
||||||
|
|
||||||
|
@attr.s(slots=True)
|
||||||
|
class Client:
|
||||||
|
"""Client that interacts with Home Assistant on behalf of a user."""
|
||||||
|
|
||||||
|
name = attr.ib(type=str)
|
||||||
|
id = attr.ib(type=str, default=attr.Factory(lambda: uuid.uuid4().hex))
|
||||||
|
secret = attr.ib(type=str, default=attr.Factory(generate_secret))
|
||||||
|
|
||||||
|
|
||||||
|
async def load_auth_provider_module(hass, provider):
|
||||||
|
"""Load an auth provider."""
|
||||||
|
try:
|
||||||
|
module = importlib.import_module(
|
||||||
|
'homeassistant.auth_providers.{}'.format(provider))
|
||||||
|
except ImportError:
|
||||||
|
_LOGGER.warning('Unable to find auth provider %s', provider)
|
||||||
|
return None
|
||||||
|
|
||||||
|
if hass.config.skip_pip or not hasattr(module, 'REQUIREMENTS'):
|
||||||
|
return module
|
||||||
|
|
||||||
|
processed = hass.data.get(DATA_REQS)
|
||||||
|
|
||||||
|
if processed is None:
|
||||||
|
processed = hass.data[DATA_REQS] = set()
|
||||||
|
elif provider in processed:
|
||||||
|
return module
|
||||||
|
|
||||||
|
req_success = await requirements.async_process_requirements(
|
||||||
|
hass, 'auth provider {}'.format(provider), module.REQUIREMENTS)
|
||||||
|
|
||||||
|
if not req_success:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return module
|
||||||
|
|
||||||
|
|
||||||
|
async def auth_manager_from_config(hass, provider_configs):
|
||||||
|
"""Initialize an auth manager from config."""
|
||||||
|
store = AuthStore(hass)
|
||||||
|
if provider_configs:
|
||||||
|
providers = await asyncio.gather(
|
||||||
|
*[_auth_provider_from_config(hass, store, config)
|
||||||
|
for config in provider_configs])
|
||||||
|
else:
|
||||||
|
providers = []
|
||||||
|
# So returned auth providers are in same order as config
|
||||||
|
provider_hash = OrderedDict()
|
||||||
|
for provider in providers:
|
||||||
|
if provider is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
key = (provider.type, provider.id)
|
||||||
|
|
||||||
|
if key in provider_hash:
|
||||||
|
_LOGGER.error(
|
||||||
|
'Found duplicate provider: %s. Please add unique IDs if you '
|
||||||
|
'want to have the same provider twice.', key)
|
||||||
|
continue
|
||||||
|
|
||||||
|
provider_hash[key] = provider
|
||||||
|
manager = AuthManager(hass, store, provider_hash)
|
||||||
|
return manager
|
||||||
|
|
||||||
|
|
||||||
|
async def _auth_provider_from_config(hass, store, config):
|
||||||
|
"""Initialize an auth provider from a config."""
|
||||||
|
provider_name = config[CONF_TYPE]
|
||||||
|
module = await load_auth_provider_module(hass, provider_name)
|
||||||
|
|
||||||
|
if module is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
config = module.CONFIG_SCHEMA(config)
|
||||||
|
except vol.Invalid as err:
|
||||||
|
_LOGGER.error('Invalid configuration for auth provider %s: %s',
|
||||||
|
provider_name, humanize_error(config, err))
|
||||||
|
return None
|
||||||
|
|
||||||
|
return AUTH_PROVIDERS[provider_name](store, config)
|
||||||
|
|
||||||
|
|
||||||
|
class AuthManager:
|
||||||
|
"""Manage the authentication for Home Assistant."""
|
||||||
|
|
||||||
|
def __init__(self, hass, store, providers):
|
||||||
|
"""Initialize the auth manager."""
|
||||||
|
self._store = store
|
||||||
|
self._providers = providers
|
||||||
|
self.login_flow = data_entry_flow.FlowManager(
|
||||||
|
hass, self._async_create_login_flow,
|
||||||
|
self._async_finish_login_flow)
|
||||||
|
self.access_tokens = {}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def async_auth_providers(self):
|
||||||
|
"""Return a list of available auth providers."""
|
||||||
|
return self._providers.values()
|
||||||
|
|
||||||
|
async def async_get_user(self, user_id):
|
||||||
|
"""Retrieve a user."""
|
||||||
|
return await self._store.async_get_user(user_id)
|
||||||
|
|
||||||
|
async def async_get_or_create_user(self, credentials):
|
||||||
|
"""Get or create a user."""
|
||||||
|
return await self._store.async_get_or_create_user(
|
||||||
|
credentials, self._async_get_auth_provider(credentials))
|
||||||
|
|
||||||
|
async def async_link_user(self, user, credentials):
|
||||||
|
"""Link credentials to an existing user."""
|
||||||
|
await self._store.async_link_user(user, credentials)
|
||||||
|
|
||||||
|
async def async_remove_user(self, user):
|
||||||
|
"""Remove a user."""
|
||||||
|
await self._store.async_remove_user(user)
|
||||||
|
|
||||||
|
async def async_create_refresh_token(self, user, client_id):
|
||||||
|
"""Create a new refresh token for a user."""
|
||||||
|
return await self._store.async_create_refresh_token(user, client_id)
|
||||||
|
|
||||||
|
async def async_get_refresh_token(self, token):
|
||||||
|
"""Get refresh token by token."""
|
||||||
|
return await self._store.async_get_refresh_token(token)
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_create_access_token(self, refresh_token):
|
||||||
|
"""Create a new access token."""
|
||||||
|
access_token = AccessToken(refresh_token)
|
||||||
|
self.access_tokens[access_token.token] = access_token
|
||||||
|
return access_token
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_get_access_token(self, token):
|
||||||
|
"""Get an access token."""
|
||||||
|
return self.access_tokens.get(token)
|
||||||
|
|
||||||
|
async def async_create_client(self, name):
|
||||||
|
"""Create a new client."""
|
||||||
|
return await self._store.async_create_client(name)
|
||||||
|
|
||||||
|
async def async_get_client(self, client_id):
|
||||||
|
"""Get a client."""
|
||||||
|
return await self._store.async_get_client(client_id)
|
||||||
|
|
||||||
|
async def _async_create_login_flow(self, handler, *, source, data):
|
||||||
|
"""Create a login flow."""
|
||||||
|
auth_provider = self._providers[handler]
|
||||||
|
|
||||||
|
if not auth_provider.initialized:
|
||||||
|
auth_provider.initialized = True
|
||||||
|
await auth_provider.async_initialize()
|
||||||
|
|
||||||
|
return await auth_provider.async_credential_flow()
|
||||||
|
|
||||||
|
async def _async_finish_login_flow(self, result):
|
||||||
|
"""Result of a credential login flow."""
|
||||||
|
auth_provider = self._providers[result['handler']]
|
||||||
|
return await auth_provider.async_get_or_create_credentials(
|
||||||
|
result['data'])
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _async_get_auth_provider(self, credentials):
|
||||||
|
"""Helper to get auth provider from a set of credentials."""
|
||||||
|
auth_provider_key = (credentials.auth_provider_type,
|
||||||
|
credentials.auth_provider_id)
|
||||||
|
return self._providers[auth_provider_key]
|
||||||
|
|
||||||
|
|
||||||
|
class AuthStore:
|
||||||
|
"""Stores authentication info.
|
||||||
|
|
||||||
|
Any mutation to an object should happen inside the auth store.
|
||||||
|
|
||||||
|
The auth store is lazy. It won't load the data from disk until a method is
|
||||||
|
called that needs it.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, hass):
|
||||||
|
"""Initialize the auth store."""
|
||||||
|
self.hass = hass
|
||||||
|
self.users = None
|
||||||
|
self.clients = None
|
||||||
|
self._load_lock = asyncio.Lock(loop=hass.loop)
|
||||||
|
|
||||||
|
async def credentials_for_provider(self, provider_type, provider_id):
|
||||||
|
"""Return credentials for specific auth provider type and id."""
|
||||||
|
if self.users is None:
|
||||||
|
await self.async_load()
|
||||||
|
|
||||||
|
return [
|
||||||
|
credentials
|
||||||
|
for user in self.users.values()
|
||||||
|
for credentials in user.credentials
|
||||||
|
if (credentials.auth_provider_type == provider_type and
|
||||||
|
credentials.auth_provider_id == provider_id)
|
||||||
|
]
|
||||||
|
|
||||||
|
async def async_get_user(self, user_id):
|
||||||
|
"""Retrieve a user."""
|
||||||
|
if self.users is None:
|
||||||
|
await self.async_load()
|
||||||
|
|
||||||
|
return self.users.get(user_id)
|
||||||
|
|
||||||
|
async def async_get_or_create_user(self, credentials, auth_provider):
|
||||||
|
"""Get or create a new user for given credentials.
|
||||||
|
|
||||||
|
If link_user is passed in, the credentials will be linked to the passed
|
||||||
|
in user if the credentials are new.
|
||||||
|
"""
|
||||||
|
if self.users is None:
|
||||||
|
await self.async_load()
|
||||||
|
|
||||||
|
# New credentials, store in user
|
||||||
|
if credentials.is_new:
|
||||||
|
info = await auth_provider.async_user_meta_for_credentials(
|
||||||
|
credentials)
|
||||||
|
# Make owner and activate user if it's the first user.
|
||||||
|
if self.users:
|
||||||
|
is_owner = False
|
||||||
|
is_active = False
|
||||||
|
else:
|
||||||
|
is_owner = True
|
||||||
|
is_active = True
|
||||||
|
|
||||||
|
new_user = User(
|
||||||
|
is_owner=is_owner,
|
||||||
|
is_active=is_active,
|
||||||
|
name=info.get('name'),
|
||||||
|
)
|
||||||
|
self.users[new_user.id] = new_user
|
||||||
|
await self.async_link_user(new_user, credentials)
|
||||||
|
return new_user
|
||||||
|
|
||||||
|
for user in self.users.values():
|
||||||
|
for creds in user.credentials:
|
||||||
|
if (creds.auth_provider_type == credentials.auth_provider_type
|
||||||
|
and creds.auth_provider_id ==
|
||||||
|
credentials.auth_provider_id):
|
||||||
|
return user
|
||||||
|
|
||||||
|
raise ValueError('We got credentials with ID but found no user')
|
||||||
|
|
||||||
|
async def async_link_user(self, user, credentials):
|
||||||
|
"""Add credentials to an existing user."""
|
||||||
|
user.credentials.append(credentials)
|
||||||
|
await self.async_save()
|
||||||
|
credentials.is_new = False
|
||||||
|
|
||||||
|
async def async_remove_user(self, user):
|
||||||
|
"""Remove a user."""
|
||||||
|
self.users.pop(user.id)
|
||||||
|
await self.async_save()
|
||||||
|
|
||||||
|
async def async_create_refresh_token(self, user, client_id):
|
||||||
|
"""Create a new token for a user."""
|
||||||
|
refresh_token = RefreshToken(user, client_id)
|
||||||
|
user.refresh_tokens[refresh_token.token] = refresh_token
|
||||||
|
await self.async_save()
|
||||||
|
return refresh_token
|
||||||
|
|
||||||
|
async def async_get_refresh_token(self, token):
|
||||||
|
"""Get refresh token by token."""
|
||||||
|
if self.users is None:
|
||||||
|
await self.async_load()
|
||||||
|
|
||||||
|
for user in self.users.values():
|
||||||
|
refresh_token = user.refresh_tokens.get(token)
|
||||||
|
if refresh_token is not None:
|
||||||
|
return refresh_token
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def async_create_client(self, name):
|
||||||
|
"""Create a new client."""
|
||||||
|
if self.clients is None:
|
||||||
|
await self.async_load()
|
||||||
|
|
||||||
|
client = Client(name)
|
||||||
|
self.clients[client.id] = client
|
||||||
|
await self.async_save()
|
||||||
|
return client
|
||||||
|
|
||||||
|
async def async_get_client(self, client_id):
|
||||||
|
"""Get a client."""
|
||||||
|
if self.clients is None:
|
||||||
|
await self.async_load()
|
||||||
|
|
||||||
|
return self.clients.get(client_id)
|
||||||
|
|
||||||
|
async def async_load(self):
|
||||||
|
"""Load the users."""
|
||||||
|
async with self._load_lock:
|
||||||
|
self.users = {}
|
||||||
|
self.clients = {}
|
||||||
|
|
||||||
|
async def async_save(self):
|
||||||
|
"""Save users."""
|
||||||
|
pass
|
1
homeassistant/auth_providers/__init__.py
Normal file
1
homeassistant/auth_providers/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
"""Auth providers for Home Assistant."""
|
116
homeassistant/auth_providers/insecure_example.py
Normal file
116
homeassistant/auth_providers/insecure_example.py
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
"""Example auth provider."""
|
||||||
|
from collections import OrderedDict
|
||||||
|
import hmac
|
||||||
|
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant import auth, data_entry_flow
|
||||||
|
from homeassistant.core import callback
|
||||||
|
|
||||||
|
|
||||||
|
USER_SCHEMA = vol.Schema({
|
||||||
|
vol.Required('username'): str,
|
||||||
|
vol.Required('password'): str,
|
||||||
|
vol.Optional('name'): str,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
CONFIG_SCHEMA = auth.AUTH_PROVIDER_SCHEMA.extend({
|
||||||
|
vol.Required('users'): [USER_SCHEMA]
|
||||||
|
}, extra=vol.PREVENT_EXTRA)
|
||||||
|
|
||||||
|
|
||||||
|
@auth.AUTH_PROVIDERS.register('insecure_example')
|
||||||
|
class ExampleAuthProvider(auth.AuthProvider):
|
||||||
|
"""Example auth provider based on hardcoded usernames and passwords."""
|
||||||
|
|
||||||
|
async def async_credential_flow(self):
|
||||||
|
"""Return a flow to login."""
|
||||||
|
return LoginFlow(self)
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_validate_login(self, username, password):
|
||||||
|
"""Helper to validate a username and password."""
|
||||||
|
user = None
|
||||||
|
|
||||||
|
# Compare all users to avoid timing attacks.
|
||||||
|
for usr in self.config['users']:
|
||||||
|
if hmac.compare_digest(username.encode('utf-8'),
|
||||||
|
usr['username'].encode('utf-8')):
|
||||||
|
user = usr
|
||||||
|
|
||||||
|
if user is None:
|
||||||
|
# Do one more compare to make timing the same as if user was found.
|
||||||
|
hmac.compare_digest(password.encode('utf-8'),
|
||||||
|
password.encode('utf-8'))
|
||||||
|
raise auth.InvalidUser
|
||||||
|
|
||||||
|
if not hmac.compare_digest(user['password'].encode('utf-8'),
|
||||||
|
password.encode('utf-8')):
|
||||||
|
raise auth.InvalidPassword
|
||||||
|
|
||||||
|
async def async_get_or_create_credentials(self, flow_result):
|
||||||
|
"""Get credentials based on the flow result."""
|
||||||
|
username = flow_result['username']
|
||||||
|
password = flow_result['password']
|
||||||
|
|
||||||
|
self.async_validate_login(username, password)
|
||||||
|
|
||||||
|
for credential in await self.async_credentials():
|
||||||
|
if credential.data['username'] == username:
|
||||||
|
return credential
|
||||||
|
|
||||||
|
# Create new credentials.
|
||||||
|
return self.async_create_credentials({
|
||||||
|
'username': username
|
||||||
|
})
|
||||||
|
|
||||||
|
async def async_user_meta_for_credentials(self, credentials):
|
||||||
|
"""Return extra user metadata for credentials.
|
||||||
|
|
||||||
|
Will be used to populate info when creating a new user.
|
||||||
|
"""
|
||||||
|
username = credentials.data['username']
|
||||||
|
|
||||||
|
for user in self.config['users']:
|
||||||
|
if user['username'] == username:
|
||||||
|
return {
|
||||||
|
'name': user.get('name')
|
||||||
|
}
|
||||||
|
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
class LoginFlow(data_entry_flow.FlowHandler):
|
||||||
|
"""Handler for the login flow."""
|
||||||
|
|
||||||
|
def __init__(self, auth_provider):
|
||||||
|
"""Initialize the login flow."""
|
||||||
|
self._auth_provider = auth_provider
|
||||||
|
|
||||||
|
async def async_step_init(self, user_input=None):
|
||||||
|
"""Handle the step of the form."""
|
||||||
|
errors = {}
|
||||||
|
|
||||||
|
if user_input is not None:
|
||||||
|
try:
|
||||||
|
self._auth_provider.async_validate_login(
|
||||||
|
user_input['username'], user_input['password'])
|
||||||
|
except (auth.InvalidUser, auth.InvalidPassword):
|
||||||
|
errors['base'] = 'invalid_auth'
|
||||||
|
|
||||||
|
if not errors:
|
||||||
|
return self.async_create_entry(
|
||||||
|
title=self._auth_provider.name,
|
||||||
|
data=user_input
|
||||||
|
)
|
||||||
|
|
||||||
|
schema = OrderedDict()
|
||||||
|
schema['username'] = str
|
||||||
|
schema['password'] = str
|
||||||
|
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id='init',
|
||||||
|
data_schema=vol.Schema(schema),
|
||||||
|
errors=errors,
|
||||||
|
)
|
@ -12,8 +12,7 @@ from typing import Any, Optional, Dict
|
|||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant import (
|
from homeassistant import (
|
||||||
core, config as conf_util, config_entries, loader,
|
core, config as conf_util, config_entries, components as core_components)
|
||||||
components as core_components)
|
|
||||||
from homeassistant.components import persistent_notification
|
from homeassistant.components import persistent_notification
|
||||||
from homeassistant.const import EVENT_HOMEASSISTANT_CLOSE
|
from homeassistant.const import EVENT_HOMEASSISTANT_CLOSE
|
||||||
from homeassistant.setup import async_setup_component
|
from homeassistant.setup import async_setup_component
|
||||||
@ -67,16 +66,15 @@ def from_config_dict(config: Dict[str, Any],
|
|||||||
return hass
|
return hass
|
||||||
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def async_from_config_dict(config: Dict[str, Any],
|
||||||
def async_from_config_dict(config: Dict[str, Any],
|
hass: core.HomeAssistant,
|
||||||
hass: core.HomeAssistant,
|
config_dir: Optional[str] = None,
|
||||||
config_dir: Optional[str] = None,
|
enable_log: bool = True,
|
||||||
enable_log: bool = True,
|
verbose: bool = False,
|
||||||
verbose: bool = False,
|
skip_pip: bool = False,
|
||||||
skip_pip: bool = False,
|
log_rotate_days: Any = None,
|
||||||
log_rotate_days: Any = None,
|
log_file: Any = None,
|
||||||
log_file: Any = None,
|
log_no_color: bool = False) \
|
||||||
log_no_color: bool = False) \
|
|
||||||
-> Optional[core.HomeAssistant]:
|
-> Optional[core.HomeAssistant]:
|
||||||
"""Try to configure Home Assistant from a configuration dictionary.
|
"""Try to configure Home Assistant from a configuration dictionary.
|
||||||
|
|
||||||
@ -92,27 +90,24 @@ def async_from_config_dict(config: Dict[str, Any],
|
|||||||
core_config = config.get(core.DOMAIN, {})
|
core_config = config.get(core.DOMAIN, {})
|
||||||
|
|
||||||
try:
|
try:
|
||||||
yield from conf_util.async_process_ha_core_config(hass, core_config)
|
await conf_util.async_process_ha_core_config(hass, core_config)
|
||||||
except vol.Invalid as ex:
|
except vol.Invalid as ex:
|
||||||
conf_util.async_log_exception(ex, 'homeassistant', core_config, hass)
|
conf_util.async_log_exception(ex, 'homeassistant', core_config, hass)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
yield from hass.async_add_job(conf_util.process_ha_config_upgrade, hass)
|
await hass.async_add_job(conf_util.process_ha_config_upgrade, hass)
|
||||||
|
|
||||||
hass.config.skip_pip = skip_pip
|
hass.config.skip_pip = skip_pip
|
||||||
if skip_pip:
|
if skip_pip:
|
||||||
_LOGGER.warning("Skipping pip installation of required modules. "
|
_LOGGER.warning("Skipping pip installation of required modules. "
|
||||||
"This may cause issues")
|
"This may cause issues")
|
||||||
|
|
||||||
if not loader.PREPARED:
|
|
||||||
yield from hass.async_add_job(loader.prepare, hass)
|
|
||||||
|
|
||||||
# Make a copy because we are mutating it.
|
# Make a copy because we are mutating it.
|
||||||
config = OrderedDict(config)
|
config = OrderedDict(config)
|
||||||
|
|
||||||
# Merge packages
|
# Merge packages
|
||||||
conf_util.merge_packages_config(
|
conf_util.merge_packages_config(
|
||||||
config, core_config.get(conf_util.CONF_PACKAGES, {}))
|
hass, config, core_config.get(conf_util.CONF_PACKAGES, {}))
|
||||||
|
|
||||||
# Ensure we have no None values after merge
|
# Ensure we have no None values after merge
|
||||||
for key, value in config.items():
|
for key, value in config.items():
|
||||||
@ -120,7 +115,7 @@ def async_from_config_dict(config: Dict[str, Any],
|
|||||||
config[key] = {}
|
config[key] = {}
|
||||||
|
|
||||||
hass.config_entries = config_entries.ConfigEntries(hass, config)
|
hass.config_entries = config_entries.ConfigEntries(hass, config)
|
||||||
yield from hass.config_entries.async_load()
|
await hass.config_entries.async_load()
|
||||||
|
|
||||||
# Filter out the repeating and common config section [homeassistant]
|
# Filter out the repeating and common config section [homeassistant]
|
||||||
components = set(key.split(' ')[0] for key in config.keys()
|
components = set(key.split(' ')[0] for key in config.keys()
|
||||||
@ -129,13 +124,13 @@ def async_from_config_dict(config: Dict[str, Any],
|
|||||||
|
|
||||||
# setup components
|
# setup components
|
||||||
# pylint: disable=not-an-iterable
|
# pylint: disable=not-an-iterable
|
||||||
res = yield from core_components.async_setup(hass, config)
|
res = await core_components.async_setup(hass, config)
|
||||||
if not res:
|
if not res:
|
||||||
_LOGGER.error("Home Assistant core failed to initialize. "
|
_LOGGER.error("Home Assistant core failed to initialize. "
|
||||||
"further initialization aborted")
|
"further initialization aborted")
|
||||||
return hass
|
return hass
|
||||||
|
|
||||||
yield from persistent_notification.async_setup(hass, config)
|
await persistent_notification.async_setup(hass, config)
|
||||||
|
|
||||||
_LOGGER.info("Home Assistant core initialized")
|
_LOGGER.info("Home Assistant core initialized")
|
||||||
|
|
||||||
@ -145,7 +140,7 @@ def async_from_config_dict(config: Dict[str, Any],
|
|||||||
continue
|
continue
|
||||||
hass.async_add_job(async_setup_component(hass, component, config))
|
hass.async_add_job(async_setup_component(hass, component, config))
|
||||||
|
|
||||||
yield from hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
# stage 2
|
# stage 2
|
||||||
for component in components:
|
for component in components:
|
||||||
@ -153,7 +148,7 @@ def async_from_config_dict(config: Dict[str, Any],
|
|||||||
continue
|
continue
|
||||||
hass.async_add_job(async_setup_component(hass, component, config))
|
hass.async_add_job(async_setup_component(hass, component, config))
|
||||||
|
|
||||||
yield from hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
stop = time()
|
stop = time()
|
||||||
_LOGGER.info("Home Assistant initialized in %.2fs", stop-start)
|
_LOGGER.info("Home Assistant initialized in %.2fs", stop-start)
|
||||||
@ -187,14 +182,13 @@ def from_config_file(config_path: str,
|
|||||||
return hass
|
return hass
|
||||||
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def async_from_config_file(config_path: str,
|
||||||
def async_from_config_file(config_path: str,
|
hass: core.HomeAssistant,
|
||||||
hass: core.HomeAssistant,
|
verbose: bool = False,
|
||||||
verbose: bool = False,
|
skip_pip: bool = True,
|
||||||
skip_pip: bool = True,
|
log_rotate_days: Any = None,
|
||||||
log_rotate_days: Any = None,
|
log_file: Any = None,
|
||||||
log_file: Any = None,
|
log_no_color: bool = False):
|
||||||
log_no_color: bool = False):
|
|
||||||
"""Read the configuration file and try to start all the functionality.
|
"""Read the configuration file and try to start all the functionality.
|
||||||
|
|
||||||
Will add functionality to 'hass' parameter.
|
Will add functionality to 'hass' parameter.
|
||||||
@ -203,13 +197,13 @@ def async_from_config_file(config_path: str,
|
|||||||
# Set config dir to directory holding config file
|
# Set config dir to directory holding config file
|
||||||
config_dir = os.path.abspath(os.path.dirname(config_path))
|
config_dir = os.path.abspath(os.path.dirname(config_path))
|
||||||
hass.config.config_dir = config_dir
|
hass.config.config_dir = config_dir
|
||||||
yield from async_mount_local_lib_path(config_dir, hass.loop)
|
await async_mount_local_lib_path(config_dir, hass.loop)
|
||||||
|
|
||||||
async_enable_logging(hass, verbose, log_rotate_days, log_file,
|
async_enable_logging(hass, verbose, log_rotate_days, log_file,
|
||||||
log_no_color)
|
log_no_color)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
config_dict = yield from hass.async_add_job(
|
config_dict = await hass.async_add_job(
|
||||||
conf_util.load_yaml_config_file, config_path)
|
conf_util.load_yaml_config_file, config_path)
|
||||||
except HomeAssistantError as err:
|
except HomeAssistantError as err:
|
||||||
_LOGGER.error("Error loading %s: %s", config_path, err)
|
_LOGGER.error("Error loading %s: %s", config_path, err)
|
||||||
@ -217,7 +211,7 @@ def async_from_config_file(config_path: str,
|
|||||||
finally:
|
finally:
|
||||||
clear_secret_cache()
|
clear_secret_cache()
|
||||||
|
|
||||||
hass = yield from async_from_config_dict(
|
hass = await async_from_config_dict(
|
||||||
config_dict, hass, enable_log=False, skip_pip=skip_pip)
|
config_dict, hass, enable_log=False, skip_pip=skip_pip)
|
||||||
return hass
|
return hass
|
||||||
|
|
||||||
@ -294,11 +288,10 @@ def async_enable_logging(hass: core.HomeAssistant,
|
|||||||
|
|
||||||
async_handler = AsyncHandler(hass.loop, err_handler)
|
async_handler = AsyncHandler(hass.loop, err_handler)
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def async_stop_async_handler(event):
|
||||||
def async_stop_async_handler(event):
|
|
||||||
"""Cleanup async handler."""
|
"""Cleanup async handler."""
|
||||||
logging.getLogger('').removeHandler(async_handler)
|
logging.getLogger('').removeHandler(async_handler)
|
||||||
yield from async_handler.async_close(blocking=True)
|
await async_handler.async_close(blocking=True)
|
||||||
|
|
||||||
hass.bus.async_listen_once(
|
hass.bus.async_listen_once(
|
||||||
EVENT_HOMEASSISTANT_CLOSE, async_stop_async_handler)
|
EVENT_HOMEASSISTANT_CLOSE, async_stop_async_handler)
|
||||||
@ -323,15 +316,14 @@ def mount_local_lib_path(config_dir: str) -> str:
|
|||||||
return deps_dir
|
return deps_dir
|
||||||
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def async_mount_local_lib_path(config_dir: str,
|
||||||
def async_mount_local_lib_path(config_dir: str,
|
loop: asyncio.AbstractEventLoop) -> str:
|
||||||
loop: asyncio.AbstractEventLoop) -> str:
|
|
||||||
"""Add local library to Python Path.
|
"""Add local library to Python Path.
|
||||||
|
|
||||||
This function is a coroutine.
|
This function is a coroutine.
|
||||||
"""
|
"""
|
||||||
deps_dir = os.path.join(config_dir, 'deps')
|
deps_dir = os.path.join(config_dir, 'deps')
|
||||||
lib_dir = yield from async_get_user_site(deps_dir, loop=loop)
|
lib_dir = await async_get_user_site(deps_dir, loop=loop)
|
||||||
if lib_dir not in sys.path:
|
if lib_dir not in sys.path:
|
||||||
sys.path.insert(0, lib_dir)
|
sys.path.insert(0, lib_dir)
|
||||||
return deps_dir
|
return deps_dir
|
||||||
|
@ -81,7 +81,7 @@ TRIGGER_SCHEMA = vol.Schema({
|
|||||||
|
|
||||||
ABODE_PLATFORMS = [
|
ABODE_PLATFORMS = [
|
||||||
'alarm_control_panel', 'binary_sensor', 'lock', 'switch', 'cover',
|
'alarm_control_panel', 'binary_sensor', 'lock', 'switch', 'cover',
|
||||||
'camera', 'light'
|
'camera', 'light', 'sensor'
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -17,7 +17,7 @@ from homeassistant.const import (
|
|||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
REQUIREMENTS = ['pyalarmdotcom==0.3.1']
|
REQUIREMENTS = ['pyalarmdotcom==0.3.2']
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -93,6 +93,13 @@ class AlarmDotCom(alarm.AlarmControlPanel):
|
|||||||
return STATE_ALARM_ARMED_AWAY
|
return STATE_ALARM_ARMED_AWAY
|
||||||
return STATE_UNKNOWN
|
return STATE_UNKNOWN
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device_state_attributes(self):
|
||||||
|
"""Return the state attributes."""
|
||||||
|
return {
|
||||||
|
'sensor_status': self._alarm.sensor_status
|
||||||
|
}
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def async_alarm_disarm(self, code=None):
|
def async_alarm_disarm(self, code=None):
|
||||||
"""Send disarm command."""
|
"""Send disarm command."""
|
||||||
|
@ -76,8 +76,7 @@ class APIEventStream(HomeAssistantView):
|
|||||||
url = URL_API_STREAM
|
url = URL_API_STREAM
|
||||||
name = "api:stream"
|
name = "api:stream"
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def get(self, request):
|
||||||
def get(self, request):
|
|
||||||
"""Provide a streaming interface for the event bus."""
|
"""Provide a streaming interface for the event bus."""
|
||||||
# pylint: disable=no-self-use
|
# pylint: disable=no-self-use
|
||||||
hass = request.app['hass']
|
hass = request.app['hass']
|
||||||
@ -88,8 +87,7 @@ class APIEventStream(HomeAssistantView):
|
|||||||
if restrict:
|
if restrict:
|
||||||
restrict = restrict.split(',') + [EVENT_HOMEASSISTANT_STOP]
|
restrict = restrict.split(',') + [EVENT_HOMEASSISTANT_STOP]
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def forward_events(event):
|
||||||
def forward_events(event):
|
|
||||||
"""Forward events to the open request."""
|
"""Forward events to the open request."""
|
||||||
if event.event_type == EVENT_TIME_CHANGED:
|
if event.event_type == EVENT_TIME_CHANGED:
|
||||||
return
|
return
|
||||||
@ -104,11 +102,11 @@ class APIEventStream(HomeAssistantView):
|
|||||||
else:
|
else:
|
||||||
data = json.dumps(event, cls=rem.JSONEncoder)
|
data = json.dumps(event, cls=rem.JSONEncoder)
|
||||||
|
|
||||||
yield from to_write.put(data)
|
await to_write.put(data)
|
||||||
|
|
||||||
response = web.StreamResponse()
|
response = web.StreamResponse()
|
||||||
response.content_type = 'text/event-stream'
|
response.content_type = 'text/event-stream'
|
||||||
yield from response.prepare(request)
|
await response.prepare(request)
|
||||||
|
|
||||||
unsub_stream = hass.bus.async_listen(MATCH_ALL, forward_events)
|
unsub_stream = hass.bus.async_listen(MATCH_ALL, forward_events)
|
||||||
|
|
||||||
@ -116,13 +114,13 @@ class APIEventStream(HomeAssistantView):
|
|||||||
_LOGGER.debug('STREAM %s ATTACHED', id(stop_obj))
|
_LOGGER.debug('STREAM %s ATTACHED', id(stop_obj))
|
||||||
|
|
||||||
# Fire off one message so browsers fire open event right away
|
# Fire off one message so browsers fire open event right away
|
||||||
yield from to_write.put(STREAM_PING_PAYLOAD)
|
await to_write.put(STREAM_PING_PAYLOAD)
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
with async_timeout.timeout(STREAM_PING_INTERVAL,
|
with async_timeout.timeout(STREAM_PING_INTERVAL,
|
||||||
loop=hass.loop):
|
loop=hass.loop):
|
||||||
payload = yield from to_write.get()
|
payload = await to_write.get()
|
||||||
|
|
||||||
if payload is stop_obj:
|
if payload is stop_obj:
|
||||||
break
|
break
|
||||||
@ -130,9 +128,9 @@ class APIEventStream(HomeAssistantView):
|
|||||||
msg = "data: {}\n\n".format(payload)
|
msg = "data: {}\n\n".format(payload)
|
||||||
_LOGGER.debug('STREAM %s WRITING %s', id(stop_obj),
|
_LOGGER.debug('STREAM %s WRITING %s', id(stop_obj),
|
||||||
msg.strip())
|
msg.strip())
|
||||||
yield from response.write(msg.encode("UTF-8"))
|
await response.write(msg.encode("UTF-8"))
|
||||||
except asyncio.TimeoutError:
|
except asyncio.TimeoutError:
|
||||||
yield from to_write.put(STREAM_PING_PAYLOAD)
|
await to_write.put(STREAM_PING_PAYLOAD)
|
||||||
|
|
||||||
except asyncio.CancelledError:
|
except asyncio.CancelledError:
|
||||||
_LOGGER.debug('STREAM %s ABORT', id(stop_obj))
|
_LOGGER.debug('STREAM %s ABORT', id(stop_obj))
|
||||||
@ -200,12 +198,11 @@ class APIEntityStateView(HomeAssistantView):
|
|||||||
return self.json(state)
|
return self.json(state)
|
||||||
return self.json_message('Entity not found', HTTP_NOT_FOUND)
|
return self.json_message('Entity not found', HTTP_NOT_FOUND)
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def post(self, request, entity_id):
|
||||||
def post(self, request, entity_id):
|
|
||||||
"""Update state of entity."""
|
"""Update state of entity."""
|
||||||
hass = request.app['hass']
|
hass = request.app['hass']
|
||||||
try:
|
try:
|
||||||
data = yield from request.json()
|
data = await request.json()
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return self.json_message('Invalid JSON specified',
|
return self.json_message('Invalid JSON specified',
|
||||||
HTTP_BAD_REQUEST)
|
HTTP_BAD_REQUEST)
|
||||||
@ -257,10 +254,9 @@ class APIEventView(HomeAssistantView):
|
|||||||
url = '/api/events/{event_type}'
|
url = '/api/events/{event_type}'
|
||||||
name = "api:event"
|
name = "api:event"
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def post(self, request, event_type):
|
||||||
def post(self, request, event_type):
|
|
||||||
"""Fire events."""
|
"""Fire events."""
|
||||||
body = yield from request.text()
|
body = await request.text()
|
||||||
try:
|
try:
|
||||||
event_data = json.loads(body) if body else None
|
event_data = json.loads(body) if body else None
|
||||||
except ValueError:
|
except ValueError:
|
||||||
@ -292,10 +288,9 @@ class APIServicesView(HomeAssistantView):
|
|||||||
url = URL_API_SERVICES
|
url = URL_API_SERVICES
|
||||||
name = "api:services"
|
name = "api:services"
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def get(self, request):
|
||||||
def get(self, request):
|
|
||||||
"""Get registered services."""
|
"""Get registered services."""
|
||||||
services = yield from async_services_json(request.app['hass'])
|
services = await async_services_json(request.app['hass'])
|
||||||
return self.json(services)
|
return self.json(services)
|
||||||
|
|
||||||
|
|
||||||
@ -305,14 +300,13 @@ class APIDomainServicesView(HomeAssistantView):
|
|||||||
url = "/api/services/{domain}/{service}"
|
url = "/api/services/{domain}/{service}"
|
||||||
name = "api:domain-services"
|
name = "api:domain-services"
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def post(self, request, domain, service):
|
||||||
def post(self, request, domain, service):
|
|
||||||
"""Call a service.
|
"""Call a service.
|
||||||
|
|
||||||
Returns a list of changed states.
|
Returns a list of changed states.
|
||||||
"""
|
"""
|
||||||
hass = request.app['hass']
|
hass = request.app['hass']
|
||||||
body = yield from request.text()
|
body = await request.text()
|
||||||
try:
|
try:
|
||||||
data = json.loads(body) if body else None
|
data = json.loads(body) if body else None
|
||||||
except ValueError:
|
except ValueError:
|
||||||
@ -320,7 +314,7 @@ class APIDomainServicesView(HomeAssistantView):
|
|||||||
HTTP_BAD_REQUEST)
|
HTTP_BAD_REQUEST)
|
||||||
|
|
||||||
with AsyncTrackStates(hass) as changed_states:
|
with AsyncTrackStates(hass) as changed_states:
|
||||||
yield from hass.services.async_call(domain, service, data, True)
|
await hass.services.async_call(domain, service, data, True)
|
||||||
|
|
||||||
return self.json(changed_states)
|
return self.json(changed_states)
|
||||||
|
|
||||||
@ -343,11 +337,10 @@ class APITemplateView(HomeAssistantView):
|
|||||||
url = URL_API_TEMPLATE
|
url = URL_API_TEMPLATE
|
||||||
name = "api:template"
|
name = "api:template"
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def post(self, request):
|
||||||
def post(self, request):
|
|
||||||
"""Render a template."""
|
"""Render a template."""
|
||||||
try:
|
try:
|
||||||
data = yield from request.json()
|
data = await request.json()
|
||||||
tpl = template.Template(data['template'], request.app['hass'])
|
tpl = template.Template(data['template'], request.app['hass'])
|
||||||
return tpl.async_render(data.get('variables'))
|
return tpl.async_render(data.get('variables'))
|
||||||
except (ValueError, TemplateError) as ex:
|
except (ValueError, TemplateError) as ex:
|
||||||
@ -366,10 +359,9 @@ class APIErrorLog(HomeAssistantView):
|
|||||||
return await self.file(request, request.app['hass'].data[DATA_LOGGING])
|
return await self.file(request, request.app['hass'].data[DATA_LOGGING])
|
||||||
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def async_services_json(hass):
|
||||||
def async_services_json(hass):
|
|
||||||
"""Generate services data to JSONify."""
|
"""Generate services data to JSONify."""
|
||||||
descriptions = yield from async_get_all_descriptions(hass)
|
descriptions = await async_get_all_descriptions(hass)
|
||||||
return [{"domain": key, "services": value}
|
return [{"domain": key, "services": value}
|
||||||
for key, value in descriptions.items()]
|
for key, value in descriptions.items()]
|
||||||
|
|
||||||
|
344
homeassistant/components/auth/__init__.py
Normal file
344
homeassistant/components/auth/__init__.py
Normal file
@ -0,0 +1,344 @@
|
|||||||
|
"""Component to allow users to login and get tokens.
|
||||||
|
|
||||||
|
All requests will require passing in a valid client ID and secret via HTTP
|
||||||
|
Basic Auth.
|
||||||
|
|
||||||
|
# GET /auth/providers
|
||||||
|
|
||||||
|
Return a list of auth providers. Example:
|
||||||
|
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "Local",
|
||||||
|
"id": null,
|
||||||
|
"type": "local_provider",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
# POST /auth/login_flow
|
||||||
|
|
||||||
|
Create a login flow. Will return the first step of the flow.
|
||||||
|
|
||||||
|
Pass in parameter 'handler' to specify the auth provider to use. Auth providers
|
||||||
|
are identified by type and id.
|
||||||
|
|
||||||
|
{
|
||||||
|
"handler": ["local_provider", null]
|
||||||
|
}
|
||||||
|
|
||||||
|
Return value will be a step in a data entry flow. See the docs for data entry
|
||||||
|
flow for details.
|
||||||
|
|
||||||
|
{
|
||||||
|
"data_schema": [
|
||||||
|
{"name": "username", "type": "string"},
|
||||||
|
{"name": "password", "type": "string"}
|
||||||
|
],
|
||||||
|
"errors": {},
|
||||||
|
"flow_id": "8f7e42faab604bcab7ac43c44ca34d58",
|
||||||
|
"handler": ["insecure_example", null],
|
||||||
|
"step_id": "init",
|
||||||
|
"type": "form"
|
||||||
|
}
|
||||||
|
|
||||||
|
# POST /auth/login_flow/{flow_id}
|
||||||
|
|
||||||
|
Progress the flow. Most flows will be 1 page, but could optionally add extra
|
||||||
|
login challenges, like TFA. Once the flow has finished, the returned step will
|
||||||
|
have type "create_entry" and "result" key will contain an authorization code.
|
||||||
|
|
||||||
|
{
|
||||||
|
"flow_id": "8f7e42faab604bcab7ac43c44ca34d58",
|
||||||
|
"handler": ["insecure_example", null],
|
||||||
|
"result": "411ee2f916e648d691e937ae9344681e",
|
||||||
|
"source": "user",
|
||||||
|
"title": "Example",
|
||||||
|
"type": "create_entry",
|
||||||
|
"version": 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# POST /auth/token
|
||||||
|
|
||||||
|
This is an OAuth2 endpoint for granting tokens. We currently support the grant
|
||||||
|
types "authorization_code" and "refresh_token". Because we follow the OAuth2
|
||||||
|
spec, data should be send in formatted as x-www-form-urlencoded. Examples will
|
||||||
|
be in JSON as it's more readable.
|
||||||
|
|
||||||
|
## Grant type authorization_code
|
||||||
|
|
||||||
|
Exchange the authorization code retrieved from the login flow for tokens.
|
||||||
|
|
||||||
|
{
|
||||||
|
"grant_type": "authorization_code",
|
||||||
|
"code": "411ee2f916e648d691e937ae9344681e"
|
||||||
|
}
|
||||||
|
|
||||||
|
Return value will be the access and refresh tokens. The access token will have
|
||||||
|
a limited expiration. New access tokens can be requested using the refresh
|
||||||
|
token.
|
||||||
|
|
||||||
|
{
|
||||||
|
"access_token": "ABCDEFGH",
|
||||||
|
"expires_in": 1800,
|
||||||
|
"refresh_token": "IJKLMNOPQRST",
|
||||||
|
"token_type": "Bearer"
|
||||||
|
}
|
||||||
|
|
||||||
|
## Grant type refresh_token
|
||||||
|
|
||||||
|
Request a new access token using a refresh token.
|
||||||
|
|
||||||
|
{
|
||||||
|
"grant_type": "refresh_token",
|
||||||
|
"refresh_token": "IJKLMNOPQRST"
|
||||||
|
}
|
||||||
|
|
||||||
|
Return value will be a new access token. The access token will have
|
||||||
|
a limited expiration.
|
||||||
|
|
||||||
|
{
|
||||||
|
"access_token": "ABCDEFGH",
|
||||||
|
"expires_in": 1800,
|
||||||
|
"token_type": "Bearer"
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
import aiohttp.web
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant import data_entry_flow
|
||||||
|
from homeassistant.core import callback
|
||||||
|
from homeassistant.helpers.data_entry_flow import (
|
||||||
|
FlowManagerIndexView, FlowManagerResourceView)
|
||||||
|
from homeassistant.components.http.view import HomeAssistantView
|
||||||
|
from homeassistant.components.http.data_validator import RequestDataValidator
|
||||||
|
|
||||||
|
from .client import verify_client
|
||||||
|
|
||||||
|
DOMAIN = 'auth'
|
||||||
|
DEPENDENCIES = ['http']
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup(hass, config):
|
||||||
|
"""Component to allow users to login."""
|
||||||
|
store_credentials, retrieve_credentials = _create_cred_store()
|
||||||
|
|
||||||
|
hass.http.register_view(AuthProvidersView)
|
||||||
|
hass.http.register_view(LoginFlowIndexView(hass.auth.login_flow))
|
||||||
|
hass.http.register_view(
|
||||||
|
LoginFlowResourceView(hass.auth.login_flow, store_credentials))
|
||||||
|
hass.http.register_view(GrantTokenView(retrieve_credentials))
|
||||||
|
hass.http.register_view(LinkUserView(retrieve_credentials))
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class AuthProvidersView(HomeAssistantView):
|
||||||
|
"""View to get available auth providers."""
|
||||||
|
|
||||||
|
url = '/auth/providers'
|
||||||
|
name = 'api:auth:providers'
|
||||||
|
requires_auth = False
|
||||||
|
|
||||||
|
@verify_client
|
||||||
|
async def get(self, request, client_id):
|
||||||
|
"""Get available auth providers."""
|
||||||
|
return self.json([{
|
||||||
|
'name': provider.name,
|
||||||
|
'id': provider.id,
|
||||||
|
'type': provider.type,
|
||||||
|
} for provider in request.app['hass'].auth.async_auth_providers])
|
||||||
|
|
||||||
|
|
||||||
|
class LoginFlowIndexView(FlowManagerIndexView):
|
||||||
|
"""View to create a config flow."""
|
||||||
|
|
||||||
|
url = '/auth/login_flow'
|
||||||
|
name = 'api:auth:login_flow'
|
||||||
|
requires_auth = False
|
||||||
|
|
||||||
|
async def get(self, request):
|
||||||
|
"""Do not allow index of flows in progress."""
|
||||||
|
return aiohttp.web.Response(status=405)
|
||||||
|
|
||||||
|
# pylint: disable=arguments-differ
|
||||||
|
@verify_client
|
||||||
|
async def post(self, request, client_id):
|
||||||
|
"""Create a new login flow."""
|
||||||
|
# pylint: disable=no-value-for-parameter
|
||||||
|
return await super().post(request)
|
||||||
|
|
||||||
|
|
||||||
|
class LoginFlowResourceView(FlowManagerResourceView):
|
||||||
|
"""View to interact with the flow manager."""
|
||||||
|
|
||||||
|
url = '/auth/login_flow/{flow_id}'
|
||||||
|
name = 'api:auth:login_flow:resource'
|
||||||
|
requires_auth = False
|
||||||
|
|
||||||
|
def __init__(self, flow_mgr, store_credentials):
|
||||||
|
"""Initialize the login flow resource view."""
|
||||||
|
super().__init__(flow_mgr)
|
||||||
|
self._store_credentials = store_credentials
|
||||||
|
|
||||||
|
# pylint: disable=arguments-differ
|
||||||
|
async def get(self, request):
|
||||||
|
"""Do not allow getting status of a flow in progress."""
|
||||||
|
return self.json_message('Invalid flow specified', 404)
|
||||||
|
|
||||||
|
# pylint: disable=arguments-differ
|
||||||
|
@verify_client
|
||||||
|
@RequestDataValidator(vol.Schema(dict), allow_empty=True)
|
||||||
|
async def post(self, request, client_id, flow_id, data):
|
||||||
|
"""Handle progressing a login flow request."""
|
||||||
|
try:
|
||||||
|
result = await self._flow_mgr.async_configure(flow_id, data)
|
||||||
|
except data_entry_flow.UnknownFlow:
|
||||||
|
return self.json_message('Invalid flow specified', 404)
|
||||||
|
except vol.Invalid:
|
||||||
|
return self.json_message('User input malformed', 400)
|
||||||
|
|
||||||
|
if result['type'] != data_entry_flow.RESULT_TYPE_CREATE_ENTRY:
|
||||||
|
return self.json(self._prepare_result_json(result))
|
||||||
|
|
||||||
|
result.pop('data')
|
||||||
|
result['result'] = self._store_credentials(client_id, result['result'])
|
||||||
|
|
||||||
|
return self.json(result)
|
||||||
|
|
||||||
|
|
||||||
|
class GrantTokenView(HomeAssistantView):
|
||||||
|
"""View to grant tokens."""
|
||||||
|
|
||||||
|
url = '/auth/token'
|
||||||
|
name = 'api:auth:token'
|
||||||
|
requires_auth = False
|
||||||
|
|
||||||
|
def __init__(self, retrieve_credentials):
|
||||||
|
"""Initialize the grant token view."""
|
||||||
|
self._retrieve_credentials = retrieve_credentials
|
||||||
|
|
||||||
|
@verify_client
|
||||||
|
async def post(self, request, client_id):
|
||||||
|
"""Grant a token."""
|
||||||
|
hass = request.app['hass']
|
||||||
|
data = await request.post()
|
||||||
|
grant_type = data.get('grant_type')
|
||||||
|
|
||||||
|
if grant_type == 'authorization_code':
|
||||||
|
return await self._async_handle_auth_code(
|
||||||
|
hass, client_id, data)
|
||||||
|
|
||||||
|
elif grant_type == 'refresh_token':
|
||||||
|
return await self._async_handle_refresh_token(
|
||||||
|
hass, client_id, data)
|
||||||
|
|
||||||
|
return self.json({
|
||||||
|
'error': 'unsupported_grant_type',
|
||||||
|
}, status_code=400)
|
||||||
|
|
||||||
|
async def _async_handle_auth_code(self, hass, client_id, data):
|
||||||
|
"""Handle authorization code request."""
|
||||||
|
code = data.get('code')
|
||||||
|
|
||||||
|
if code is None:
|
||||||
|
return self.json({
|
||||||
|
'error': 'invalid_request',
|
||||||
|
}, status_code=400)
|
||||||
|
|
||||||
|
credentials = self._retrieve_credentials(client_id, code)
|
||||||
|
|
||||||
|
if credentials is None:
|
||||||
|
return self.json({
|
||||||
|
'error': 'invalid_request',
|
||||||
|
}, status_code=400)
|
||||||
|
|
||||||
|
user = await hass.auth.async_get_or_create_user(credentials)
|
||||||
|
refresh_token = await hass.auth.async_create_refresh_token(user,
|
||||||
|
client_id)
|
||||||
|
access_token = hass.auth.async_create_access_token(refresh_token)
|
||||||
|
|
||||||
|
return self.json({
|
||||||
|
'access_token': access_token.token,
|
||||||
|
'token_type': 'Bearer',
|
||||||
|
'refresh_token': refresh_token.token,
|
||||||
|
'expires_in':
|
||||||
|
int(refresh_token.access_token_expiration.total_seconds()),
|
||||||
|
})
|
||||||
|
|
||||||
|
async def _async_handle_refresh_token(self, hass, client_id, data):
|
||||||
|
"""Handle authorization code request."""
|
||||||
|
token = data.get('refresh_token')
|
||||||
|
|
||||||
|
if token is None:
|
||||||
|
return self.json({
|
||||||
|
'error': 'invalid_request',
|
||||||
|
}, status_code=400)
|
||||||
|
|
||||||
|
refresh_token = await hass.auth.async_get_refresh_token(token)
|
||||||
|
|
||||||
|
if refresh_token is None or refresh_token.client_id != client_id:
|
||||||
|
return self.json({
|
||||||
|
'error': 'invalid_grant',
|
||||||
|
}, status_code=400)
|
||||||
|
|
||||||
|
access_token = hass.auth.async_create_access_token(refresh_token)
|
||||||
|
|
||||||
|
return self.json({
|
||||||
|
'access_token': access_token.token,
|
||||||
|
'token_type': 'Bearer',
|
||||||
|
'expires_in':
|
||||||
|
int(refresh_token.access_token_expiration.total_seconds()),
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
class LinkUserView(HomeAssistantView):
|
||||||
|
"""View to link existing users to new credentials."""
|
||||||
|
|
||||||
|
url = '/auth/link_user'
|
||||||
|
name = 'api:auth:link_user'
|
||||||
|
|
||||||
|
def __init__(self, retrieve_credentials):
|
||||||
|
"""Initialize the link user view."""
|
||||||
|
self._retrieve_credentials = retrieve_credentials
|
||||||
|
|
||||||
|
@RequestDataValidator(vol.Schema({
|
||||||
|
'code': str,
|
||||||
|
'client_id': str,
|
||||||
|
}))
|
||||||
|
async def post(self, request, data):
|
||||||
|
"""Link a user."""
|
||||||
|
hass = request.app['hass']
|
||||||
|
user = request['hass_user']
|
||||||
|
|
||||||
|
credentials = self._retrieve_credentials(
|
||||||
|
data['client_id'], data['code'])
|
||||||
|
|
||||||
|
if credentials is None:
|
||||||
|
return self.json_message('Invalid code', status_code=400)
|
||||||
|
|
||||||
|
await hass.auth.async_link_user(user, credentials)
|
||||||
|
return self.json_message('User linked')
|
||||||
|
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _create_cred_store():
|
||||||
|
"""Create a credential store."""
|
||||||
|
temp_credentials = {}
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def store_credentials(client_id, credentials):
|
||||||
|
"""Store credentials and return a code to retrieve it."""
|
||||||
|
code = uuid.uuid4().hex
|
||||||
|
temp_credentials[(client_id, code)] = credentials
|
||||||
|
return code
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def retrieve_credentials(client_id, code):
|
||||||
|
"""Retrieve credentials."""
|
||||||
|
return temp_credentials.pop((client_id, code), None)
|
||||||
|
|
||||||
|
return store_credentials, retrieve_credentials
|
63
homeassistant/components/auth/client.py
Normal file
63
homeassistant/components/auth/client.py
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
"""Helpers to resolve client ID/secret."""
|
||||||
|
import base64
|
||||||
|
from functools import wraps
|
||||||
|
import hmac
|
||||||
|
|
||||||
|
import aiohttp.hdrs
|
||||||
|
|
||||||
|
|
||||||
|
def verify_client(method):
|
||||||
|
"""Decorator to verify client id/secret on requests."""
|
||||||
|
@wraps(method)
|
||||||
|
async def wrapper(view, request, *args, **kwargs):
|
||||||
|
"""Verify client id/secret before doing request."""
|
||||||
|
client_id = await _verify_client(request)
|
||||||
|
|
||||||
|
if client_id is None:
|
||||||
|
return view.json({
|
||||||
|
'error': 'invalid_client',
|
||||||
|
}, status_code=401)
|
||||||
|
|
||||||
|
return await method(
|
||||||
|
view, request, *args, client_id=client_id, **kwargs)
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
|
async def _verify_client(request):
|
||||||
|
"""Method to verify the client id/secret in consistent time.
|
||||||
|
|
||||||
|
By using a consistent time for looking up client id and comparing the
|
||||||
|
secret, we prevent attacks by malicious actors trying different client ids
|
||||||
|
and are able to derive from the time it takes to process the request if
|
||||||
|
they guessed the client id correctly.
|
||||||
|
"""
|
||||||
|
if aiohttp.hdrs.AUTHORIZATION not in request.headers:
|
||||||
|
return None
|
||||||
|
|
||||||
|
auth_type, auth_value = \
|
||||||
|
request.headers.get(aiohttp.hdrs.AUTHORIZATION).split(' ', 1)
|
||||||
|
|
||||||
|
if auth_type != 'Basic':
|
||||||
|
return None
|
||||||
|
|
||||||
|
decoded = base64.b64decode(auth_value).decode('utf-8')
|
||||||
|
try:
|
||||||
|
client_id, client_secret = decoded.split(':', 1)
|
||||||
|
except ValueError:
|
||||||
|
# If no ':' in decoded
|
||||||
|
return None
|
||||||
|
|
||||||
|
client = await request.app['hass'].auth.async_get_client(client_id)
|
||||||
|
|
||||||
|
if client is None:
|
||||||
|
# Still do a compare so we run same time as if a client was found.
|
||||||
|
hmac.compare_digest(client_secret.encode('utf-8'),
|
||||||
|
client_secret.encode('utf-8'))
|
||||||
|
return None
|
||||||
|
|
||||||
|
if hmac.compare_digest(client_secret.encode('utf-8'),
|
||||||
|
client.secret.encode('utf-8')):
|
||||||
|
return client_id
|
||||||
|
|
||||||
|
return None
|
@ -6,6 +6,7 @@ https://home-assistant.io/components/automation/
|
|||||||
"""
|
"""
|
||||||
import asyncio
|
import asyncio
|
||||||
from functools import partial
|
from functools import partial
|
||||||
|
import importlib
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
@ -22,7 +23,6 @@ from homeassistant.helpers import extract_domain_configs, script, condition
|
|||||||
from homeassistant.helpers.entity import ToggleEntity
|
from homeassistant.helpers.entity import ToggleEntity
|
||||||
from homeassistant.helpers.entity_component import EntityComponent
|
from homeassistant.helpers.entity_component import EntityComponent
|
||||||
from homeassistant.helpers.restore_state import async_get_last_state
|
from homeassistant.helpers.restore_state import async_get_last_state
|
||||||
from homeassistant.loader import get_platform
|
|
||||||
from homeassistant.util.dt import utcnow
|
from homeassistant.util.dt import utcnow
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
@ -58,12 +58,14 @@ _LOGGER = logging.getLogger(__name__)
|
|||||||
|
|
||||||
def _platform_validator(config):
|
def _platform_validator(config):
|
||||||
"""Validate it is a valid platform."""
|
"""Validate it is a valid platform."""
|
||||||
platform = get_platform(DOMAIN, config[CONF_PLATFORM])
|
try:
|
||||||
|
platform = importlib.import_module(
|
||||||
|
'homeassistant.components.automation.{}'.format(
|
||||||
|
config[CONF_PLATFORM]))
|
||||||
|
except ImportError:
|
||||||
|
raise vol.Invalid('Invalid platform specified') from None
|
||||||
|
|
||||||
if not hasattr(platform, 'TRIGGER_SCHEMA'):
|
return platform.TRIGGER_SCHEMA(config)
|
||||||
return config
|
|
||||||
|
|
||||||
return getattr(platform, 'TRIGGER_SCHEMA')(config)
|
|
||||||
|
|
||||||
|
|
||||||
_TRIGGER_SCHEMA = vol.All(
|
_TRIGGER_SCHEMA = vol.All(
|
||||||
@ -71,7 +73,7 @@ _TRIGGER_SCHEMA = vol.All(
|
|||||||
[
|
[
|
||||||
vol.All(
|
vol.All(
|
||||||
vol.Schema({
|
vol.Schema({
|
||||||
vol.Required(CONF_PLATFORM): cv.platform_validator(DOMAIN)
|
vol.Required(CONF_PLATFORM): str
|
||||||
}, extra=vol.ALLOW_EXTRA),
|
}, extra=vol.ALLOW_EXTRA),
|
||||||
_platform_validator
|
_platform_validator
|
||||||
),
|
),
|
||||||
|
@ -50,13 +50,23 @@ DEVICE_CLASSES_SCHEMA = vol.All(vol.Lower, vol.In(DEVICE_CLASSES))
|
|||||||
|
|
||||||
async def async_setup(hass, config):
|
async def async_setup(hass, config):
|
||||||
"""Track states and offer events for binary sensors."""
|
"""Track states and offer events for binary sensors."""
|
||||||
component = EntityComponent(
|
component = hass.data[DOMAIN] = EntityComponent(
|
||||||
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL)
|
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL)
|
||||||
|
|
||||||
await component.async_setup(config)
|
await component.async_setup(config)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(hass, entry):
|
||||||
|
"""Setup a config entry."""
|
||||||
|
return await hass.data[DOMAIN].async_setup_entry(entry)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_unload_entry(hass, entry):
|
||||||
|
"""Unload a config entry."""
|
||||||
|
return await hass.data[DOMAIN].async_unload_entry(entry)
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=no-self-use
|
# pylint: disable=no-self-use
|
||||||
class BinarySensorDevice(Entity):
|
class BinarySensorDevice(Entity):
|
||||||
"""Represent a binary sensor."""
|
"""Represent a binary sensor."""
|
||||||
|
@ -11,7 +11,6 @@ import voluptuous as vol
|
|||||||
from homeassistant.components.binary_sensor import (
|
from homeassistant.components.binary_sensor import (
|
||||||
BinarySensorDevice, PLATFORM_SCHEMA)
|
BinarySensorDevice, PLATFORM_SCHEMA)
|
||||||
from homeassistant.const import CONF_MONITORED_CONDITIONS
|
from homeassistant.const import CONF_MONITORED_CONDITIONS
|
||||||
from homeassistant.loader import get_component
|
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
@ -31,7 +30,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
|||||||
|
|
||||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
"""Set up the available BloomSky weather binary sensors."""
|
"""Set up the available BloomSky weather binary sensors."""
|
||||||
bloomsky = get_component('bloomsky')
|
bloomsky = hass.components.bloomsky
|
||||||
# Default needed in case of discovery
|
# Default needed in case of discovery
|
||||||
sensors = config.get(CONF_MONITORED_CONDITIONS, SENSOR_TYPES)
|
sensors = config.get(CONF_MONITORED_CONDITIONS, SENSOR_TYPES)
|
||||||
|
|
||||||
|
@ -6,27 +6,35 @@ https://home-assistant.io/components/binary_sensor.deconz/
|
|||||||
"""
|
"""
|
||||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||||
from homeassistant.components.deconz import (
|
from homeassistant.components.deconz import (
|
||||||
DOMAIN as DATA_DECONZ, DATA_DECONZ_ID)
|
DOMAIN as DATA_DECONZ, DATA_DECONZ_ID, DATA_DECONZ_UNSUB)
|
||||||
from homeassistant.const import ATTR_BATTERY_LEVEL
|
from homeassistant.const import ATTR_BATTERY_LEVEL
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
|
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||||
|
|
||||||
DEPENDENCIES = ['deconz']
|
DEPENDENCIES = ['deconz']
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_platform(hass, config, async_add_devices,
|
async def async_setup_platform(hass, config, async_add_devices,
|
||||||
discovery_info=None):
|
discovery_info=None):
|
||||||
|
"""Old way of setting up deCONZ binary sensors."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(hass, config_entry, async_add_devices):
|
||||||
"""Set up the deCONZ binary sensor."""
|
"""Set up the deCONZ binary sensor."""
|
||||||
if discovery_info is None:
|
@callback
|
||||||
return
|
def async_add_sensor(sensors):
|
||||||
|
"""Add binary sensor from deCONZ."""
|
||||||
|
from pydeconz.sensor import DECONZ_BINARY_SENSOR
|
||||||
|
entities = []
|
||||||
|
for sensor in sensors:
|
||||||
|
if sensor.type in DECONZ_BINARY_SENSOR:
|
||||||
|
entities.append(DeconzBinarySensor(sensor))
|
||||||
|
async_add_devices(entities, True)
|
||||||
|
hass.data[DATA_DECONZ_UNSUB].append(
|
||||||
|
async_dispatcher_connect(hass, 'deconz_new_sensor', async_add_sensor))
|
||||||
|
|
||||||
from pydeconz.sensor import DECONZ_BINARY_SENSOR
|
async_add_sensor(hass.data[DATA_DECONZ].sensors.values())
|
||||||
sensors = hass.data[DATA_DECONZ].sensors
|
|
||||||
entities = []
|
|
||||||
|
|
||||||
for sensor in sensors.values():
|
|
||||||
if sensor and sensor.type in DECONZ_BINARY_SENSOR:
|
|
||||||
entities.append(DeconzBinarySensor(sensor))
|
|
||||||
async_add_devices(entities, True)
|
|
||||||
|
|
||||||
|
|
||||||
class DeconzBinarySensor(BinarySensorDevice):
|
class DeconzBinarySensor(BinarySensorDevice):
|
||||||
|
@ -23,7 +23,7 @@ SENSOR_TYPES = {'openClosedSensor': 'opening',
|
|||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||||
"""Set up the INSTEON PLM device class for the hass platform."""
|
"""Set up the INSTEON PLM device class for the hass platform."""
|
||||||
plm = hass.data['insteon_plm']
|
plm = hass.data['insteon_plm'].get('plm')
|
||||||
|
|
||||||
address = discovery_info['address']
|
address = discovery_info['address']
|
||||||
device = plm.devices[address]
|
device = plm.devices[address]
|
||||||
|
@ -13,7 +13,6 @@ import voluptuous as vol
|
|||||||
from homeassistant.components.binary_sensor import (
|
from homeassistant.components.binary_sensor import (
|
||||||
BinarySensorDevice, PLATFORM_SCHEMA)
|
BinarySensorDevice, PLATFORM_SCHEMA)
|
||||||
from homeassistant.components.netatmo import CameraData
|
from homeassistant.components.netatmo import CameraData
|
||||||
from homeassistant.loader import get_component
|
|
||||||
from homeassistant.const import CONF_TIMEOUT
|
from homeassistant.const import CONF_TIMEOUT
|
||||||
from homeassistant.helpers import config_validation as cv
|
from homeassistant.helpers import config_validation as cv
|
||||||
|
|
||||||
@ -61,7 +60,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
|||||||
# pylint: disable=unused-argument
|
# pylint: disable=unused-argument
|
||||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
"""Set up the access to Netatmo binary sensor."""
|
"""Set up the access to Netatmo binary sensor."""
|
||||||
netatmo = get_component('netatmo')
|
netatmo = hass.components.netatmo
|
||||||
home = config.get(CONF_HOME)
|
home = config.get(CONF_HOME)
|
||||||
timeout = config.get(CONF_TIMEOUT)
|
timeout = config.get(CONF_TIMEOUT)
|
||||||
if timeout is None:
|
if timeout is None:
|
||||||
|
@ -14,7 +14,7 @@ from homeassistant.components.binary_sensor import (
|
|||||||
from homeassistant.const import CONF_NAME
|
from homeassistant.const import CONF_NAME
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
REQUIREMENTS = ['tapsaff==0.1.3']
|
REQUIREMENTS = ['tapsaff==0.2.0']
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -23,7 +23,7 @@ from homeassistant.helpers.entity import generate_entity_id
|
|||||||
from homeassistant.helpers.event import async_track_state_change
|
from homeassistant.helpers.event import async_track_state_change
|
||||||
from homeassistant.util import utcnow
|
from homeassistant.util import utcnow
|
||||||
|
|
||||||
REQUIREMENTS = ['numpy==1.14.2']
|
REQUIREMENTS = ['numpy==1.14.3']
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -7,7 +7,6 @@ https://home-assistant.io/components/binary_sensor.wemo/
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||||
from homeassistant.loader import get_component
|
|
||||||
|
|
||||||
DEPENDENCIES = ['wemo']
|
DEPENDENCIES = ['wemo']
|
||||||
|
|
||||||
@ -25,18 +24,18 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
|||||||
device = discovery.device_from_description(location, mac)
|
device = discovery.device_from_description(location, mac)
|
||||||
|
|
||||||
if device:
|
if device:
|
||||||
add_devices_callback([WemoBinarySensor(device)])
|
add_devices_callback([WemoBinarySensor(hass, device)])
|
||||||
|
|
||||||
|
|
||||||
class WemoBinarySensor(BinarySensorDevice):
|
class WemoBinarySensor(BinarySensorDevice):
|
||||||
"""Representation a WeMo binary sensor."""
|
"""Representation a WeMo binary sensor."""
|
||||||
|
|
||||||
def __init__(self, device):
|
def __init__(self, hass, device):
|
||||||
"""Initialize the WeMo sensor."""
|
"""Initialize the WeMo sensor."""
|
||||||
self.wemo = device
|
self.wemo = device
|
||||||
self._state = None
|
self._state = None
|
||||||
|
|
||||||
wemo = get_component('wemo')
|
wemo = hass.components.wemo
|
||||||
wemo.SUBSCRIPTION_REGISTRY.register(self.wemo)
|
wemo.SUBSCRIPTION_REGISTRY.register(self.wemo)
|
||||||
wemo.SUBSCRIPTION_REGISTRY.on(self.wemo, None, self._update_callback)
|
wemo.SUBSCRIPTION_REGISTRY.on(self.wemo, None, self._update_callback)
|
||||||
|
|
||||||
|
@ -17,16 +17,17 @@ import homeassistant.helpers.config_validation as cv
|
|||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
REQUIREMENTS = ['holidays==0.9.4']
|
REQUIREMENTS = ['holidays==0.9.5']
|
||||||
|
|
||||||
# List of all countries currently supported by holidays
|
# List of all countries currently supported by holidays
|
||||||
# There seems to be no way to get the list out at runtime
|
# There seems to be no way to get the list out at runtime
|
||||||
ALL_COUNTRIES = ['Australia', 'AU', 'Austria', 'AT', 'Belgium', 'BE', 'Canada',
|
ALL_COUNTRIES = ['Argentina', 'AR', 'Australia', 'AU', 'Austria', 'AT',
|
||||||
'CA', 'Colombia', 'CO', 'Czech', 'CZ', 'Denmark', 'DK',
|
'Belgium', 'BE', 'Canada', 'CA', 'Colombia', 'CO', 'Czech',
|
||||||
'England', 'EuropeanCentralBank', 'ECB', 'TAR', 'Finland',
|
'CZ', 'Denmark', 'DK', 'England', 'EuropeanCentralBank',
|
||||||
'FI', 'France', 'FRA', 'Germany', 'DE', 'Ireland',
|
'ECB', 'TAR', 'Finland', 'FI', 'France', 'FRA', 'Germany',
|
||||||
'Isle of Man', 'Italy', 'IT', 'Japan', 'JP', 'Mexico', 'MX',
|
'DE', 'Hungary', 'HU', 'Ireland', 'Isle of Man', 'Italy',
|
||||||
'Netherlands', 'NL', 'NewZealand', 'NZ', 'Northern Ireland',
|
'IT', 'Japan', 'JP', 'Mexico', 'MX', 'Netherlands', 'NL',
|
||||||
|
'NewZealand', 'NZ', 'Northern Ireland',
|
||||||
'Norway', 'NO', 'Polish', 'PL', 'Portugal', 'PT',
|
'Norway', 'NO', 'Polish', 'PL', 'Portugal', 'PT',
|
||||||
'PortugalExt', 'PTE', 'Scotland', 'Slovenia', 'SI',
|
'PortugalExt', 'PTE', 'Scotland', 'Slovenia', 'SI',
|
||||||
'Slovakia', 'SK', 'South Africa', 'ZA', 'Spain', 'ES',
|
'Slovakia', 'SK', 'South Africa', 'ZA', 'Spain', 'ES',
|
||||||
|
@ -25,30 +25,35 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
for (_, gateway) in hass.data[PY_XIAOMI_GATEWAY].gateways.items():
|
for (_, gateway) in hass.data[PY_XIAOMI_GATEWAY].gateways.items():
|
||||||
for device in gateway.devices['binary_sensor']:
|
for device in gateway.devices['binary_sensor']:
|
||||||
model = device['model']
|
model = device['model']
|
||||||
if model in ['motion', 'sensor_motion.aq2']:
|
if model in ['motion', 'sensor_motion', 'sensor_motion.aq2']:
|
||||||
devices.append(XiaomiMotionSensor(device, hass, gateway))
|
devices.append(XiaomiMotionSensor(device, hass, gateway))
|
||||||
elif model in ['magnet', 'sensor_magnet.aq2']:
|
elif model in ['magnet', 'sensor_magnet', 'sensor_magnet.aq2']:
|
||||||
devices.append(XiaomiDoorSensor(device, gateway))
|
devices.append(XiaomiDoorSensor(device, gateway))
|
||||||
elif model == 'sensor_wleak.aq1':
|
elif model == 'sensor_wleak.aq1':
|
||||||
devices.append(XiaomiWaterLeakSensor(device, gateway))
|
devices.append(XiaomiWaterLeakSensor(device, gateway))
|
||||||
elif model == 'smoke':
|
elif model in ['smoke', 'sensor_smoke']:
|
||||||
devices.append(XiaomiSmokeSensor(device, gateway))
|
devices.append(XiaomiSmokeSensor(device, gateway))
|
||||||
elif model == 'natgas':
|
elif model in ['natgas', 'sensor_natgas']:
|
||||||
devices.append(XiaomiNatgasSensor(device, gateway))
|
devices.append(XiaomiNatgasSensor(device, gateway))
|
||||||
elif model in ['switch', 'sensor_switch.aq2', 'sensor_switch.aq3']:
|
elif model in ['switch', 'sensor_switch',
|
||||||
devices.append(XiaomiButton(device, 'Switch', 'status',
|
'sensor_switch.aq2', 'sensor_switch.aq3']:
|
||||||
|
if 'proto' not in device or int(device['proto'][0:1]) == 1:
|
||||||
|
data_key = 'status'
|
||||||
|
else:
|
||||||
|
data_key = 'channel_0'
|
||||||
|
devices.append(XiaomiButton(device, 'Switch', data_key,
|
||||||
hass, gateway))
|
hass, gateway))
|
||||||
elif model == '86sw1':
|
elif model in ['86sw1', 'sensor_86sw1.aq1']:
|
||||||
devices.append(XiaomiButton(device, 'Wall Switch', 'channel_0',
|
devices.append(XiaomiButton(device, 'Wall Switch', 'channel_0',
|
||||||
hass, gateway))
|
hass, gateway))
|
||||||
elif model == '86sw2':
|
elif model in ['86sw2', 'sensor_86sw2.aq1']:
|
||||||
devices.append(XiaomiButton(device, 'Wall Switch (Left)',
|
devices.append(XiaomiButton(device, 'Wall Switch (Left)',
|
||||||
'channel_0', hass, gateway))
|
'channel_0', hass, gateway))
|
||||||
devices.append(XiaomiButton(device, 'Wall Switch (Right)',
|
devices.append(XiaomiButton(device, 'Wall Switch (Right)',
|
||||||
'channel_1', hass, gateway))
|
'channel_1', hass, gateway))
|
||||||
devices.append(XiaomiButton(device, 'Wall Switch (Both)',
|
devices.append(XiaomiButton(device, 'Wall Switch (Both)',
|
||||||
'dual_channel', hass, gateway))
|
'dual_channel', hass, gateway))
|
||||||
elif model == 'cube':
|
elif model in ['cube', 'sensor_cube']:
|
||||||
devices.append(XiaomiCube(device, hass, gateway))
|
devices.append(XiaomiCube(device, hass, gateway))
|
||||||
add_devices(devices)
|
add_devices(devices)
|
||||||
|
|
||||||
@ -129,8 +134,12 @@ class XiaomiMotionSensor(XiaomiBinarySensor):
|
|||||||
"""Initialize the XiaomiMotionSensor."""
|
"""Initialize the XiaomiMotionSensor."""
|
||||||
self._hass = hass
|
self._hass = hass
|
||||||
self._no_motion_since = 0
|
self._no_motion_since = 0
|
||||||
|
if 'proto' not in device or int(device['proto'][0:1]) == 1:
|
||||||
|
data_key = 'status'
|
||||||
|
else:
|
||||||
|
data_key = 'motion_status'
|
||||||
XiaomiBinarySensor.__init__(self, device, 'Motion Sensor', xiaomi_hub,
|
XiaomiBinarySensor.__init__(self, device, 'Motion Sensor', xiaomi_hub,
|
||||||
'status', 'motion')
|
data_key, 'motion')
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def device_state_attributes(self):
|
def device_state_attributes(self):
|
||||||
|
@ -31,12 +31,21 @@ async def async_setup_platform(hass, config, async_add_devices,
|
|||||||
if discovery_info is None:
|
if discovery_info is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
from zigpy.zcl.clusters.general import OnOff
|
||||||
from zigpy.zcl.clusters.security import IasZone
|
from zigpy.zcl.clusters.security import IasZone
|
||||||
|
if IasZone.cluster_id in discovery_info['in_clusters']:
|
||||||
|
await _async_setup_iaszone(hass, config, async_add_devices,
|
||||||
|
discovery_info)
|
||||||
|
elif OnOff.cluster_id in discovery_info['out_clusters']:
|
||||||
|
await _async_setup_remote(hass, config, async_add_devices,
|
||||||
|
discovery_info)
|
||||||
|
|
||||||
in_clusters = discovery_info['in_clusters']
|
|
||||||
|
|
||||||
|
async def _async_setup_iaszone(hass, config, async_add_devices,
|
||||||
|
discovery_info):
|
||||||
device_class = None
|
device_class = None
|
||||||
cluster = in_clusters[IasZone.cluster_id]
|
from zigpy.zcl.clusters.security import IasZone
|
||||||
|
cluster = discovery_info['in_clusters'][IasZone.cluster_id]
|
||||||
if discovery_info['new_join']:
|
if discovery_info['new_join']:
|
||||||
await cluster.bind()
|
await cluster.bind()
|
||||||
ieee = cluster.endpoint.device.application.ieee
|
ieee = cluster.endpoint.device.application.ieee
|
||||||
@ -53,8 +62,34 @@ async def async_setup_platform(hass, config, async_add_devices,
|
|||||||
async_add_devices([sensor], update_before_add=True)
|
async_add_devices([sensor], update_before_add=True)
|
||||||
|
|
||||||
|
|
||||||
|
async def _async_setup_remote(hass, config, async_add_devices, discovery_info):
|
||||||
|
|
||||||
|
async def safe(coro):
|
||||||
|
"""Run coro, catching ZigBee delivery errors, and ignoring them."""
|
||||||
|
import zigpy.exceptions
|
||||||
|
try:
|
||||||
|
await coro
|
||||||
|
except zigpy.exceptions.DeliveryError as exc:
|
||||||
|
_LOGGER.warning("Ignoring error during setup: %s", exc)
|
||||||
|
|
||||||
|
if discovery_info['new_join']:
|
||||||
|
from zigpy.zcl.clusters.general import OnOff, LevelControl
|
||||||
|
out_clusters = discovery_info['out_clusters']
|
||||||
|
if OnOff.cluster_id in out_clusters:
|
||||||
|
cluster = out_clusters[OnOff.cluster_id]
|
||||||
|
await safe(cluster.bind())
|
||||||
|
await safe(cluster.configure_reporting(0, 0, 600, 1))
|
||||||
|
if LevelControl.cluster_id in out_clusters:
|
||||||
|
cluster = out_clusters[LevelControl.cluster_id]
|
||||||
|
await safe(cluster.bind())
|
||||||
|
await safe(cluster.configure_reporting(0, 1, 600, 1))
|
||||||
|
|
||||||
|
sensor = Switch(**discovery_info)
|
||||||
|
async_add_devices([sensor], update_before_add=True)
|
||||||
|
|
||||||
|
|
||||||
class BinarySensor(zha.Entity, BinarySensorDevice):
|
class BinarySensor(zha.Entity, BinarySensorDevice):
|
||||||
"""THe ZHA Binary Sensor."""
|
"""The ZHA Binary Sensor."""
|
||||||
|
|
||||||
_domain = DOMAIN
|
_domain = DOMAIN
|
||||||
|
|
||||||
@ -102,3 +137,113 @@ class BinarySensor(zha.Entity, BinarySensorDevice):
|
|||||||
state = result.get('zone_status', self._state)
|
state = result.get('zone_status', self._state)
|
||||||
if isinstance(state, (int, uint16_t)):
|
if isinstance(state, (int, uint16_t)):
|
||||||
self._state = result.get('zone_status', self._state) & 3
|
self._state = result.get('zone_status', self._state) & 3
|
||||||
|
|
||||||
|
|
||||||
|
class Switch(zha.Entity, BinarySensorDevice):
|
||||||
|
"""ZHA switch/remote controller/button."""
|
||||||
|
|
||||||
|
_domain = DOMAIN
|
||||||
|
|
||||||
|
class OnOffListener:
|
||||||
|
"""Listener for the OnOff ZigBee cluster."""
|
||||||
|
|
||||||
|
def __init__(self, entity):
|
||||||
|
"""Initialize OnOffListener."""
|
||||||
|
self._entity = entity
|
||||||
|
|
||||||
|
def cluster_command(self, tsn, command_id, args):
|
||||||
|
"""Handle commands received to this cluster."""
|
||||||
|
if command_id in (0x0000, 0x0040):
|
||||||
|
self._entity.set_state(False)
|
||||||
|
elif command_id in (0x0001, 0x0041, 0x0042):
|
||||||
|
self._entity.set_state(True)
|
||||||
|
elif command_id == 0x0002:
|
||||||
|
self._entity.set_state(not self._entity.is_on)
|
||||||
|
|
||||||
|
def attribute_updated(self, attrid, value):
|
||||||
|
"""Handle attribute updates on this cluster."""
|
||||||
|
if attrid == 0:
|
||||||
|
self._entity.set_state(value)
|
||||||
|
|
||||||
|
def zdo_command(self, *args, **kwargs):
|
||||||
|
"""Handle ZDO commands on this cluster."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
class LevelListener:
|
||||||
|
"""Listener for the LevelControl ZigBee cluster."""
|
||||||
|
|
||||||
|
def __init__(self, entity):
|
||||||
|
"""Initialize LevelListener."""
|
||||||
|
self._entity = entity
|
||||||
|
|
||||||
|
def cluster_command(self, tsn, command_id, args):
|
||||||
|
"""Handle commands received to this cluster."""
|
||||||
|
if command_id in (0x0000, 0x0004): # move_to_level, -with_on_off
|
||||||
|
self._entity.set_level(args[0])
|
||||||
|
elif command_id in (0x0001, 0x0005): # move, -with_on_off
|
||||||
|
# We should dim slowly -- for now, just step once
|
||||||
|
rate = args[1]
|
||||||
|
if args[0] == 0xff:
|
||||||
|
rate = 10 # Should read default move rate
|
||||||
|
self._entity.move_level(-rate if args[0] else rate)
|
||||||
|
elif command_id == 0x0002: # step
|
||||||
|
# Step (technically shouldn't change on/off)
|
||||||
|
self._entity.move_level(-args[1] if args[0] else args[1])
|
||||||
|
|
||||||
|
def attribute_update(self, attrid, value):
|
||||||
|
"""Handle attribute updates on this cluster."""
|
||||||
|
if attrid == 0:
|
||||||
|
self._entity.set_level(value)
|
||||||
|
|
||||||
|
def zdo_command(self, *args, **kwargs):
|
||||||
|
"""Handle ZDO commands on this cluster."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
"""Initialize Switch."""
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
self._state = True
|
||||||
|
self._level = 255
|
||||||
|
from zigpy.zcl.clusters import general
|
||||||
|
self._out_listeners = {
|
||||||
|
general.OnOff.cluster_id: self.OnOffListener(self),
|
||||||
|
general.LevelControl.cluster_id: self.LevelListener(self),
|
||||||
|
}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_on(self) -> bool:
|
||||||
|
"""Return true if the binary sensor is on."""
|
||||||
|
return self._state
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device_state_attributes(self):
|
||||||
|
"""Return the device state attributes."""
|
||||||
|
return {'level': self._state and self._level or 0}
|
||||||
|
|
||||||
|
def move_level(self, change):
|
||||||
|
"""Increment the level, setting state if appropriate."""
|
||||||
|
if not self._state and change > 0:
|
||||||
|
self._level = 0
|
||||||
|
self._level = min(255, max(0, self._level + change))
|
||||||
|
self._state = bool(self._level)
|
||||||
|
self.async_schedule_update_ha_state()
|
||||||
|
|
||||||
|
def set_level(self, level):
|
||||||
|
"""Set the level, setting state if appropriate."""
|
||||||
|
self._level = level
|
||||||
|
self._state = bool(self._level)
|
||||||
|
self.async_schedule_update_ha_state()
|
||||||
|
|
||||||
|
def set_state(self, state):
|
||||||
|
"""Set the state."""
|
||||||
|
self._state = state
|
||||||
|
if self._level == 0:
|
||||||
|
self._level = 255
|
||||||
|
self.async_schedule_update_ha_state()
|
||||||
|
|
||||||
|
async def async_update(self):
|
||||||
|
"""Retrieve latest state."""
|
||||||
|
from zigpy.zcl.clusters.general import OnOff
|
||||||
|
result = await zha.safe_read(
|
||||||
|
self._endpoint.out_clusters[OnOff.cluster_id], ['on_off'])
|
||||||
|
self._state = result.get('on_off', self._state)
|
||||||
|
@ -6,6 +6,7 @@ For more details about this component, please refer to the documentation at
|
|||||||
https://home-assistant.io/components/camera/
|
https://home-assistant.io/components/camera/
|
||||||
"""
|
"""
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import base64
|
||||||
import collections
|
import collections
|
||||||
from contextlib import suppress
|
from contextlib import suppress
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
@ -13,20 +14,20 @@ import logging
|
|||||||
import hashlib
|
import hashlib
|
||||||
from random import SystemRandom
|
from random import SystemRandom
|
||||||
|
|
||||||
import aiohttp
|
import attr
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
import async_timeout
|
import async_timeout
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
from homeassistant.const import (ATTR_ENTITY_ID, ATTR_ENTITY_PICTURE)
|
from homeassistant.const import ATTR_ENTITY_ID
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
from homeassistant.loader import bind_hass
|
from homeassistant.loader import bind_hass
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
|
||||||
from homeassistant.helpers.entity import Entity
|
from homeassistant.helpers.entity import Entity
|
||||||
from homeassistant.helpers.entity_component import EntityComponent
|
from homeassistant.helpers.entity_component import EntityComponent
|
||||||
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
|
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
|
||||||
from homeassistant.components.http import HomeAssistantView, KEY_AUTHENTICATED
|
from homeassistant.components.http import HomeAssistantView, KEY_AUTHENTICATED
|
||||||
|
from homeassistant.components import websocket_api
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
DOMAIN = 'camera'
|
DOMAIN = 'camera'
|
||||||
@ -53,6 +54,9 @@ ENTITY_IMAGE_URL = '/api/camera_proxy/{0}?token={1}'
|
|||||||
TOKEN_CHANGE_INTERVAL = timedelta(minutes=5)
|
TOKEN_CHANGE_INTERVAL = timedelta(minutes=5)
|
||||||
_RND = SystemRandom()
|
_RND = SystemRandom()
|
||||||
|
|
||||||
|
FALLBACK_STREAM_INTERVAL = 1 # seconds
|
||||||
|
MIN_STREAM_INTERVAL = 0.5 # seconds
|
||||||
|
|
||||||
CAMERA_SERVICE_SCHEMA = vol.Schema({
|
CAMERA_SERVICE_SCHEMA = vol.Schema({
|
||||||
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
|
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
|
||||||
})
|
})
|
||||||
@ -61,6 +65,20 @@ CAMERA_SERVICE_SNAPSHOT = CAMERA_SERVICE_SCHEMA.extend({
|
|||||||
vol.Required(ATTR_FILENAME): cv.template
|
vol.Required(ATTR_FILENAME): cv.template
|
||||||
})
|
})
|
||||||
|
|
||||||
|
WS_TYPE_CAMERA_THUMBNAIL = 'camera_thumbnail'
|
||||||
|
SCHEMA_WS_CAMERA_THUMBNAIL = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
|
||||||
|
'type': WS_TYPE_CAMERA_THUMBNAIL,
|
||||||
|
'entity_id': cv.entity_id
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@attr.s
|
||||||
|
class Image:
|
||||||
|
"""Represent an image."""
|
||||||
|
|
||||||
|
content_type = attr.ib(type=str)
|
||||||
|
content = attr.ib(type=bytes)
|
||||||
|
|
||||||
|
|
||||||
@bind_hass
|
@bind_hass
|
||||||
def enable_motion_detection(hass, entity_id=None):
|
def enable_motion_detection(hass, entity_id=None):
|
||||||
@ -89,43 +107,40 @@ def async_snapshot(hass, filename, entity_id=None):
|
|||||||
|
|
||||||
|
|
||||||
@bind_hass
|
@bind_hass
|
||||||
@asyncio.coroutine
|
async def async_get_image(hass, entity_id, timeout=10):
|
||||||
def async_get_image(hass, entity_id, timeout=10):
|
|
||||||
"""Fetch an image from a camera entity."""
|
"""Fetch an image from a camera entity."""
|
||||||
websession = async_get_clientsession(hass)
|
component = hass.data.get(DOMAIN)
|
||||||
state = hass.states.get(entity_id)
|
|
||||||
|
|
||||||
if state is None:
|
if component is None:
|
||||||
raise HomeAssistantError(
|
raise HomeAssistantError('Camera component not setup')
|
||||||
"No entity '{0}' for grab an image".format(entity_id))
|
|
||||||
|
|
||||||
url = "{0}{1}".format(
|
camera = component.get_entity(entity_id)
|
||||||
hass.config.api.base_url,
|
|
||||||
state.attributes.get(ATTR_ENTITY_PICTURE)
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
if camera is None:
|
||||||
|
raise HomeAssistantError('Camera not found')
|
||||||
|
|
||||||
|
with suppress(asyncio.CancelledError, asyncio.TimeoutError):
|
||||||
with async_timeout.timeout(timeout, loop=hass.loop):
|
with async_timeout.timeout(timeout, loop=hass.loop):
|
||||||
response = yield from websession.get(url)
|
image = await camera.async_camera_image()
|
||||||
|
|
||||||
if response.status != 200:
|
if image:
|
||||||
raise HomeAssistantError("Error {0} on {1}".format(
|
return Image(camera.content_type, image)
|
||||||
response.status, url))
|
|
||||||
|
|
||||||
image = yield from response.read()
|
raise HomeAssistantError('Unable to get image')
|
||||||
return image
|
|
||||||
|
|
||||||
except (asyncio.TimeoutError, aiohttp.ClientError):
|
|
||||||
raise HomeAssistantError("Can't connect to {0}".format(url))
|
|
||||||
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def async_setup(hass, config):
|
def async_setup(hass, config):
|
||||||
"""Set up the camera component."""
|
"""Set up the camera component."""
|
||||||
component = EntityComponent(_LOGGER, DOMAIN, hass, SCAN_INTERVAL)
|
component = hass.data[DOMAIN] = \
|
||||||
|
EntityComponent(_LOGGER, DOMAIN, hass, SCAN_INTERVAL)
|
||||||
|
|
||||||
hass.http.register_view(CameraImageView(component))
|
hass.http.register_view(CameraImageView(component))
|
||||||
hass.http.register_view(CameraMjpegStream(component))
|
hass.http.register_view(CameraMjpegStream(component))
|
||||||
|
hass.components.websocket_api.async_register_command(
|
||||||
|
WS_TYPE_CAMERA_THUMBNAIL, websocket_camera_thumbnail,
|
||||||
|
SCHEMA_WS_CAMERA_THUMBNAIL
|
||||||
|
)
|
||||||
|
|
||||||
yield from component.async_setup(config)
|
yield from component.async_setup(config)
|
||||||
|
|
||||||
@ -252,19 +267,21 @@ class Camera(Entity):
|
|||||||
"""
|
"""
|
||||||
return self.hass.async_add_job(self.camera_image)
|
return self.hass.async_add_job(self.camera_image)
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def handle_async_still_stream(self, request, interval):
|
||||||
def handle_async_mjpeg_stream(self, request):
|
|
||||||
"""Generate an HTTP MJPEG stream from camera images.
|
"""Generate an HTTP MJPEG stream from camera images.
|
||||||
|
|
||||||
This method must be run in the event loop.
|
This method must be run in the event loop.
|
||||||
"""
|
"""
|
||||||
response = web.StreamResponse()
|
if interval < MIN_STREAM_INTERVAL:
|
||||||
|
raise ValueError("Stream interval must be be > {}"
|
||||||
|
.format(MIN_STREAM_INTERVAL))
|
||||||
|
|
||||||
|
response = web.StreamResponse()
|
||||||
response.content_type = ('multipart/x-mixed-replace; '
|
response.content_type = ('multipart/x-mixed-replace; '
|
||||||
'boundary=--frameboundary')
|
'boundary=--frameboundary')
|
||||||
yield from response.prepare(request)
|
await response.prepare(request)
|
||||||
|
|
||||||
async def write(img_bytes):
|
async def write_to_mjpeg_stream(img_bytes):
|
||||||
"""Write image to stream."""
|
"""Write image to stream."""
|
||||||
await response.write(bytes(
|
await response.write(bytes(
|
||||||
'--frameboundary\r\n'
|
'--frameboundary\r\n'
|
||||||
@ -277,21 +294,21 @@ class Camera(Entity):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
while True:
|
while True:
|
||||||
img_bytes = yield from self.async_camera_image()
|
img_bytes = await self.async_camera_image()
|
||||||
if not img_bytes:
|
if not img_bytes:
|
||||||
break
|
break
|
||||||
|
|
||||||
if img_bytes and img_bytes != last_image:
|
if img_bytes and img_bytes != last_image:
|
||||||
yield from write(img_bytes)
|
await write_to_mjpeg_stream(img_bytes)
|
||||||
|
|
||||||
# Chrome seems to always ignore first picture,
|
# Chrome seems to always ignore first picture,
|
||||||
# print it twice.
|
# print it twice.
|
||||||
if last_image is None:
|
if last_image is None:
|
||||||
yield from write(img_bytes)
|
await write_to_mjpeg_stream(img_bytes)
|
||||||
|
|
||||||
last_image = img_bytes
|
last_image = img_bytes
|
||||||
|
|
||||||
yield from asyncio.sleep(.5)
|
await asyncio.sleep(interval)
|
||||||
|
|
||||||
except asyncio.CancelledError:
|
except asyncio.CancelledError:
|
||||||
_LOGGER.debug("Stream closed by frontend.")
|
_LOGGER.debug("Stream closed by frontend.")
|
||||||
@ -299,7 +316,17 @@ class Camera(Entity):
|
|||||||
|
|
||||||
finally:
|
finally:
|
||||||
if response is not None:
|
if response is not None:
|
||||||
yield from response.write_eof()
|
await response.write_eof()
|
||||||
|
|
||||||
|
async def handle_async_mjpeg_stream(self, request):
|
||||||
|
"""Serve an HTTP MJPEG stream from the camera.
|
||||||
|
|
||||||
|
This method can be overridden by camera plaforms to proxy
|
||||||
|
a direct stream from the camera.
|
||||||
|
This method must be run in the event loop.
|
||||||
|
"""
|
||||||
|
await self.handle_async_still_stream(request,
|
||||||
|
FALLBACK_STREAM_INTERVAL)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def state(self):
|
def state(self):
|
||||||
@ -329,20 +356,20 @@ class Camera(Entity):
|
|||||||
@property
|
@property
|
||||||
def state_attributes(self):
|
def state_attributes(self):
|
||||||
"""Return the camera state attributes."""
|
"""Return the camera state attributes."""
|
||||||
attr = {
|
attrs = {
|
||||||
'access_token': self.access_tokens[-1],
|
'access_token': self.access_tokens[-1],
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.model:
|
if self.model:
|
||||||
attr['model_name'] = self.model
|
attrs['model_name'] = self.model
|
||||||
|
|
||||||
if self.brand:
|
if self.brand:
|
||||||
attr['brand'] = self.brand
|
attrs['brand'] = self.brand
|
||||||
|
|
||||||
if self.motion_detection_enabled:
|
if self.motion_detection_enabled:
|
||||||
attr['motion_detection'] = self.motion_detection_enabled
|
attrs['motion_detection'] = self.motion_detection_enabled
|
||||||
|
|
||||||
return attr
|
return attrs
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def async_update_token(self):
|
def async_update_token(self):
|
||||||
@ -411,7 +438,40 @@ class CameraMjpegStream(CameraView):
|
|||||||
url = '/api/camera_proxy_stream/{entity_id}'
|
url = '/api/camera_proxy_stream/{entity_id}'
|
||||||
name = 'api:camera:stream'
|
name = 'api:camera:stream'
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def handle(self, request, camera):
|
||||||
def handle(self, request, camera):
|
"""Serve camera stream, possibly with interval."""
|
||||||
"""Serve camera image."""
|
interval = request.query.get('interval')
|
||||||
yield from camera.handle_async_mjpeg_stream(request)
|
if interval is None:
|
||||||
|
await camera.handle_async_mjpeg_stream(request)
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Compose camera stream from stills
|
||||||
|
interval = float(request.query.get('interval'))
|
||||||
|
await camera.handle_async_still_stream(request, interval)
|
||||||
|
return
|
||||||
|
except ValueError:
|
||||||
|
return web.Response(status=400)
|
||||||
|
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def websocket_camera_thumbnail(hass, connection, msg):
|
||||||
|
"""Handle get camera thumbnail websocket command.
|
||||||
|
|
||||||
|
Async friendly.
|
||||||
|
"""
|
||||||
|
async def send_camera_still():
|
||||||
|
"""Send a camera still."""
|
||||||
|
try:
|
||||||
|
image = await async_get_image(hass, msg['entity_id'])
|
||||||
|
connection.send_message_outside(websocket_api.result_message(
|
||||||
|
msg['id'], {
|
||||||
|
'content_type': image.content_type,
|
||||||
|
'content': base64.b64encode(image.content).decode('utf-8')
|
||||||
|
}
|
||||||
|
))
|
||||||
|
except HomeAssistantError:
|
||||||
|
connection.send_message_outside(websocket_api.error_message(
|
||||||
|
msg['id'], 'image_fetch_failed', 'Unable to fetch image'))
|
||||||
|
|
||||||
|
hass.async_add_job(send_camera_still())
|
||||||
|
@ -9,7 +9,6 @@ import logging
|
|||||||
import requests
|
import requests
|
||||||
|
|
||||||
from homeassistant.components.camera import Camera
|
from homeassistant.components.camera import Camera
|
||||||
from homeassistant.loader import get_component
|
|
||||||
|
|
||||||
DEPENDENCIES = ['bloomsky']
|
DEPENDENCIES = ['bloomsky']
|
||||||
|
|
||||||
@ -17,7 +16,7 @@ DEPENDENCIES = ['bloomsky']
|
|||||||
# pylint: disable=unused-argument
|
# pylint: disable=unused-argument
|
||||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
"""Set up access to BloomSky cameras."""
|
"""Set up access to BloomSky cameras."""
|
||||||
bloomsky = get_component('bloomsky')
|
bloomsky = hass.components.bloomsky
|
||||||
for device in bloomsky.BLOOMSKY.devices.values():
|
for device in bloomsky.BLOOMSKY.devices.values():
|
||||||
add_devices([BloomSkyCamera(bloomsky.BLOOMSKY, device)])
|
add_devices([BloomSkyCamera(bloomsky.BLOOMSKY, device)])
|
||||||
|
|
||||||
|
@ -11,31 +11,44 @@ import os
|
|||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.const import CONF_NAME
|
from homeassistant.const import CONF_NAME
|
||||||
from homeassistant.components.camera import Camera, PLATFORM_SCHEMA
|
from homeassistant.components.camera import (
|
||||||
|
Camera, CAMERA_SERVICE_SCHEMA, DOMAIN, PLATFORM_SCHEMA)
|
||||||
from homeassistant.helpers import config_validation as cv
|
from homeassistant.helpers import config_validation as cv
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
CONF_FILE_PATH = 'file_path'
|
CONF_FILE_PATH = 'file_path'
|
||||||
|
|
||||||
DEFAULT_NAME = 'Local File'
|
DEFAULT_NAME = 'Local File'
|
||||||
|
SERVICE_UPDATE_FILE_PATH = 'local_file_update_file_path'
|
||||||
|
|
||||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||||
vol.Required(CONF_FILE_PATH): cv.string,
|
vol.Required(CONF_FILE_PATH): cv.string,
|
||||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string
|
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string
|
||||||
})
|
})
|
||||||
|
|
||||||
|
CAMERA_SERVICE_UPDATE_FILE_PATH = CAMERA_SERVICE_SCHEMA.extend({
|
||||||
|
vol.Required(CONF_FILE_PATH): cv.string
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
"""Set up the Camera that works with local files."""
|
"""Set up the Camera that works with local files."""
|
||||||
file_path = config[CONF_FILE_PATH]
|
file_path = config[CONF_FILE_PATH]
|
||||||
|
camera = LocalFile(config[CONF_NAME], file_path)
|
||||||
|
|
||||||
# check filepath given is readable
|
def update_file_path_service(call):
|
||||||
if not os.access(file_path, os.R_OK):
|
"""Update the file path."""
|
||||||
_LOGGER.warning("Could not read camera %s image from file: %s",
|
file_path = call.data.get(CONF_FILE_PATH)
|
||||||
config[CONF_NAME], file_path)
|
camera.update_file_path(file_path)
|
||||||
|
return True
|
||||||
|
|
||||||
add_devices([LocalFile(config[CONF_NAME], file_path)])
|
hass.services.register(
|
||||||
|
DOMAIN,
|
||||||
|
SERVICE_UPDATE_FILE_PATH,
|
||||||
|
update_file_path_service,
|
||||||
|
schema=CAMERA_SERVICE_UPDATE_FILE_PATH)
|
||||||
|
|
||||||
|
add_devices([camera])
|
||||||
|
|
||||||
|
|
||||||
class LocalFile(Camera):
|
class LocalFile(Camera):
|
||||||
@ -46,6 +59,7 @@ class LocalFile(Camera):
|
|||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
self._name = name
|
self._name = name
|
||||||
|
self.check_file_path_access(file_path)
|
||||||
self._file_path = file_path
|
self._file_path = file_path
|
||||||
# Set content type of local file
|
# Set content type of local file
|
||||||
content, _ = mimetypes.guess_type(file_path)
|
content, _ = mimetypes.guess_type(file_path)
|
||||||
@ -61,7 +75,26 @@ class LocalFile(Camera):
|
|||||||
_LOGGER.warning("Could not read camera %s image from file: %s",
|
_LOGGER.warning("Could not read camera %s image from file: %s",
|
||||||
self._name, self._file_path)
|
self._name, self._file_path)
|
||||||
|
|
||||||
|
def check_file_path_access(self, file_path):
|
||||||
|
"""Check that filepath given is readable."""
|
||||||
|
if not os.access(file_path, os.R_OK):
|
||||||
|
_LOGGER.warning("Could not read camera %s image from file: %s",
|
||||||
|
self._name, file_path)
|
||||||
|
|
||||||
|
def update_file_path(self, file_path):
|
||||||
|
"""Update the file_path."""
|
||||||
|
self.check_file_path_access(file_path)
|
||||||
|
self._file_path = file_path
|
||||||
|
self.schedule_update_ha_state()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
"""Return the name of this camera."""
|
"""Return the name of this camera."""
|
||||||
return self._name
|
return self._name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device_state_attributes(self):
|
||||||
|
"""Return the camera state attributes."""
|
||||||
|
return {
|
||||||
|
'file_path': self._file_path,
|
||||||
|
}
|
||||||
|
@ -12,7 +12,6 @@ import voluptuous as vol
|
|||||||
from homeassistant.const import CONF_VERIFY_SSL
|
from homeassistant.const import CONF_VERIFY_SSL
|
||||||
from homeassistant.components.netatmo import CameraData
|
from homeassistant.components.netatmo import CameraData
|
||||||
from homeassistant.components.camera import (Camera, PLATFORM_SCHEMA)
|
from homeassistant.components.camera import (Camera, PLATFORM_SCHEMA)
|
||||||
from homeassistant.loader import get_component
|
|
||||||
from homeassistant.helpers import config_validation as cv
|
from homeassistant.helpers import config_validation as cv
|
||||||
|
|
||||||
DEPENDENCIES = ['netatmo']
|
DEPENDENCIES = ['netatmo']
|
||||||
@ -33,7 +32,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
|||||||
# pylint: disable=unused-argument
|
# pylint: disable=unused-argument
|
||||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
"""Set up access to Netatmo cameras."""
|
"""Set up access to Netatmo cameras."""
|
||||||
netatmo = get_component('netatmo')
|
netatmo = hass.components.netatmo
|
||||||
home = config.get(CONF_HOME)
|
home = config.get(CONF_HOME)
|
||||||
verify_ssl = config.get(CONF_VERIFY_SSL, True)
|
verify_ssl = config.get(CONF_VERIFY_SSL, True)
|
||||||
import lnetatmo
|
import lnetatmo
|
||||||
|
@ -24,6 +24,16 @@ snapshot:
|
|||||||
description: Template of a Filename. Variable is entity_id.
|
description: Template of a Filename. Variable is entity_id.
|
||||||
example: '/tmp/snapshot_{{ entity_id }}'
|
example: '/tmp/snapshot_{{ entity_id }}'
|
||||||
|
|
||||||
|
local_file_update_file_path:
|
||||||
|
description: Update the file_path for a local_file camera.
|
||||||
|
fields:
|
||||||
|
entity_id:
|
||||||
|
description: Name(s) of entities to update.
|
||||||
|
example: 'camera.local_file'
|
||||||
|
file_path:
|
||||||
|
description: Path to the new image file.
|
||||||
|
example: '/images/newimage.jpg'
|
||||||
|
|
||||||
onvif_ptz:
|
onvif_ptz:
|
||||||
description: Pan/Tilt/Zoom service for ONVIF camera.
|
description: Pan/Tilt/Zoom service for ONVIF camera.
|
||||||
fields:
|
fields:
|
||||||
@ -39,4 +49,3 @@ onvif_ptz:
|
|||||||
zoom:
|
zoom:
|
||||||
description: "Zoom. Allowed values: ZOOM_IN, ZOOM_OUT"
|
description: "Zoom. Allowed values: ZOOM_IN, ZOOM_OUT"
|
||||||
example: "ZOOM_IN"
|
example: "ZOOM_IN"
|
||||||
|
|
||||||
|
@ -13,7 +13,6 @@ from homeassistant.components.climate import (
|
|||||||
STATE_HEAT, STATE_IDLE, ClimateDevice, PLATFORM_SCHEMA,
|
STATE_HEAT, STATE_IDLE, ClimateDevice, PLATFORM_SCHEMA,
|
||||||
SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE, SUPPORT_AWAY_MODE)
|
SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE, SUPPORT_AWAY_MODE)
|
||||||
from homeassistant.util import Throttle
|
from homeassistant.util import Throttle
|
||||||
from homeassistant.loader import get_component
|
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
DEPENDENCIES = ['netatmo']
|
DEPENDENCIES = ['netatmo']
|
||||||
@ -42,7 +41,7 @@ SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE |
|
|||||||
|
|
||||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
"""Set up the NetAtmo Thermostat."""
|
"""Set up the NetAtmo Thermostat."""
|
||||||
netatmo = get_component('netatmo')
|
netatmo = hass.components.netatmo
|
||||||
device = config.get(CONF_RELAY)
|
device = config.get(CONF_RELAY)
|
||||||
|
|
||||||
import lnetatmo
|
import lnetatmo
|
||||||
|
@ -21,6 +21,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
|||||||
from homeassistant.util import dt as dt_util
|
from homeassistant.util import dt as dt_util
|
||||||
from homeassistant.components.alexa import smart_home as alexa_sh
|
from homeassistant.components.alexa import smart_home as alexa_sh
|
||||||
from homeassistant.components.google_assistant import helpers as ga_h
|
from homeassistant.components.google_assistant import helpers as ga_h
|
||||||
|
from homeassistant.components.google_assistant import const as ga_c
|
||||||
|
|
||||||
from . import http_api, iot
|
from . import http_api, iot
|
||||||
from .const import CONFIG_DIR, DOMAIN, SERVERS
|
from .const import CONFIG_DIR, DOMAIN, SERVERS
|
||||||
@ -52,7 +53,8 @@ ALEXA_ENTITY_SCHEMA = vol.Schema({
|
|||||||
|
|
||||||
GOOGLE_ENTITY_SCHEMA = vol.Schema({
|
GOOGLE_ENTITY_SCHEMA = vol.Schema({
|
||||||
vol.Optional(CONF_NAME): cv.string,
|
vol.Optional(CONF_NAME): cv.string,
|
||||||
vol.Optional(CONF_ALIASES): vol.All(cv.ensure_list, [cv.string])
|
vol.Optional(CONF_ALIASES): vol.All(cv.ensure_list, [cv.string]),
|
||||||
|
vol.Optional(ga_c.CONF_ROOM_HINT): cv.string,
|
||||||
})
|
})
|
||||||
|
|
||||||
ASSISTANT_SCHEMA = vol.Schema({
|
ASSISTANT_SCHEMA = vol.Schema({
|
||||||
|
@ -15,7 +15,7 @@ from homeassistant.const import (
|
|||||||
CONF_IP_ADDRESS, CONF_NAME)
|
CONF_IP_ADDRESS, CONF_NAME)
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
REQUIREMENTS = ['pygogogate2==0.0.3']
|
REQUIREMENTS = ['pygogogate2==0.0.7']
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -79,5 +79,7 @@ class TahomaCover(TahomaDevice, CoverDevice):
|
|||||||
if self.tahoma_device.type == \
|
if self.tahoma_device.type == \
|
||||||
'io:RollerShutterWithLowSpeedManagementIOComponent':
|
'io:RollerShutterWithLowSpeedManagementIOComponent':
|
||||||
self.apply_action('setPosition', 'secured')
|
self.apply_action('setPosition', 'secured')
|
||||||
|
elif self.tahoma_device.type == 'rts:BlindRTSComponent':
|
||||||
|
self.apply_action('my')
|
||||||
else:
|
else:
|
||||||
self.apply_action('stopIdentify')
|
self.apply_action('stopIdentify')
|
||||||
|
25
homeassistant/components/deconz/.translations/bg.json
Normal file
25
homeassistant/components/deconz/.translations/bg.json
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"abort": {
|
||||||
|
"no_bridges": "\u041d\u0435 \u0441\u0430 \u043e\u0442\u043a\u0440\u0438\u0442\u0438 \u043c\u043e\u0441\u0442\u043e\u0432\u0435 deCONZ",
|
||||||
|
"one_instance_only": "\u041a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 \u043f\u043e\u0434\u0434\u044a\u0440\u0436\u0430 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u043e \u043a\u043e\u043f\u0438\u0435 \u043d\u0430 deCONZ"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"no_key": "\u041d\u0435 \u043c\u043e\u0436\u0430 \u0434\u0430 \u0441\u0435 \u043f\u043e\u043b\u0443\u0447\u0438 API \u043a\u043b\u044e\u0447"
|
||||||
|
},
|
||||||
|
"step": {
|
||||||
|
"init": {
|
||||||
|
"data": {
|
||||||
|
"host": "\u0425\u043e\u0441\u0442",
|
||||||
|
"port": "\u041f\u043e\u0440\u0442 (\u0441\u0442\u043e\u0439\u043d\u043e\u0441\u0442 \u043f\u043e \u043f\u043e\u0434\u0440\u0430\u0437\u0431\u0438\u0440\u0430\u043d\u0435: '80')"
|
||||||
|
},
|
||||||
|
"title": "\u0414\u0435\u0444\u0438\u043d\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 deCONZ \u0448\u043b\u044e\u0437"
|
||||||
|
},
|
||||||
|
"link": {
|
||||||
|
"description": "\u041e\u0442\u043a\u043b\u044e\u0447\u0438 deCONZ \u0448\u043b\u044e\u0437\u0430 \u0437\u0430 \u0434\u0430 \u0441\u0435 \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0430 \u0441 Home Assistant.\n\n1. \u041e\u0442\u0432\u043e\u0440\u0435\u0442\u0435 \u0441\u0438\u0441\u0442\u0435\u043c\u043d\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u043d\u0430 deCONZ\n2. \u041d\u0430\u0442\u0438\u0441\u043d\u0435\u0442\u0435 \u0431\u0443\u0442\u043e\u043d\u0430 \"Unlock Gateway\"",
|
||||||
|
"title": "\u0412\u0440\u044a\u0437\u043a\u0430 \u0441 deCONZ"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"title": "deCONZ"
|
||||||
|
}
|
||||||
|
}
|
26
homeassistant/components/deconz/.translations/cy.json
Normal file
26
homeassistant/components/deconz/.translations/cy.json
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"abort": {
|
||||||
|
"already_configured": "Pont eisoes wedi'i ffurfweddu",
|
||||||
|
"no_bridges": "Dim pontydd deCONZ wedi eu darganfod",
|
||||||
|
"one_instance_only": "Elfen dim ond yn cefnogi enghraifft deCONZ"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"no_key": "Methu cael allwedd API"
|
||||||
|
},
|
||||||
|
"step": {
|
||||||
|
"init": {
|
||||||
|
"data": {
|
||||||
|
"host": "Gwesteiwr",
|
||||||
|
"port": "Port (gwerth diofyn: '80')"
|
||||||
|
},
|
||||||
|
"title": "Diffiniwch porth dad-adeiladu"
|
||||||
|
},
|
||||||
|
"link": {
|
||||||
|
"description": "Datgloi eich porth deCONZ i gofrestru gyda Cynorthwydd Cartref.\n\n1. Ewch i osodiadau system deCONZ \n2. Bwyso botwm \"Datgloi porth\"",
|
||||||
|
"title": "Cysylltu \u00e2 deCONZ"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"title": "deCONZ"
|
||||||
|
}
|
||||||
|
}
|
11
homeassistant/components/deconz/.translations/da.json
Normal file
11
homeassistant/components/deconz/.translations/da.json
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"step": {
|
||||||
|
"init": {
|
||||||
|
"data": {
|
||||||
|
"host": "V\u00e6rt"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
26
homeassistant/components/deconz/.translations/de.json
Normal file
26
homeassistant/components/deconz/.translations/de.json
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"abort": {
|
||||||
|
"already_configured": "Bridge ist bereits konfiguriert",
|
||||||
|
"no_bridges": "Keine deCON-Bridges entdeckt",
|
||||||
|
"one_instance_only": "Komponente unterst\u00fctzt nur eine deCONZ-Instanz"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"no_key": "Es konnte kein API-Schl\u00fcssel abgerufen werden"
|
||||||
|
},
|
||||||
|
"step": {
|
||||||
|
"init": {
|
||||||
|
"data": {
|
||||||
|
"host": "Host",
|
||||||
|
"port": "Port (Standartwert : '80')"
|
||||||
|
},
|
||||||
|
"title": "Definieren Sie den deCONZ-Gateway"
|
||||||
|
},
|
||||||
|
"link": {
|
||||||
|
"description": "Entsperren Sie Ihr deCONZ-Gateway, um sich bei Home Assistant zu registrieren. \n\n 1. Gehen Sie zu den deCONZ-Systemeinstellungen \n 2. Dr\u00fccken Sie die Taste \"Gateway entsperren\"",
|
||||||
|
"title": "Mit deCONZ verbinden"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"title": "deCONZ"
|
||||||
|
}
|
||||||
|
}
|
@ -1,26 +1,26 @@
|
|||||||
{
|
{
|
||||||
"config": {
|
"config": {
|
||||||
"title": "deCONZ",
|
|
||||||
"step": {
|
|
||||||
"init": {
|
|
||||||
"title": "Define deCONZ gateway",
|
|
||||||
"data": {
|
|
||||||
"host": "Host",
|
|
||||||
"port": "Port (default value: '80')"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"link": {
|
|
||||||
"title": "Link with deCONZ",
|
|
||||||
"description": "Unlock your deCONZ gateway to register with Home Assistant.\n\n1. Go to deCONZ system settings\n2. Press \"Unlock Gateway\" button"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"error": {
|
|
||||||
"no_key": "Couldn't get an API key"
|
|
||||||
},
|
|
||||||
"abort": {
|
"abort": {
|
||||||
"already_configured": "Bridge is already configured",
|
"already_configured": "Bridge is already configured",
|
||||||
"no_bridges": "No deCONZ bridges discovered",
|
"no_bridges": "No deCONZ bridges discovered",
|
||||||
"one_instance_only": "Component only supports one deCONZ instance"
|
"one_instance_only": "Component only supports one deCONZ instance"
|
||||||
}
|
},
|
||||||
|
"error": {
|
||||||
|
"no_key": "Couldn't get an API key"
|
||||||
|
},
|
||||||
|
"step": {
|
||||||
|
"init": {
|
||||||
|
"data": {
|
||||||
|
"host": "Host",
|
||||||
|
"port": "Port (default value: '80')"
|
||||||
|
},
|
||||||
|
"title": "Define deCONZ gateway"
|
||||||
|
},
|
||||||
|
"link": {
|
||||||
|
"description": "Unlock your deCONZ gateway to register with Home Assistant.\n\n1. Go to deCONZ system settings\n2. Press \"Unlock Gateway\" button",
|
||||||
|
"title": "Link with deCONZ"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"title": "deCONZ"
|
||||||
}
|
}
|
||||||
}
|
}
|
22
homeassistant/components/deconz/.translations/hu.json
Normal file
22
homeassistant/components/deconz/.translations/hu.json
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"abort": {
|
||||||
|
"one_instance_only": "Ez a komponens csak egy deCONZ egys\u00e9get t\u00e1mogat"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"no_key": "API kulcs lek\u00e9r\u00e9se nem siker\u00fclt"
|
||||||
|
},
|
||||||
|
"step": {
|
||||||
|
"init": {
|
||||||
|
"data": {
|
||||||
|
"host": "H\u00e1zigazda (Host)",
|
||||||
|
"port": "Port (alap\u00e9rtelmezett \u00e9rt\u00e9k: '80')"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"link": {
|
||||||
|
"title": "Kapcsol\u00f3d\u00e1s a deCONZ-hoz"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"title": "deCONZ"
|
||||||
|
}
|
||||||
|
}
|
26
homeassistant/components/deconz/.translations/ko.json
Normal file
26
homeassistant/components/deconz/.translations/ko.json
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"abort": {
|
||||||
|
"already_configured": "\ube0c\ub9bf\uc9c0\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
|
||||||
|
"no_bridges": "\ubc1c\uacac\ub41c deCONZ \ube0c\ub9bf\uc9c0\uac00 \uc5c6\uc2b5\ub2c8\ub2e4",
|
||||||
|
"one_instance_only": "\uad6c\uc131\uc694\uc18c\ub294 \ud558\ub098\uc758 deCONZ \uc778\uc2a4\ud134\uc2a4 \ub9cc \uc9c0\uc6d0\ud569\ub2c8\ub2e4"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"no_key": "API \ud0a4\ub97c \uac00\uc838\uc62c \uc218 \uc5c6\uc2b5\ub2c8\ub2e4"
|
||||||
|
},
|
||||||
|
"step": {
|
||||||
|
"init": {
|
||||||
|
"data": {
|
||||||
|
"host": "\ud638\uc2a4\ud2b8",
|
||||||
|
"port": "\ud3ec\ud2b8 (\uae30\ubcf8\uac12: '80')"
|
||||||
|
},
|
||||||
|
"title": "deCONZ \uac8c\uc774\ud2b8\uc6e8\uc774 \uc815\uc758"
|
||||||
|
},
|
||||||
|
"link": {
|
||||||
|
"description": "deCONZ \uac8c\uc774\ud2b8\uc6e8\uc774\ub97c \uc5b8\ub77d\ud558\uc5ec Home Assistant \uc5d0 \uc5f0\uacb0\ud558\uae30\n\n1. deCONZ \uc2dc\uc2a4\ud15c \uc124\uc815\uc73c\ub85c \uc774\ub3d9\ud558\uc138\uc694\n2. \"Unlock Gateway\" \ubc84\ud2bc\uc744 \ub204\ub974\uc138\uc694 ",
|
||||||
|
"title": "deCONZ \uc640 \uc5f0\uacb0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"title": "deCONZ"
|
||||||
|
}
|
||||||
|
}
|
26
homeassistant/components/deconz/.translations/lb.json
Normal file
26
homeassistant/components/deconz/.translations/lb.json
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"abort": {
|
||||||
|
"already_configured": "Bridge ass schon konfigur\u00e9iert",
|
||||||
|
"no_bridges": "Keng dECONZ bridges fonnt",
|
||||||
|
"one_instance_only": "Komponent \u00ebnnerst\u00ebtzt n\u00ebmmen eng deCONZ Instanz"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"no_key": "Konnt keen API Schl\u00ebssel kr\u00e9ien"
|
||||||
|
},
|
||||||
|
"step": {
|
||||||
|
"init": {
|
||||||
|
"data": {
|
||||||
|
"host": "Host",
|
||||||
|
"port": "Port (Standard Wert: '80')"
|
||||||
|
},
|
||||||
|
"title": "deCONZ gateway d\u00e9fin\u00e9ieren"
|
||||||
|
},
|
||||||
|
"link": {
|
||||||
|
"description": "Entsperrt \u00e4r deCONZ gateway fir se mat Home Assistant ze registr\u00e9ieren.\n\n1. Gidd op\u00a0deCONZ System Astellungen\n2. Dr\u00e9ckt \"Unlock\" Gateway Kn\u00e4ppchen",
|
||||||
|
"title": "Link mat deCONZ"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"title": "deCONZ"
|
||||||
|
}
|
||||||
|
}
|
26
homeassistant/components/deconz/.translations/nl.json
Normal file
26
homeassistant/components/deconz/.translations/nl.json
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"abort": {
|
||||||
|
"already_configured": "Bridge is al geconfigureerd",
|
||||||
|
"no_bridges": "Geen deCONZ bruggen ontdekt",
|
||||||
|
"one_instance_only": "Component ondersteunt slechts \u00e9\u00e9n deCONZ instance"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"no_key": "Kon geen API-sleutel ophalen"
|
||||||
|
},
|
||||||
|
"step": {
|
||||||
|
"init": {
|
||||||
|
"data": {
|
||||||
|
"host": "Host",
|
||||||
|
"port": "Poort (standaard: '80')"
|
||||||
|
},
|
||||||
|
"title": "Definieer deCONZ gateway"
|
||||||
|
},
|
||||||
|
"link": {
|
||||||
|
"description": "Ontgrendel je deCONZ gateway om te registreren met Home Assistant.\n\n1. Ga naar deCONZ systeeminstellingen\n2. Druk op de knop \"Gateway ontgrendelen\"",
|
||||||
|
"title": "Koppel met deCONZ"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"title": "deCONZ"
|
||||||
|
}
|
||||||
|
}
|
26
homeassistant/components/deconz/.translations/no.json
Normal file
26
homeassistant/components/deconz/.translations/no.json
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"abort": {
|
||||||
|
"already_configured": "Broen er allerede konfigurert",
|
||||||
|
"no_bridges": "Ingen deCONZ broer oppdaget",
|
||||||
|
"one_instance_only": "Komponenten st\u00f8tter bare \u00e9n deCONZ forekomst"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"no_key": "Kunne ikke f\u00e5 en API-n\u00f8kkel"
|
||||||
|
},
|
||||||
|
"step": {
|
||||||
|
"init": {
|
||||||
|
"data": {
|
||||||
|
"host": "Vert",
|
||||||
|
"port": "Port (standardverdi: '80')"
|
||||||
|
},
|
||||||
|
"title": "Definer deCONZ-gatewayen"
|
||||||
|
},
|
||||||
|
"link": {
|
||||||
|
"description": "L\u00e5s opp deCONZ-gatewayen din for \u00e5 registrere deg med Home Assistant. \n\n 1. G\u00e5 til deCONZ-systeminnstillinger \n 2. Trykk p\u00e5 \"L\u00e5s opp gateway\" knappen",
|
||||||
|
"title": "Koble til deCONZ"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"title": "deCONZ"
|
||||||
|
}
|
||||||
|
}
|
26
homeassistant/components/deconz/.translations/pl.json
Normal file
26
homeassistant/components/deconz/.translations/pl.json
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"abort": {
|
||||||
|
"already_configured": "Mostek jest ju\u017c skonfigurowany",
|
||||||
|
"no_bridges": "Nie odkryto mostk\u00f3w deCONZ",
|
||||||
|
"one_instance_only": "Komponent obs\u0142uguje tylko jedn\u0105 instancj\u0119 deCONZ"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"no_key": "Nie mo\u017cna uzyska\u0107 klucza API"
|
||||||
|
},
|
||||||
|
"step": {
|
||||||
|
"init": {
|
||||||
|
"data": {
|
||||||
|
"host": "Host",
|
||||||
|
"port": "Port (warto\u015b\u0107 domy\u015blna: \"80\")"
|
||||||
|
},
|
||||||
|
"title": "Zdefiniuj bramk\u0119 deCONZ"
|
||||||
|
},
|
||||||
|
"link": {
|
||||||
|
"description": "Odblokuj bramk\u0119 deCONZ, aby zarejestrowa\u0107 j\u0105 w Home Assistant. \n\n 1. Przejd\u017a do ustawie\u0144 systemu deCONZ \n 2. Naci\u015bnij przycisk \"Odblokuj bramk\u0119\"",
|
||||||
|
"title": "Po\u0142\u0105cz z deCONZ"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"title": "deCONZ"
|
||||||
|
}
|
||||||
|
}
|
7
homeassistant/components/deconz/.translations/pt.json
Normal file
7
homeassistant/components/deconz/.translations/pt.json
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"abort": {
|
||||||
|
"already_configured": "Bridge j\u00e1 est\u00e1 configurada"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
26
homeassistant/components/deconz/.translations/ru.json
Normal file
26
homeassistant/components/deconz/.translations/ru.json
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"abort": {
|
||||||
|
"already_configured": "\u0428\u043b\u044e\u0437 \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d",
|
||||||
|
"no_bridges": "\u0428\u043b\u044e\u0437\u044b deCONZ \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b",
|
||||||
|
"one_instance_only": "\u041a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442 \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u0438\u043d \u044d\u043a\u0437\u0435\u043c\u043f\u043b\u044f\u0440 deCONZ"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"no_key": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u043a\u043b\u044e\u0447 API"
|
||||||
|
},
|
||||||
|
"step": {
|
||||||
|
"init": {
|
||||||
|
"data": {
|
||||||
|
"host": "\u0425\u043e\u0441\u0442",
|
||||||
|
"port": "\u041f\u043e\u0440\u0442 (\u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e: '80')"
|
||||||
|
},
|
||||||
|
"title": "\u041e\u043f\u0440\u0435\u0434\u0435\u043b\u0438\u0442\u044c \u0448\u043b\u044e\u0437 deCONZ"
|
||||||
|
},
|
||||||
|
"link": {
|
||||||
|
"description": "\u0420\u0430\u0437\u0431\u043b\u043e\u043a\u0438\u0440\u0443\u0439\u0442\u0435 \u0448\u043b\u044e\u0437 deCONZ \u0434\u043b\u044f \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u0438 \u0432 Home Assistant:\n\n1. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043a \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430\u043c \u0441\u0438\u0441\u0442\u0435\u043c\u044b deCONZ\n2. \u041d\u0430\u0436\u043c\u0438\u0442\u0435 \u043a\u043d\u043e\u043f\u043a\u0443 \u00ab\u0420\u0430\u0437\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0448\u043b\u044e\u0437\u00bb",
|
||||||
|
"title": "\u0421\u0432\u044f\u0437\u044c \u0441 deCONZ"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"title": "deCONZ"
|
||||||
|
}
|
||||||
|
}
|
26
homeassistant/components/deconz/.translations/sl.json
Normal file
26
homeassistant/components/deconz/.translations/sl.json
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"abort": {
|
||||||
|
"already_configured": "Most je \u017ee nastavljen",
|
||||||
|
"no_bridges": "Ni odkritih mostov deCONZ",
|
||||||
|
"one_instance_only": "Komponenta podpira le en primerek deCONZ"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"no_key": "Klju\u010da API ni mogo\u010de dobiti"
|
||||||
|
},
|
||||||
|
"step": {
|
||||||
|
"init": {
|
||||||
|
"data": {
|
||||||
|
"host": "Gostitelj",
|
||||||
|
"port": "Vrata (privzeta vrednost: '80')"
|
||||||
|
},
|
||||||
|
"title": "Dolo\u010dite deCONZ prehod"
|
||||||
|
},
|
||||||
|
"link": {
|
||||||
|
"description": "Odklenite va\u0161 deCONZ gateway za registracijo z Home Assistant-om. \n1. Pojdite v deCONT sistemske nastavitve\n2. Pritisnite tipko \"odkleni prehod\"",
|
||||||
|
"title": "Povezava z deCONZ"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"title": "deCONZ"
|
||||||
|
}
|
||||||
|
}
|
26
homeassistant/components/deconz/.translations/zh-Hans.json
Normal file
26
homeassistant/components/deconz/.translations/zh-Hans.json
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"abort": {
|
||||||
|
"already_configured": "\u6865\u63a5\u5668\u5df2\u914d\u7f6e\u5b8c\u6210",
|
||||||
|
"no_bridges": "\u6ca1\u6709\u53d1\u73b0 deCONZ \u7684\u6865\u63a5\u8bbe\u5907",
|
||||||
|
"one_instance_only": "\u7ec4\u4ef6\u53ea\u652f\u6301\u4e00\u4e2a deCONZ \u5b9e\u4f8b"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"no_key": "\u65e0\u6cd5\u83b7\u53d6 API \u5bc6\u94a5"
|
||||||
|
},
|
||||||
|
"step": {
|
||||||
|
"init": {
|
||||||
|
"data": {
|
||||||
|
"host": "\u4e3b\u673a",
|
||||||
|
"port": "\u7aef\u53e3\uff08\u9ed8\u8ba4\u503c\uff1a'80'\uff09"
|
||||||
|
},
|
||||||
|
"title": "\u5b9a\u4e49 deCONZ \u7f51\u5173"
|
||||||
|
},
|
||||||
|
"link": {
|
||||||
|
"description": "\u89e3\u9501\u60a8\u7684 deCONZ \u7f51\u5173\u4ee5\u6ce8\u518c\u5230 Home Assistant\u3002 \n\n 1. \u524d\u5f80 deCONZ \u7cfb\u7edf\u8bbe\u7f6e\n 2. \u70b9\u51fb\u201c\u89e3\u9501\u7f51\u5173\u201d\u6309\u94ae",
|
||||||
|
"title": "\u8fde\u63a5 deCONZ"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"title": "deCONZ"
|
||||||
|
}
|
||||||
|
}
|
25
homeassistant/components/deconz/.translations/zh-Hant.json
Normal file
25
homeassistant/components/deconz/.translations/zh-Hant.json
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"abort": {
|
||||||
|
"no_bridges": "\u672a\u641c\u5c0b\u5230 deCONZ Bridfe",
|
||||||
|
"one_instance_only": "\u7d44\u4ef6\u50c5\u652f\u63f4\u4e00\u7d44 deCONZ \u5be6\u4f8b"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"no_key": "\u7121\u6cd5\u53d6\u5f97 API key"
|
||||||
|
},
|
||||||
|
"step": {
|
||||||
|
"init": {
|
||||||
|
"data": {
|
||||||
|
"host": "\u4e3b\u6a5f\u7aef",
|
||||||
|
"port": "\u901a\u8a0a\u57e0\uff08\u9810\u8a2d\u503c\uff1a'80'\uff09"
|
||||||
|
},
|
||||||
|
"title": "\u5b9a\u7fa9 deCONZ \u7db2\u95dc"
|
||||||
|
},
|
||||||
|
"link": {
|
||||||
|
"description": "\u89e3\u9664 deCONZ \u7db2\u95dc\u9396\u5b9a\uff0c\u4ee5\u65bc Home Assistant \u9032\u884c\u8a3b\u518a\u3002\n\n1. \u9032\u5165 deCONZ \u7cfb\u7d71\u8a2d\u5b9a\n2. \u6309\u4e0b\u300c\u89e3\u9664\u7db2\u95dc\u9396\u5b9a\uff08Unlock Gateway\uff09\u300d\u6309\u9215",
|
||||||
|
"title": "\u9023\u7d50\u81f3 deCONZ"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"title": "deCONZ"
|
||||||
|
}
|
||||||
|
}
|
@ -7,17 +7,22 @@ https://home-assistant.io/components/deconz/
|
|||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONF_API_KEY, CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP)
|
CONF_API_KEY, CONF_EVENT, CONF_HOST,
|
||||||
from homeassistant.core import callback
|
CONF_ID, CONF_PORT, EVENT_HOMEASSISTANT_STOP)
|
||||||
from homeassistant.helpers import (
|
from homeassistant.core import EventOrigin, callback
|
||||||
aiohttp_client, discovery, config_validation as cv)
|
from homeassistant.helpers import aiohttp_client, config_validation as cv
|
||||||
|
from homeassistant.helpers.dispatcher import (
|
||||||
|
async_dispatcher_connect, async_dispatcher_send)
|
||||||
|
from homeassistant.util import slugify
|
||||||
from homeassistant.util.json import load_json
|
from homeassistant.util.json import load_json
|
||||||
|
|
||||||
# Loading the config flow file will register the flow
|
# Loading the config flow file will register the flow
|
||||||
from .config_flow import configured_hosts
|
from .config_flow import configured_hosts
|
||||||
from .const import CONFIG_FILE, DATA_DECONZ_ID, DOMAIN, _LOGGER
|
from .const import (
|
||||||
|
CONFIG_FILE, DATA_DECONZ_EVENT, DATA_DECONZ_ID,
|
||||||
|
DATA_DECONZ_UNSUB, DOMAIN, _LOGGER)
|
||||||
|
|
||||||
REQUIREMENTS = ['pydeconz==36']
|
REQUIREMENTS = ['pydeconz==37']
|
||||||
|
|
||||||
CONFIG_SCHEMA = vol.Schema({
|
CONFIG_SCHEMA = vol.Schema({
|
||||||
DOMAIN: vol.Schema({
|
DOMAIN: vol.Schema({
|
||||||
@ -27,6 +32,8 @@ CONFIG_SCHEMA = vol.Schema({
|
|||||||
})
|
})
|
||||||
}, extra=vol.ALLOW_EXTRA)
|
}, extra=vol.ALLOW_EXTRA)
|
||||||
|
|
||||||
|
SERVICE_DECONZ = 'configure'
|
||||||
|
|
||||||
SERVICE_FIELD = 'field'
|
SERVICE_FIELD = 'field'
|
||||||
SERVICE_ENTITY = 'entity'
|
SERVICE_ENTITY = 'entity'
|
||||||
SERVICE_DATA = 'data'
|
SERVICE_DATA = 'data'
|
||||||
@ -58,28 +65,27 @@ async def async_setup(hass, config):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(hass, entry):
|
async def async_setup_entry(hass, config_entry):
|
||||||
"""Set up a deCONZ bridge for a config entry."""
|
"""Set up a deCONZ bridge for a config entry.
|
||||||
if DOMAIN in hass.data:
|
|
||||||
_LOGGER.error(
|
|
||||||
"Config entry failed since one deCONZ instance already exists")
|
|
||||||
return False
|
|
||||||
result = await async_setup_deconz(hass, None, entry.data)
|
|
||||||
if result:
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_deconz(hass, config, deconz_config):
|
|
||||||
"""Set up a deCONZ session.
|
|
||||||
|
|
||||||
Load config, group, light and sensor data for server information.
|
Load config, group, light and sensor data for server information.
|
||||||
Start websocket for push notification of state changes from deCONZ.
|
Start websocket for push notification of state changes from deCONZ.
|
||||||
"""
|
"""
|
||||||
_LOGGER.debug("deCONZ config %s", deconz_config)
|
|
||||||
from pydeconz import DeconzSession
|
from pydeconz import DeconzSession
|
||||||
|
if DOMAIN in hass.data:
|
||||||
|
_LOGGER.error(
|
||||||
|
"Config entry failed since one deCONZ instance already exists")
|
||||||
|
return False
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_add_device_callback(device_type, device):
|
||||||
|
"""Called when a new device has been created in deCONZ."""
|
||||||
|
async_dispatcher_send(
|
||||||
|
hass, 'deconz_new_{}'.format(device_type), [device])
|
||||||
|
|
||||||
session = aiohttp_client.async_get_clientsession(hass)
|
session = aiohttp_client.async_get_clientsession(hass)
|
||||||
deconz = DeconzSession(hass.loop, session, **deconz_config)
|
deconz = DeconzSession(hass.loop, session, **config_entry.data,
|
||||||
|
async_add_device=async_add_device_callback)
|
||||||
result = await deconz.async_load_parameters()
|
result = await deconz.async_load_parameters()
|
||||||
if result is False:
|
if result is False:
|
||||||
_LOGGER.error("Failed to communicate with deCONZ")
|
_LOGGER.error("Failed to communicate with deCONZ")
|
||||||
@ -87,10 +93,25 @@ async def async_setup_deconz(hass, config, deconz_config):
|
|||||||
|
|
||||||
hass.data[DOMAIN] = deconz
|
hass.data[DOMAIN] = deconz
|
||||||
hass.data[DATA_DECONZ_ID] = {}
|
hass.data[DATA_DECONZ_ID] = {}
|
||||||
|
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']:
|
||||||
hass.async_add_job(discovery.async_load_platform(
|
hass.async_add_job(hass.config_entries.async_forward_entry_setup(
|
||||||
hass, component, DOMAIN, {}, config))
|
config_entry, component))
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_add_remote(sensors):
|
||||||
|
"""Setup remote from deCONZ."""
|
||||||
|
from pydeconz.sensor import SWITCH as DECONZ_REMOTE
|
||||||
|
for sensor in sensors:
|
||||||
|
if sensor.type in DECONZ_REMOTE:
|
||||||
|
hass.data[DATA_DECONZ_EVENT].append(DeconzEvent(hass, sensor))
|
||||||
|
hass.data[DATA_DECONZ_UNSUB].append(
|
||||||
|
async_dispatcher_connect(hass, 'deconz_new_sensor', async_add_remote))
|
||||||
|
|
||||||
|
async_add_remote(deconz.sensors.values())
|
||||||
|
|
||||||
deconz.start()
|
deconz.start()
|
||||||
|
|
||||||
async def async_configure(call):
|
async def async_configure(call):
|
||||||
@ -121,7 +142,7 @@ async def async_setup_deconz(hass, config, deconz_config):
|
|||||||
return
|
return
|
||||||
await deconz.async_put_state(field, data)
|
await deconz.async_put_state(field, data)
|
||||||
hass.services.async_register(
|
hass.services.async_register(
|
||||||
DOMAIN, 'configure', async_configure, schema=SERVICE_SCHEMA)
|
DOMAIN, SERVICE_DECONZ, async_configure, schema=SERVICE_SCHEMA)
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def deconz_shutdown(event):
|
def deconz_shutdown(event):
|
||||||
@ -136,3 +157,43 @@ async def async_setup_deconz(hass, config, deconz_config):
|
|||||||
|
|
||||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, deconz_shutdown)
|
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, deconz_shutdown)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def async_unload_entry(hass, config_entry):
|
||||||
|
"""Unload deCONZ config entry."""
|
||||||
|
deconz = hass.data.pop(DOMAIN)
|
||||||
|
hass.services.async_remove(DOMAIN, SERVICE_DECONZ)
|
||||||
|
deconz.close()
|
||||||
|
for component in ['binary_sensor', 'light', 'scene', 'sensor']:
|
||||||
|
await hass.config_entries.async_forward_entry_unload(
|
||||||
|
config_entry, component)
|
||||||
|
dispatchers = hass.data[DATA_DECONZ_UNSUB]
|
||||||
|
for unsub_dispatcher in dispatchers:
|
||||||
|
unsub_dispatcher()
|
||||||
|
hass.data[DATA_DECONZ_UNSUB] = []
|
||||||
|
hass.data[DATA_DECONZ_EVENT] = []
|
||||||
|
hass.data[DATA_DECONZ_ID] = []
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class DeconzEvent(object):
|
||||||
|
"""When you want signals instead of entities.
|
||||||
|
|
||||||
|
Stateless sensors such as remotes are expected to generate an event
|
||||||
|
instead of a sensor entity in hass.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, hass, device):
|
||||||
|
"""Register callback that will be used for signals."""
|
||||||
|
self._hass = hass
|
||||||
|
self._device = device
|
||||||
|
self._device.register_async_callback(self.async_update_callback)
|
||||||
|
self._event = 'deconz_{}'.format(CONF_EVENT)
|
||||||
|
self._id = slugify(self._device.name)
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_update_callback(self, reason):
|
||||||
|
"""Fire the event if reason is that state is updated."""
|
||||||
|
if reason['state']:
|
||||||
|
data = {CONF_ID: self._id, CONF_EVENT: self._device.state}
|
||||||
|
self._hass.bus.async_fire(self._event, data, EventOrigin.remote)
|
||||||
|
@ -5,4 +5,6 @@ _LOGGER = logging.getLogger('homeassistant.components.deconz')
|
|||||||
|
|
||||||
DOMAIN = 'deconz'
|
DOMAIN = 'deconz'
|
||||||
CONFIG_FILE = 'deconz.conf'
|
CONFIG_FILE = 'deconz.conf'
|
||||||
|
DATA_DECONZ_EVENT = 'deconz_events'
|
||||||
DATA_DECONZ_ID = 'deconz_entities'
|
DATA_DECONZ_ID = 'deconz_entities'
|
||||||
|
DATA_DECONZ_UNSUB = 'deconz_dispatchers'
|
||||||
|
@ -16,7 +16,6 @@ from homeassistant.const import STATE_HOME, STATE_NOT_HOME
|
|||||||
from homeassistant.helpers.event import (
|
from homeassistant.helpers.event import (
|
||||||
async_track_point_in_utc_time, async_track_state_change)
|
async_track_point_in_utc_time, async_track_state_change)
|
||||||
from homeassistant.helpers.sun import is_up, get_astral_event_next
|
from homeassistant.helpers.sun import is_up, get_astral_event_next
|
||||||
from homeassistant.loader import get_component
|
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
DOMAIN = 'device_sun_light_trigger'
|
DOMAIN = 'device_sun_light_trigger'
|
||||||
@ -48,9 +47,9 @@ CONFIG_SCHEMA = vol.Schema({
|
|||||||
def async_setup(hass, config):
|
def async_setup(hass, config):
|
||||||
"""Set up the triggers to control lights based on device presence."""
|
"""Set up the triggers to control lights based on device presence."""
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
device_tracker = get_component('device_tracker')
|
device_tracker = hass.components.device_tracker
|
||||||
group = get_component('group')
|
group = hass.components.group
|
||||||
light = get_component('light')
|
light = hass.components.light
|
||||||
conf = config[DOMAIN]
|
conf = config[DOMAIN]
|
||||||
disable_turn_off = conf.get(CONF_DISABLE_TURN_OFF)
|
disable_turn_off = conf.get(CONF_DISABLE_TURN_OFF)
|
||||||
light_group = conf.get(CONF_LIGHT_GROUP, light.ENTITY_ID_ALL_LIGHTS)
|
light_group = conf.get(CONF_LIGHT_GROUP, light.ENTITY_ID_ALL_LIGHTS)
|
||||||
@ -58,14 +57,14 @@ def async_setup(hass, config):
|
|||||||
device_group = conf.get(
|
device_group = conf.get(
|
||||||
CONF_DEVICE_GROUP, device_tracker.ENTITY_ID_ALL_DEVICES)
|
CONF_DEVICE_GROUP, device_tracker.ENTITY_ID_ALL_DEVICES)
|
||||||
device_entity_ids = group.get_entity_ids(
|
device_entity_ids = group.get_entity_ids(
|
||||||
hass, device_group, device_tracker.DOMAIN)
|
device_group, device_tracker.DOMAIN)
|
||||||
|
|
||||||
if not device_entity_ids:
|
if not device_entity_ids:
|
||||||
logger.error("No devices found to track")
|
logger.error("No devices found to track")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Get the light IDs from the specified group
|
# Get the light IDs from the specified group
|
||||||
light_ids = group.get_entity_ids(hass, light_group, light.DOMAIN)
|
light_ids = group.get_entity_ids(light_group, light.DOMAIN)
|
||||||
|
|
||||||
if not light_ids:
|
if not light_ids:
|
||||||
logger.error("No lights found to turn on")
|
logger.error("No lights found to turn on")
|
||||||
@ -85,9 +84,9 @@ def async_setup(hass, config):
|
|||||||
|
|
||||||
def async_turn_on_before_sunset(light_id):
|
def async_turn_on_before_sunset(light_id):
|
||||||
"""Turn on lights."""
|
"""Turn on lights."""
|
||||||
if not device_tracker.is_on(hass) or light.is_on(hass, light_id):
|
if not device_tracker.is_on() or light.is_on(light_id):
|
||||||
return
|
return
|
||||||
light.async_turn_on(hass, light_id,
|
light.async_turn_on(light_id,
|
||||||
transition=LIGHT_TRANSITION_TIME.seconds,
|
transition=LIGHT_TRANSITION_TIME.seconds,
|
||||||
profile=light_profile)
|
profile=light_profile)
|
||||||
|
|
||||||
@ -129,7 +128,7 @@ def async_setup(hass, config):
|
|||||||
@callback
|
@callback
|
||||||
def check_light_on_dev_state_change(entity, old_state, new_state):
|
def check_light_on_dev_state_change(entity, old_state, new_state):
|
||||||
"""Handle tracked device state changes."""
|
"""Handle tracked device state changes."""
|
||||||
lights_are_on = group.is_on(hass, light_group)
|
lights_are_on = group.is_on(light_group)
|
||||||
light_needed = not (lights_are_on or is_up(hass))
|
light_needed = not (lights_are_on or is_up(hass))
|
||||||
|
|
||||||
# These variables are needed for the elif check
|
# These variables are needed for the elif check
|
||||||
@ -139,7 +138,7 @@ def async_setup(hass, config):
|
|||||||
# Do we need lights?
|
# Do we need lights?
|
||||||
if light_needed:
|
if light_needed:
|
||||||
logger.info("Home coming event for %s. Turning lights on", entity)
|
logger.info("Home coming event for %s. Turning lights on", entity)
|
||||||
light.async_turn_on(hass, light_ids, profile=light_profile)
|
light.async_turn_on(light_ids, profile=light_profile)
|
||||||
|
|
||||||
# Are we in the time span were we would turn on the lights
|
# Are we in the time span were we would turn on the lights
|
||||||
# if someone would be home?
|
# if someone would be home?
|
||||||
@ -152,7 +151,7 @@ def async_setup(hass, config):
|
|||||||
# when the fading in started and turn it on if so
|
# when the fading in started and turn it on if so
|
||||||
for index, light_id in enumerate(light_ids):
|
for index, light_id in enumerate(light_ids):
|
||||||
if now > start_point + index * LIGHT_TRANSITION_TIME:
|
if now > start_point + index * LIGHT_TRANSITION_TIME:
|
||||||
light.async_turn_on(hass, light_id)
|
light.async_turn_on(light_id)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# If this light didn't happen to be turned on yet so
|
# If this light didn't happen to be turned on yet so
|
||||||
@ -169,12 +168,12 @@ def async_setup(hass, config):
|
|||||||
@callback
|
@callback
|
||||||
def turn_off_lights_when_all_leave(entity, old_state, new_state):
|
def turn_off_lights_when_all_leave(entity, old_state, new_state):
|
||||||
"""Handle device group state change."""
|
"""Handle device group state change."""
|
||||||
if not group.is_on(hass, light_group):
|
if not group.is_on(light_group):
|
||||||
return
|
return
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
"Everyone has left but there are lights on. Turning them off")
|
"Everyone has left but there are lights on. Turning them off")
|
||||||
light.async_turn_off(hass, light_ids)
|
light.async_turn_off(light_ids)
|
||||||
|
|
||||||
async_track_state_change(
|
async_track_state_change(
|
||||||
hass, device_group, turn_off_lights_when_all_leave,
|
hass, device_group, turn_off_lights_when_all_leave,
|
||||||
|
@ -15,6 +15,7 @@ from homeassistant.setup import async_prepare_setup_platform
|
|||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
from homeassistant.loader import bind_hass
|
from homeassistant.loader import bind_hass
|
||||||
from homeassistant.components import group, zone
|
from homeassistant.components import group, zone
|
||||||
|
from homeassistant.components.zone.zone import async_active_zone
|
||||||
from homeassistant.config import load_yaml_config_file, async_log_exception
|
from homeassistant.config import load_yaml_config_file, async_log_exception
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
from homeassistant.helpers import config_per_platform, discovery
|
from homeassistant.helpers import config_per_platform, discovery
|
||||||
@ -23,7 +24,6 @@ from homeassistant.helpers.event import async_track_time_interval
|
|||||||
from homeassistant.helpers.restore_state import async_get_last_state
|
from homeassistant.helpers.restore_state import async_get_last_state
|
||||||
from homeassistant.helpers.typing import GPSType, ConfigType, HomeAssistantType
|
from homeassistant.helpers.typing import GPSType, ConfigType, HomeAssistantType
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
from homeassistant.loader import get_component
|
|
||||||
import homeassistant.util as util
|
import homeassistant.util as util
|
||||||
from homeassistant.util.async_ import run_coroutine_threadsafe
|
from homeassistant.util.async_ import run_coroutine_threadsafe
|
||||||
import homeassistant.util.dt as dt_util
|
import homeassistant.util.dt as dt_util
|
||||||
@ -321,7 +321,7 @@ class DeviceTracker(object):
|
|||||||
# During init, we ignore the group
|
# During init, we ignore the group
|
||||||
if self.group and self.track_new:
|
if self.group and self.track_new:
|
||||||
self.group.async_set_group(
|
self.group.async_set_group(
|
||||||
self.hass, util.slugify(GROUP_NAME_ALL_DEVICES), visible=False,
|
util.slugify(GROUP_NAME_ALL_DEVICES), visible=False,
|
||||||
name=GROUP_NAME_ALL_DEVICES, add=[device.entity_id])
|
name=GROUP_NAME_ALL_DEVICES, add=[device.entity_id])
|
||||||
|
|
||||||
self.hass.bus.async_fire(EVENT_NEW_DEVICE, {
|
self.hass.bus.async_fire(EVENT_NEW_DEVICE, {
|
||||||
@ -356,9 +356,9 @@ class DeviceTracker(object):
|
|||||||
entity_ids = [dev.entity_id for dev in self.devices.values()
|
entity_ids = [dev.entity_id for dev in self.devices.values()
|
||||||
if dev.track]
|
if dev.track]
|
||||||
|
|
||||||
self.group = get_component('group')
|
self.group = self.hass.components.group
|
||||||
self.group.async_set_group(
|
self.group.async_set_group(
|
||||||
self.hass, util.slugify(GROUP_NAME_ALL_DEVICES), visible=False,
|
util.slugify(GROUP_NAME_ALL_DEVICES), visible=False,
|
||||||
name=GROUP_NAME_ALL_DEVICES, entity_ids=entity_ids)
|
name=GROUP_NAME_ALL_DEVICES, entity_ids=entity_ids)
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
@ -541,7 +541,7 @@ class Device(Entity):
|
|||||||
elif self.location_name:
|
elif self.location_name:
|
||||||
self._state = self.location_name
|
self._state = self.location_name
|
||||||
elif self.gps is not None and self.source_type == SOURCE_TYPE_GPS:
|
elif self.gps is not None and self.source_type == SOURCE_TYPE_GPS:
|
||||||
zone_state = zone.async_active_zone(
|
zone_state = async_active_zone(
|
||||||
self.hass, self.gps[0], self.gps[1], self.gps_accuracy)
|
self.hass, self.gps[0], self.gps[1], self.gps_accuracy)
|
||||||
if zone_state is None:
|
if zone_state is None:
|
||||||
self._state = STATE_NOT_HOME
|
self._state = STATE_NOT_HOME
|
||||||
|
@ -19,7 +19,7 @@ from homeassistant.util import slugify
|
|||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
REQUIREMENTS = ['locationsharinglib==1.2.1']
|
REQUIREMENTS = ['locationsharinglib==1.2.2']
|
||||||
|
|
||||||
CREDENTIALS_FILE = '.google_maps_location_sharing.cookies'
|
CREDENTIALS_FILE = '.google_maps_location_sharing.cookies'
|
||||||
|
|
||||||
@ -79,5 +79,6 @@ class GoogleMapsScanner(object):
|
|||||||
gps=(person.latitude, person.longitude),
|
gps=(person.latitude, person.longitude),
|
||||||
picture=person.picture_url,
|
picture=person.picture_url,
|
||||||
source_type=SOURCE_TYPE_GPS,
|
source_type=SOURCE_TYPE_GPS,
|
||||||
|
gps_accuracy=person.accuracy,
|
||||||
attributes=attrs
|
attributes=attrs
|
||||||
)
|
)
|
||||||
|
@ -4,7 +4,6 @@ Support for the GPSLogger platform.
|
|||||||
For more details about this platform, please refer to the documentation at
|
For more details about this platform, please refer to the documentation at
|
||||||
https://home-assistant.io/components/device_tracker.gpslogger/
|
https://home-assistant.io/components/device_tracker.gpslogger/
|
||||||
"""
|
"""
|
||||||
import asyncio
|
|
||||||
import logging
|
import logging
|
||||||
from hmac import compare_digest
|
from hmac import compare_digest
|
||||||
|
|
||||||
@ -22,6 +21,7 @@ from homeassistant.components.http import (
|
|||||||
from homeassistant.components.device_tracker import ( # NOQA
|
from homeassistant.components.device_tracker import ( # NOQA
|
||||||
DOMAIN, PLATFORM_SCHEMA
|
DOMAIN, PLATFORM_SCHEMA
|
||||||
)
|
)
|
||||||
|
from homeassistant.helpers.typing import HomeAssistantType, ConfigType
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -32,8 +32,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def async_setup_scanner(hass: HomeAssistantType, config: ConfigType,
|
||||||
def async_setup_scanner(hass, config, async_see, discovery_info=None):
|
async_see, discovery_info=None):
|
||||||
"""Set up an endpoint for the GPSLogger application."""
|
"""Set up an endpoint for the GPSLogger application."""
|
||||||
hass.http.register_view(GPSLoggerView(async_see, config))
|
hass.http.register_view(GPSLoggerView(async_see, config))
|
||||||
|
|
||||||
@ -54,8 +54,7 @@ class GPSLoggerView(HomeAssistantView):
|
|||||||
# password is set
|
# password is set
|
||||||
self.requires_auth = self._password is None
|
self.requires_auth = self._password is None
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def get(self, request: Request):
|
||||||
def get(self, request: Request):
|
|
||||||
"""Handle for GPSLogger message received as GET."""
|
"""Handle for GPSLogger message received as GET."""
|
||||||
hass = request.app['hass']
|
hass = request.app['hass']
|
||||||
data = request.query
|
data = request.query
|
||||||
|
@ -13,7 +13,7 @@ import voluptuous as vol
|
|||||||
from homeassistant.const import CONF_USERNAME, CONF_PASSWORD
|
from homeassistant.const import CONF_USERNAME, CONF_PASSWORD
|
||||||
from homeassistant.components.device_tracker import (
|
from homeassistant.components.device_tracker import (
|
||||||
PLATFORM_SCHEMA, DOMAIN, ATTR_ATTRIBUTES, ENTITY_ID_FORMAT, DeviceScanner)
|
PLATFORM_SCHEMA, DOMAIN, ATTR_ATTRIBUTES, ENTITY_ID_FORMAT, DeviceScanner)
|
||||||
from homeassistant.components.zone import active_zone
|
from homeassistant.components.zone.zone import active_zone
|
||||||
from homeassistant.helpers.event import track_utc_time_change
|
from homeassistant.helpers.event import track_utc_time_change
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
from homeassistant.util import slugify
|
from homeassistant.util import slugify
|
||||||
|
@ -12,21 +12,27 @@ import homeassistant.helpers.config_validation as cv
|
|||||||
from homeassistant.components.device_tracker import (
|
from homeassistant.components.device_tracker import (
|
||||||
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
|
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_PORT)
|
CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_PORT, CONF_SSL,
|
||||||
|
CONF_DEVICES, CONF_EXCLUDE)
|
||||||
|
|
||||||
REQUIREMENTS = ['pynetgear==0.3.3']
|
REQUIREMENTS = ['pynetgear==0.4.0']
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
DEFAULT_HOST = 'routerlogin.net'
|
CONF_APS = 'accesspoints'
|
||||||
DEFAULT_USER = 'admin'
|
|
||||||
DEFAULT_PORT = 5000
|
|
||||||
|
|
||||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||||
vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string,
|
vol.Optional(CONF_HOST, default=''): cv.string,
|
||||||
vol.Optional(CONF_USERNAME, default=DEFAULT_USER): cv.string,
|
vol.Optional(CONF_SSL, default=False): cv.boolean,
|
||||||
|
vol.Optional(CONF_USERNAME, default=''): cv.string,
|
||||||
vol.Required(CONF_PASSWORD): cv.string,
|
vol.Required(CONF_PASSWORD): cv.string,
|
||||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port
|
vol.Optional(CONF_PORT, default=None): vol.Any(None, cv.port),
|
||||||
|
vol.Optional(CONF_DEVICES, default=[]):
|
||||||
|
vol.All(cv.ensure_list, [cv.string]),
|
||||||
|
vol.Optional(CONF_EXCLUDE, default=[]):
|
||||||
|
vol.All(cv.ensure_list, [cv.string]),
|
||||||
|
vol.Optional(CONF_APS, default=[]):
|
||||||
|
vol.All(cv.ensure_list, [cv.string]),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@ -34,11 +40,16 @@ def get_scanner(hass, config):
|
|||||||
"""Validate the configuration and returns a Netgear scanner."""
|
"""Validate the configuration and returns a Netgear scanner."""
|
||||||
info = config[DOMAIN]
|
info = config[DOMAIN]
|
||||||
host = info.get(CONF_HOST)
|
host = info.get(CONF_HOST)
|
||||||
|
ssl = info.get(CONF_SSL)
|
||||||
username = info.get(CONF_USERNAME)
|
username = info.get(CONF_USERNAME)
|
||||||
password = info.get(CONF_PASSWORD)
|
password = info.get(CONF_PASSWORD)
|
||||||
port = info.get(CONF_PORT)
|
port = info.get(CONF_PORT)
|
||||||
|
devices = info.get(CONF_DEVICES)
|
||||||
|
excluded_devices = info.get(CONF_EXCLUDE)
|
||||||
|
accesspoints = info.get(CONF_APS)
|
||||||
|
|
||||||
scanner = NetgearDeviceScanner(host, username, password, port)
|
scanner = NetgearDeviceScanner(host, ssl, username, password, port,
|
||||||
|
devices, excluded_devices, accesspoints)
|
||||||
|
|
||||||
return scanner if scanner.success_init else None
|
return scanner if scanner.success_init else None
|
||||||
|
|
||||||
@ -46,16 +57,21 @@ def get_scanner(hass, config):
|
|||||||
class NetgearDeviceScanner(DeviceScanner):
|
class NetgearDeviceScanner(DeviceScanner):
|
||||||
"""Queries a Netgear wireless router using the SOAP-API."""
|
"""Queries a Netgear wireless router using the SOAP-API."""
|
||||||
|
|
||||||
def __init__(self, host, username, password, port):
|
def __init__(self, host, ssl, username, password, port, devices,
|
||||||
|
excluded_devices, accesspoints):
|
||||||
"""Initialize the scanner."""
|
"""Initialize the scanner."""
|
||||||
import pynetgear
|
import pynetgear
|
||||||
|
|
||||||
|
self.tracked_devices = devices
|
||||||
|
self.excluded_devices = excluded_devices
|
||||||
|
self.tracked_accesspoints = accesspoints
|
||||||
|
|
||||||
self.last_results = []
|
self.last_results = []
|
||||||
self._api = pynetgear.Netgear(password, host, username, port)
|
self._api = pynetgear.Netgear(password, host, username, port, ssl)
|
||||||
|
|
||||||
_LOGGER.info("Logging in")
|
_LOGGER.info("Logging in")
|
||||||
|
|
||||||
results = self._api.get_attached_devices()
|
results = self.get_attached_devices()
|
||||||
|
|
||||||
self.success_init = results is not None
|
self.success_init = results is not None
|
||||||
|
|
||||||
@ -68,15 +84,50 @@ class NetgearDeviceScanner(DeviceScanner):
|
|||||||
"""Scan for new devices and return a list with found device IDs."""
|
"""Scan for new devices and return a list with found device IDs."""
|
||||||
self._update_info()
|
self._update_info()
|
||||||
|
|
||||||
return (device.mac for device in self.last_results)
|
devices = []
|
||||||
|
|
||||||
|
for dev in self.last_results:
|
||||||
|
tracked = (not self.tracked_devices or
|
||||||
|
dev.mac in self.tracked_devices or
|
||||||
|
dev.name in self.tracked_devices)
|
||||||
|
tracked = tracked and (not self.excluded_devices or not(
|
||||||
|
dev.mac in self.excluded_devices or
|
||||||
|
dev.name in self.excluded_devices))
|
||||||
|
if tracked:
|
||||||
|
devices.append(dev.mac)
|
||||||
|
if (self.tracked_accesspoints and
|
||||||
|
dev.conn_ap_mac in self.tracked_accesspoints):
|
||||||
|
devices.append(dev.mac + "_" + dev.conn_ap_mac)
|
||||||
|
|
||||||
|
return devices
|
||||||
|
|
||||||
def get_device_name(self, device):
|
def get_device_name(self, device):
|
||||||
"""Return the name of the given device or None if we don't know."""
|
"""Return the name of the given device or the MAC if we don't know."""
|
||||||
try:
|
parts = device.split("_")
|
||||||
return next(result.name for result in self.last_results
|
mac = parts[0]
|
||||||
if result.mac == device)
|
ap_mac = None
|
||||||
except StopIteration:
|
if len(parts) > 1:
|
||||||
return None
|
ap_mac = parts[1]
|
||||||
|
|
||||||
|
name = None
|
||||||
|
for dev in self.last_results:
|
||||||
|
if dev.mac == mac:
|
||||||
|
name = dev.name
|
||||||
|
break
|
||||||
|
|
||||||
|
if not name or name == "--":
|
||||||
|
name = mac
|
||||||
|
|
||||||
|
if ap_mac:
|
||||||
|
ap_name = "Router"
|
||||||
|
for dev in self.last_results:
|
||||||
|
if dev.mac == ap_mac:
|
||||||
|
ap_name = dev.name
|
||||||
|
break
|
||||||
|
|
||||||
|
return name + " on " + ap_name
|
||||||
|
|
||||||
|
return name
|
||||||
|
|
||||||
def _update_info(self):
|
def _update_info(self):
|
||||||
"""Retrieve latest information from the Netgear router.
|
"""Retrieve latest information from the Netgear router.
|
||||||
@ -88,9 +139,21 @@ class NetgearDeviceScanner(DeviceScanner):
|
|||||||
|
|
||||||
_LOGGER.info("Scanning")
|
_LOGGER.info("Scanning")
|
||||||
|
|
||||||
results = self._api.get_attached_devices()
|
results = self.get_attached_devices()
|
||||||
|
|
||||||
if results is None:
|
if results is None:
|
||||||
_LOGGER.warning("Error scanning devices")
|
_LOGGER.warning("Error scanning devices")
|
||||||
|
|
||||||
self.last_results = results or []
|
self.last_results = results or []
|
||||||
|
|
||||||
|
def get_attached_devices(self):
|
||||||
|
"""
|
||||||
|
List attached devices with pynetgear.
|
||||||
|
|
||||||
|
The v2 method takes more time and is more heavy on the router
|
||||||
|
so we only use it if we need connected AP info.
|
||||||
|
"""
|
||||||
|
if self.tracked_accesspoints:
|
||||||
|
return self._api.get_attached_devices_2()
|
||||||
|
|
||||||
|
return self._api.get_attached_devices()
|
||||||
|
@ -207,7 +207,7 @@ def _req_json_rpc(url, session_id, rpcmethod, subsystem, method, **params):
|
|||||||
try:
|
try:
|
||||||
res = requests.post(url, data=data, timeout=5)
|
res = requests.post(url, data=data, timeout=5)
|
||||||
|
|
||||||
except requests.exceptions.Timeout:
|
except (requests.exceptions.ConnectionError, requests.exceptions.Timeout):
|
||||||
return
|
return
|
||||||
|
|
||||||
if res.status_code == 200:
|
if res.status_code == 200:
|
||||||
|
@ -4,7 +4,6 @@ Support for Dialogflow webhook.
|
|||||||
For more details about this component, please refer to the documentation at
|
For more details about this component, please refer to the documentation at
|
||||||
https://home-assistant.io/components/dialogflow/
|
https://home-assistant.io/components/dialogflow/
|
||||||
"""
|
"""
|
||||||
import asyncio
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
@ -37,8 +36,7 @@ class DialogFlowError(HomeAssistantError):
|
|||||||
"""Raised when a DialogFlow error happens."""
|
"""Raised when a DialogFlow error happens."""
|
||||||
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def async_setup(hass, config):
|
||||||
def async_setup(hass, config):
|
|
||||||
"""Set up Dialogflow component."""
|
"""Set up Dialogflow component."""
|
||||||
hass.http.register_view(DialogflowIntentsView)
|
hass.http.register_view(DialogflowIntentsView)
|
||||||
|
|
||||||
@ -51,16 +49,15 @@ class DialogflowIntentsView(HomeAssistantView):
|
|||||||
url = INTENTS_API_ENDPOINT
|
url = INTENTS_API_ENDPOINT
|
||||||
name = 'api:dialogflow'
|
name = 'api:dialogflow'
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def post(self, request):
|
||||||
def post(self, request):
|
|
||||||
"""Handle Dialogflow."""
|
"""Handle Dialogflow."""
|
||||||
hass = request.app['hass']
|
hass = request.app['hass']
|
||||||
message = yield from request.json()
|
message = await request.json()
|
||||||
|
|
||||||
_LOGGER.debug("Received Dialogflow request: %s", message)
|
_LOGGER.debug("Received Dialogflow request: %s", message)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response = yield from async_handle_message(hass, message)
|
response = await async_handle_message(hass, message)
|
||||||
return b'' if response is None else self.json(response)
|
return b'' if response is None else self.json(response)
|
||||||
|
|
||||||
except DialogFlowError as err:
|
except DialogFlowError as err:
|
||||||
@ -93,8 +90,7 @@ def dialogflow_error_response(hass, message, error):
|
|||||||
return dialogflow_response.as_dict()
|
return dialogflow_response.as_dict()
|
||||||
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def async_handle_message(hass, message):
|
||||||
def async_handle_message(hass, message):
|
|
||||||
"""Handle a DialogFlow message."""
|
"""Handle a DialogFlow message."""
|
||||||
req = message.get('result')
|
req = message.get('result')
|
||||||
action_incomplete = req['actionIncomplete']
|
action_incomplete = req['actionIncomplete']
|
||||||
@ -110,7 +106,7 @@ def async_handle_message(hass, message):
|
|||||||
raise DialogFlowError(
|
raise DialogFlowError(
|
||||||
"You have not defined an action in your Dialogflow intent.")
|
"You have not defined an action in your Dialogflow intent.")
|
||||||
|
|
||||||
intent_response = yield from intent.async_handle(
|
intent_response = await intent.async_handle(
|
||||||
hass, DOMAIN, action,
|
hass, DOMAIN, action,
|
||||||
{key: {'value': value} for key, value
|
{key: {'value': value} for key, value
|
||||||
in parameters.items()})
|
in parameters.items()})
|
||||||
|
@ -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
|
from homeassistant.helpers.discovery import async_load_platform, async_discover
|
||||||
import homeassistant.util.dt as dt_util
|
import homeassistant.util.dt as dt_util
|
||||||
|
|
||||||
REQUIREMENTS = ['netdisco==1.3.1']
|
REQUIREMENTS = ['netdisco==1.4.1']
|
||||||
|
|
||||||
DOMAIN = 'discovery'
|
DOMAIN = 'discovery'
|
||||||
|
|
||||||
@ -79,6 +79,7 @@ SERVICE_HANDLERS = {
|
|||||||
'bluesound': ('media_player', 'bluesound'),
|
'bluesound': ('media_player', 'bluesound'),
|
||||||
'songpal': ('media_player', 'songpal'),
|
'songpal': ('media_player', 'songpal'),
|
||||||
'kodi': ('media_player', 'kodi'),
|
'kodi': ('media_player', 'kodi'),
|
||||||
|
'volumio': ('media_player', 'volumio'),
|
||||||
}
|
}
|
||||||
|
|
||||||
OPTIONAL_SERVICE_HANDLERS = {
|
OPTIONAL_SERVICE_HANDLERS = {
|
||||||
|
@ -22,7 +22,7 @@ from homeassistant.helpers.entity import Entity
|
|||||||
from homeassistant.helpers.event import async_track_point_in_utc_time
|
from homeassistant.helpers.event import async_track_point_in_utc_time
|
||||||
from homeassistant.util.dt import utcnow
|
from homeassistant.util.dt import utcnow
|
||||||
|
|
||||||
REQUIREMENTS = ['pyeight==0.0.7']
|
REQUIREMENTS = ['pyeight==0.0.8']
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -20,7 +20,7 @@ import homeassistant.helpers.config_validation as cv
|
|||||||
from homeassistant.util.json import load_json, save_json
|
from homeassistant.util.json import load_json, save_json
|
||||||
from .hue_api import (
|
from .hue_api import (
|
||||||
HueUsernameView, HueAllLightsStateView, HueOneLightStateView,
|
HueUsernameView, HueAllLightsStateView, HueOneLightStateView,
|
||||||
HueOneLightChangeView)
|
HueOneLightChangeView, HueGroupView)
|
||||||
from .upnp import DescriptionXmlView, UPNPResponderThread
|
from .upnp import DescriptionXmlView, UPNPResponderThread
|
||||||
|
|
||||||
DOMAIN = 'emulated_hue'
|
DOMAIN = 'emulated_hue'
|
||||||
@ -104,6 +104,7 @@ def setup(hass, yaml_config):
|
|||||||
server.register_view(HueAllLightsStateView(config))
|
server.register_view(HueAllLightsStateView(config))
|
||||||
server.register_view(HueOneLightStateView(config))
|
server.register_view(HueOneLightStateView(config))
|
||||||
server.register_view(HueOneLightChangeView(config))
|
server.register_view(HueOneLightChangeView(config))
|
||||||
|
server.register_view(HueGroupView(config))
|
||||||
|
|
||||||
upnp_listener = UPNPResponderThread(
|
upnp_listener = UPNPResponderThread(
|
||||||
config.host_ip_addr, config.listen_port,
|
config.host_ip_addr, config.listen_port,
|
||||||
|
@ -51,6 +51,29 @@ class HueUsernameView(HomeAssistantView):
|
|||||||
return self.json([{'success': {'username': '12345678901234567890'}}])
|
return self.json([{'success': {'username': '12345678901234567890'}}])
|
||||||
|
|
||||||
|
|
||||||
|
class HueGroupView(HomeAssistantView):
|
||||||
|
"""Group handler to get Logitech Pop working."""
|
||||||
|
|
||||||
|
url = '/api/{username}/groups/0/action'
|
||||||
|
name = 'emulated_hue:groups:state'
|
||||||
|
requires_auth = False
|
||||||
|
|
||||||
|
def __init__(self, config):
|
||||||
|
"""Initialize the instance of the view."""
|
||||||
|
self.config = config
|
||||||
|
|
||||||
|
@core.callback
|
||||||
|
def put(self, request, username):
|
||||||
|
"""Process a request to make the Logitech Pop working."""
|
||||||
|
return self.json([{
|
||||||
|
'error': {
|
||||||
|
'address': '/groups/0/action/scene',
|
||||||
|
'type': 7,
|
||||||
|
'description': 'invalid value, dummy for parameter, scene'
|
||||||
|
}
|
||||||
|
}])
|
||||||
|
|
||||||
|
|
||||||
class HueAllLightsStateView(HomeAssistantView):
|
class HueAllLightsStateView(HomeAssistantView):
|
||||||
"""Handle requests for getting and setting info about entities."""
|
"""Handle requests for getting and setting info about entities."""
|
||||||
|
|
||||||
|
@ -31,7 +31,7 @@ _LOGGER = logging.getLogger(__name__)
|
|||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||||
"""Set up the INSTEON PLM device class for the hass platform."""
|
"""Set up the INSTEON PLM device class for the hass platform."""
|
||||||
plm = hass.data['insteon_plm']
|
plm = hass.data['insteon_plm'].get('plm')
|
||||||
|
|
||||||
address = discovery_info['address']
|
address = discovery_info['address']
|
||||||
device = plm.devices[address]
|
device = plm.devices[address]
|
||||||
|
@ -4,7 +4,6 @@ Support for MQTT fans.
|
|||||||
For more details about this platform, please refer to the documentation
|
For more details about this platform, please refer to the documentation
|
||||||
https://home-assistant.io/components/fan.mqtt/
|
https://home-assistant.io/components/fan.mqtt/
|
||||||
"""
|
"""
|
||||||
import asyncio
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
@ -19,6 +18,7 @@ from homeassistant.components.mqtt import (
|
|||||||
CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, CONF_RETAIN,
|
CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, CONF_RETAIN,
|
||||||
MqttAvailability)
|
MqttAvailability)
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
from homeassistant.helpers.typing import HomeAssistantType, ConfigType
|
||||||
from homeassistant.components.fan import (SPEED_LOW, SPEED_MEDIUM,
|
from homeassistant.components.fan import (SPEED_LOW, SPEED_MEDIUM,
|
||||||
SPEED_HIGH, FanEntity,
|
SPEED_HIGH, FanEntity,
|
||||||
SUPPORT_SET_SPEED, SUPPORT_OSCILLATE,
|
SUPPORT_SET_SPEED, SUPPORT_OSCILLATE,
|
||||||
@ -77,8 +77,8 @@ PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend({
|
|||||||
}).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema)
|
}).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema)
|
||||||
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def async_setup_platform(hass: HomeAssistantType, config: ConfigType,
|
||||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
async_add_devices, discovery_info=None):
|
||||||
"""Set up the MQTT fan platform."""
|
"""Set up the MQTT fan platform."""
|
||||||
if discovery_info is not None:
|
if discovery_info is not None:
|
||||||
config = PLATFORM_SCHEMA(discovery_info)
|
config = PLATFORM_SCHEMA(discovery_info)
|
||||||
@ -149,10 +149,9 @@ class MqttFan(MqttAvailability, FanEntity):
|
|||||||
self._supported_features |= (topic[CONF_SPEED_STATE_TOPIC]
|
self._supported_features |= (topic[CONF_SPEED_STATE_TOPIC]
|
||||||
is not None and SUPPORT_SET_SPEED)
|
is not None and SUPPORT_SET_SPEED)
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def async_added_to_hass(self):
|
||||||
def async_added_to_hass(self):
|
|
||||||
"""Subscribe to MQTT events."""
|
"""Subscribe to MQTT events."""
|
||||||
yield from super().async_added_to_hass()
|
await super().async_added_to_hass()
|
||||||
|
|
||||||
templates = {}
|
templates = {}
|
||||||
for key, tpl in list(self._templates.items()):
|
for key, tpl in list(self._templates.items()):
|
||||||
@ -173,7 +172,7 @@ class MqttFan(MqttAvailability, FanEntity):
|
|||||||
self.async_schedule_update_ha_state()
|
self.async_schedule_update_ha_state()
|
||||||
|
|
||||||
if self._topic[CONF_STATE_TOPIC] is not None:
|
if self._topic[CONF_STATE_TOPIC] is not None:
|
||||||
yield from mqtt.async_subscribe(
|
await mqtt.async_subscribe(
|
||||||
self.hass, self._topic[CONF_STATE_TOPIC], state_received,
|
self.hass, self._topic[CONF_STATE_TOPIC], state_received,
|
||||||
self._qos)
|
self._qos)
|
||||||
|
|
||||||
@ -190,7 +189,7 @@ class MqttFan(MqttAvailability, FanEntity):
|
|||||||
self.async_schedule_update_ha_state()
|
self.async_schedule_update_ha_state()
|
||||||
|
|
||||||
if self._topic[CONF_SPEED_STATE_TOPIC] is not None:
|
if self._topic[CONF_SPEED_STATE_TOPIC] is not None:
|
||||||
yield from mqtt.async_subscribe(
|
await mqtt.async_subscribe(
|
||||||
self.hass, self._topic[CONF_SPEED_STATE_TOPIC], speed_received,
|
self.hass, self._topic[CONF_SPEED_STATE_TOPIC], speed_received,
|
||||||
self._qos)
|
self._qos)
|
||||||
self._speed = SPEED_OFF
|
self._speed = SPEED_OFF
|
||||||
@ -206,7 +205,7 @@ class MqttFan(MqttAvailability, FanEntity):
|
|||||||
self.async_schedule_update_ha_state()
|
self.async_schedule_update_ha_state()
|
||||||
|
|
||||||
if self._topic[CONF_OSCILLATION_STATE_TOPIC] is not None:
|
if self._topic[CONF_OSCILLATION_STATE_TOPIC] is not None:
|
||||||
yield from mqtt.async_subscribe(
|
await mqtt.async_subscribe(
|
||||||
self.hass, self._topic[CONF_OSCILLATION_STATE_TOPIC],
|
self.hass, self._topic[CONF_OSCILLATION_STATE_TOPIC],
|
||||||
oscillation_received, self._qos)
|
oscillation_received, self._qos)
|
||||||
self._oscillation = False
|
self._oscillation = False
|
||||||
@ -251,8 +250,7 @@ class MqttFan(MqttAvailability, FanEntity):
|
|||||||
"""Return the oscillation state."""
|
"""Return the oscillation state."""
|
||||||
return self._oscillation
|
return self._oscillation
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def async_turn_on(self, speed: str = None, **kwargs) -> None:
|
||||||
def async_turn_on(self, speed: str = None, **kwargs) -> None:
|
|
||||||
"""Turn on the entity.
|
"""Turn on the entity.
|
||||||
|
|
||||||
This method is a coroutine.
|
This method is a coroutine.
|
||||||
@ -261,10 +259,9 @@ class MqttFan(MqttAvailability, FanEntity):
|
|||||||
self.hass, self._topic[CONF_COMMAND_TOPIC],
|
self.hass, self._topic[CONF_COMMAND_TOPIC],
|
||||||
self._payload[STATE_ON], self._qos, self._retain)
|
self._payload[STATE_ON], self._qos, self._retain)
|
||||||
if speed:
|
if speed:
|
||||||
yield from self.async_set_speed(speed)
|
await self.async_set_speed(speed)
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def async_turn_off(self, **kwargs) -> None:
|
||||||
def async_turn_off(self, **kwargs) -> None:
|
|
||||||
"""Turn off the entity.
|
"""Turn off the entity.
|
||||||
|
|
||||||
This method is a coroutine.
|
This method is a coroutine.
|
||||||
@ -273,8 +270,7 @@ class MqttFan(MqttAvailability, FanEntity):
|
|||||||
self.hass, self._topic[CONF_COMMAND_TOPIC],
|
self.hass, self._topic[CONF_COMMAND_TOPIC],
|
||||||
self._payload[STATE_OFF], self._qos, self._retain)
|
self._payload[STATE_OFF], self._qos, self._retain)
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def async_set_speed(self, speed: str) -> None:
|
||||||
def async_set_speed(self, speed: str) -> None:
|
|
||||||
"""Set the speed of the fan.
|
"""Set the speed of the fan.
|
||||||
|
|
||||||
This method is a coroutine.
|
This method is a coroutine.
|
||||||
@ -299,8 +295,7 @@ class MqttFan(MqttAvailability, FanEntity):
|
|||||||
self._speed = speed
|
self._speed = speed
|
||||||
self.async_schedule_update_ha_state()
|
self.async_schedule_update_ha_state()
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def async_oscillate(self, oscillating: bool) -> None:
|
||||||
def async_oscillate(self, oscillating: bool) -> None:
|
|
||||||
"""Set oscillation.
|
"""Set oscillation.
|
||||||
|
|
||||||
This method is a coroutine.
|
This method is a coroutine.
|
||||||
|
324
homeassistant/components/fan/template.py
Normal file
324
homeassistant/components/fan/template.py
Normal file
@ -0,0 +1,324 @@
|
|||||||
|
"""
|
||||||
|
Support for Template fans.
|
||||||
|
|
||||||
|
For more details about this platform, please refer to the documentation
|
||||||
|
https://home-assistant.io/components/fan.template/
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant.core import callback
|
||||||
|
from homeassistant.const import (
|
||||||
|
CONF_FRIENDLY_NAME, CONF_VALUE_TEMPLATE, CONF_ENTITY_ID,
|
||||||
|
STATE_ON, STATE_OFF, MATCH_ALL, EVENT_HOMEASSISTANT_START,
|
||||||
|
STATE_UNKNOWN)
|
||||||
|
|
||||||
|
from homeassistant.exceptions import TemplateError
|
||||||
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA
|
||||||
|
from homeassistant.helpers.entity import ToggleEntity
|
||||||
|
from homeassistant.components.fan import (SPEED_LOW, SPEED_MEDIUM,
|
||||||
|
SPEED_HIGH, SUPPORT_SET_SPEED,
|
||||||
|
SUPPORT_OSCILLATE, FanEntity,
|
||||||
|
ATTR_SPEED, ATTR_OSCILLATING,
|
||||||
|
ENTITY_ID_FORMAT)
|
||||||
|
|
||||||
|
from homeassistant.helpers.entity import async_generate_entity_id
|
||||||
|
from homeassistant.helpers.script import Script
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
CONF_FANS = 'fans'
|
||||||
|
CONF_SPEED_LIST = 'speeds'
|
||||||
|
CONF_SPEED_TEMPLATE = 'speed_template'
|
||||||
|
CONF_OSCILLATING_TEMPLATE = 'oscillating_template'
|
||||||
|
CONF_ON_ACTION = 'turn_on'
|
||||||
|
CONF_OFF_ACTION = 'turn_off'
|
||||||
|
CONF_SET_SPEED_ACTION = 'set_speed'
|
||||||
|
CONF_SET_OSCILLATING_ACTION = 'set_oscillating'
|
||||||
|
|
||||||
|
_VALID_STATES = [STATE_ON, STATE_OFF]
|
||||||
|
_VALID_OSC = [True, False]
|
||||||
|
|
||||||
|
FAN_SCHEMA = vol.Schema({
|
||||||
|
vol.Optional(CONF_FRIENDLY_NAME): cv.string,
|
||||||
|
vol.Required(CONF_VALUE_TEMPLATE): cv.template,
|
||||||
|
vol.Optional(CONF_SPEED_TEMPLATE): cv.template,
|
||||||
|
vol.Optional(CONF_OSCILLATING_TEMPLATE): cv.template,
|
||||||
|
|
||||||
|
vol.Required(CONF_ON_ACTION): cv.SCRIPT_SCHEMA,
|
||||||
|
vol.Required(CONF_OFF_ACTION): cv.SCRIPT_SCHEMA,
|
||||||
|
|
||||||
|
vol.Optional(CONF_SET_SPEED_ACTION): cv.SCRIPT_SCHEMA,
|
||||||
|
vol.Optional(CONF_SET_OSCILLATING_ACTION): cv.SCRIPT_SCHEMA,
|
||||||
|
|
||||||
|
vol.Optional(
|
||||||
|
CONF_SPEED_LIST,
|
||||||
|
default=[SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH]
|
||||||
|
): cv.ensure_list,
|
||||||
|
|
||||||
|
vol.Optional(CONF_ENTITY_ID): cv.entity_ids
|
||||||
|
})
|
||||||
|
|
||||||
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||||
|
vol.Required(CONF_FANS): vol.Schema({cv.slug: FAN_SCHEMA}),
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_platform(
|
||||||
|
hass, config, async_add_devices, discovery_info=None
|
||||||
|
):
|
||||||
|
"""Set up the Template Fans."""
|
||||||
|
fans = []
|
||||||
|
|
||||||
|
for device, device_config in config[CONF_FANS].items():
|
||||||
|
friendly_name = device_config.get(CONF_FRIENDLY_NAME, device)
|
||||||
|
|
||||||
|
state_template = device_config[CONF_VALUE_TEMPLATE]
|
||||||
|
speed_template = device_config.get(CONF_SPEED_TEMPLATE)
|
||||||
|
oscillating_template = device_config.get(
|
||||||
|
CONF_OSCILLATING_TEMPLATE
|
||||||
|
)
|
||||||
|
|
||||||
|
on_action = device_config[CONF_ON_ACTION]
|
||||||
|
off_action = device_config[CONF_OFF_ACTION]
|
||||||
|
set_speed_action = device_config.get(CONF_SET_SPEED_ACTION)
|
||||||
|
set_oscillating_action = device_config.get(CONF_SET_OSCILLATING_ACTION)
|
||||||
|
|
||||||
|
speed_list = device_config[CONF_SPEED_LIST]
|
||||||
|
|
||||||
|
entity_ids = set()
|
||||||
|
manual_entity_ids = device_config.get(CONF_ENTITY_ID)
|
||||||
|
|
||||||
|
for template in (state_template, speed_template, oscillating_template):
|
||||||
|
if template is None:
|
||||||
|
continue
|
||||||
|
template.hass = hass
|
||||||
|
|
||||||
|
if entity_ids == MATCH_ALL or manual_entity_ids is not None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
template_entity_ids = template.extract_entities()
|
||||||
|
if template_entity_ids == MATCH_ALL:
|
||||||
|
entity_ids = MATCH_ALL
|
||||||
|
else:
|
||||||
|
entity_ids |= set(template_entity_ids)
|
||||||
|
|
||||||
|
if manual_entity_ids is not None:
|
||||||
|
entity_ids = manual_entity_ids
|
||||||
|
elif entity_ids != MATCH_ALL:
|
||||||
|
entity_ids = list(entity_ids)
|
||||||
|
|
||||||
|
fans.append(
|
||||||
|
TemplateFan(
|
||||||
|
hass, device, friendly_name,
|
||||||
|
state_template, speed_template, oscillating_template,
|
||||||
|
on_action, off_action, set_speed_action,
|
||||||
|
set_oscillating_action, speed_list, entity_ids
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
async_add_devices(fans)
|
||||||
|
|
||||||
|
|
||||||
|
class TemplateFan(FanEntity):
|
||||||
|
"""A template fan component."""
|
||||||
|
|
||||||
|
def __init__(self, hass, device_id, friendly_name,
|
||||||
|
state_template, speed_template, oscillating_template,
|
||||||
|
on_action, off_action, set_speed_action,
|
||||||
|
set_oscillating_action, speed_list, entity_ids):
|
||||||
|
"""Initialize the fan."""
|
||||||
|
self.hass = hass
|
||||||
|
self.entity_id = async_generate_entity_id(
|
||||||
|
ENTITY_ID_FORMAT, device_id, hass=hass)
|
||||||
|
self._name = friendly_name
|
||||||
|
|
||||||
|
self._template = state_template
|
||||||
|
self._speed_template = speed_template
|
||||||
|
self._oscillating_template = oscillating_template
|
||||||
|
self._supported_features = 0
|
||||||
|
|
||||||
|
self._on_script = Script(hass, on_action)
|
||||||
|
self._off_script = Script(hass, off_action)
|
||||||
|
|
||||||
|
self._set_speed_script = None
|
||||||
|
if set_speed_action:
|
||||||
|
self._set_speed_script = Script(hass, set_speed_action)
|
||||||
|
|
||||||
|
self._set_oscillating_script = None
|
||||||
|
if set_oscillating_action:
|
||||||
|
self._set_oscillating_script = Script(hass, set_oscillating_action)
|
||||||
|
|
||||||
|
self._state = STATE_OFF
|
||||||
|
self._speed = None
|
||||||
|
self._oscillating = None
|
||||||
|
|
||||||
|
self._template.hass = self.hass
|
||||||
|
if self._speed_template:
|
||||||
|
self._speed_template.hass = self.hass
|
||||||
|
self._supported_features |= SUPPORT_SET_SPEED
|
||||||
|
if self._oscillating_template:
|
||||||
|
self._oscillating_template.hass = self.hass
|
||||||
|
self._supported_features |= SUPPORT_OSCILLATE
|
||||||
|
|
||||||
|
self._entities = entity_ids
|
||||||
|
# List of valid speeds
|
||||||
|
self._speed_list = speed_list
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
"""Return the display name of this fan."""
|
||||||
|
return self._name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def supported_features(self) -> int:
|
||||||
|
"""Flag supported features."""
|
||||||
|
return self._supported_features
|
||||||
|
|
||||||
|
@property
|
||||||
|
def speed_list(self: ToggleEntity) -> list:
|
||||||
|
"""Get the list of available speeds."""
|
||||||
|
return self._speed_list
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_on(self):
|
||||||
|
"""Return true if device is on."""
|
||||||
|
return self._state == STATE_ON
|
||||||
|
|
||||||
|
@property
|
||||||
|
def speed(self):
|
||||||
|
"""Return the current speed."""
|
||||||
|
return self._speed
|
||||||
|
|
||||||
|
@property
|
||||||
|
def oscillating(self):
|
||||||
|
"""Return the oscillation state."""
|
||||||
|
return self._oscillating
|
||||||
|
|
||||||
|
@property
|
||||||
|
def should_poll(self):
|
||||||
|
"""Return the polling state."""
|
||||||
|
return False
|
||||||
|
|
||||||
|
# pylint: disable=arguments-differ
|
||||||
|
async def async_turn_on(self, speed: str = None) -> None:
|
||||||
|
"""Turn on the fan."""
|
||||||
|
await self._on_script.async_run()
|
||||||
|
self._state = STATE_ON
|
||||||
|
|
||||||
|
if speed is not None:
|
||||||
|
await self.async_set_speed(speed)
|
||||||
|
|
||||||
|
# pylint: disable=arguments-differ
|
||||||
|
async def async_turn_off(self) -> None:
|
||||||
|
"""Turn off the fan."""
|
||||||
|
await self._off_script.async_run()
|
||||||
|
self._state = STATE_OFF
|
||||||
|
|
||||||
|
async def async_set_speed(self, speed: str) -> None:
|
||||||
|
"""Set the speed of the fan."""
|
||||||
|
if self._set_speed_script is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
if speed in self._speed_list:
|
||||||
|
self._speed = speed
|
||||||
|
await self._set_speed_script.async_run({ATTR_SPEED: speed})
|
||||||
|
else:
|
||||||
|
_LOGGER.error(
|
||||||
|
'Received invalid speed: %s. ' +
|
||||||
|
'Expected: %s.',
|
||||||
|
speed, self._speed_list)
|
||||||
|
|
||||||
|
async def async_oscillate(self, oscillating: bool) -> None:
|
||||||
|
"""Set oscillation of the fan."""
|
||||||
|
if self._set_oscillating_script is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
await self._set_oscillating_script.async_run(
|
||||||
|
{ATTR_OSCILLATING: oscillating}
|
||||||
|
)
|
||||||
|
self._oscillating = oscillating
|
||||||
|
|
||||||
|
async def async_added_to_hass(self):
|
||||||
|
"""Register callbacks."""
|
||||||
|
@callback
|
||||||
|
def template_fan_state_listener(entity, old_state, new_state):
|
||||||
|
"""Handle target device state changes."""
|
||||||
|
self.async_schedule_update_ha_state(True)
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def template_fan_startup(event):
|
||||||
|
"""Update template on startup."""
|
||||||
|
self.hass.helpers.event.async_track_state_change(
|
||||||
|
self._entities, template_fan_state_listener)
|
||||||
|
|
||||||
|
self.async_schedule_update_ha_state(True)
|
||||||
|
|
||||||
|
self.hass.bus.async_listen_once(
|
||||||
|
EVENT_HOMEASSISTANT_START, template_fan_startup)
|
||||||
|
|
||||||
|
async def async_update(self):
|
||||||
|
"""Update the state from the template."""
|
||||||
|
# Update state
|
||||||
|
try:
|
||||||
|
state = self._template.async_render()
|
||||||
|
except TemplateError as ex:
|
||||||
|
_LOGGER.error(ex)
|
||||||
|
state = None
|
||||||
|
self._state = None
|
||||||
|
|
||||||
|
# Validate state
|
||||||
|
if state in _VALID_STATES:
|
||||||
|
self._state = state
|
||||||
|
elif state == STATE_UNKNOWN:
|
||||||
|
self._state = None
|
||||||
|
else:
|
||||||
|
_LOGGER.error(
|
||||||
|
'Received invalid fan is_on state: %s. ' +
|
||||||
|
'Expected: %s.',
|
||||||
|
state, ', '.join(_VALID_STATES))
|
||||||
|
self._state = None
|
||||||
|
|
||||||
|
# Update speed if 'speed_template' is configured
|
||||||
|
if self._speed_template is not None:
|
||||||
|
try:
|
||||||
|
speed = self._speed_template.async_render()
|
||||||
|
except TemplateError as ex:
|
||||||
|
_LOGGER.error(ex)
|
||||||
|
speed = None
|
||||||
|
self._state = None
|
||||||
|
|
||||||
|
# Validate speed
|
||||||
|
if speed in self._speed_list:
|
||||||
|
self._speed = speed
|
||||||
|
elif speed == STATE_UNKNOWN:
|
||||||
|
self._speed = None
|
||||||
|
else:
|
||||||
|
_LOGGER.error(
|
||||||
|
'Received invalid speed: %s. ' +
|
||||||
|
'Expected: %s.',
|
||||||
|
speed, self._speed_list)
|
||||||
|
self._speed = None
|
||||||
|
|
||||||
|
# Update oscillating if 'oscillating_template' is configured
|
||||||
|
if self._oscillating_template is not None:
|
||||||
|
try:
|
||||||
|
oscillating = self._oscillating_template.async_render()
|
||||||
|
except TemplateError as ex:
|
||||||
|
_LOGGER.error(ex)
|
||||||
|
self._state = None
|
||||||
|
|
||||||
|
# Validate osc
|
||||||
|
if oscillating == 'True' or oscillating is True:
|
||||||
|
self._oscillating = True
|
||||||
|
elif oscillating == 'False' or oscillating is False:
|
||||||
|
self._oscillating = False
|
||||||
|
elif oscillating == STATE_UNKNOWN:
|
||||||
|
self._oscillating = None
|
||||||
|
else:
|
||||||
|
_LOGGER.error(
|
||||||
|
'Received invalid oscillating: %s. ' +
|
||||||
|
'Expected: True/False.', oscillating)
|
||||||
|
self._oscillating = None
|
@ -16,15 +16,16 @@ import voluptuous as vol
|
|||||||
import jinja2
|
import jinja2
|
||||||
|
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
from homeassistant.components.http import HomeAssistantView
|
from homeassistant.components.http.view import HomeAssistantView
|
||||||
from homeassistant.components.http.const import KEY_AUTHENTICATED
|
from homeassistant.components.http.const import KEY_AUTHENTICATED
|
||||||
|
from homeassistant.components import websocket_api
|
||||||
from homeassistant.config import find_config_file, load_yaml_config_file
|
from homeassistant.config import find_config_file, load_yaml_config_file
|
||||||
from homeassistant.const import CONF_NAME, EVENT_THEMES_UPDATED
|
from homeassistant.const import CONF_NAME, EVENT_THEMES_UPDATED
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
from homeassistant.helpers.translation import async_get_translations
|
from homeassistant.helpers.translation import async_get_translations
|
||||||
from homeassistant.loader import bind_hass
|
from homeassistant.loader import bind_hass
|
||||||
|
|
||||||
REQUIREMENTS = ['home-assistant-frontend==20180426.0']
|
REQUIREMENTS = ['home-assistant-frontend==20180509.0']
|
||||||
|
|
||||||
DOMAIN = 'frontend'
|
DOMAIN = 'frontend'
|
||||||
DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log']
|
DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log']
|
||||||
@ -94,6 +95,10 @@ SERVICE_RELOAD_THEMES = 'reload_themes'
|
|||||||
SERVICE_SET_THEME_SCHEMA = vol.Schema({
|
SERVICE_SET_THEME_SCHEMA = vol.Schema({
|
||||||
vol.Required(CONF_NAME): cv.string,
|
vol.Required(CONF_NAME): cv.string,
|
||||||
})
|
})
|
||||||
|
WS_TYPE_GET_PANELS = 'get_panels'
|
||||||
|
SCHEMA_GET_PANELS = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
|
||||||
|
vol.Required('type'): WS_TYPE_GET_PANELS,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
class AbstractPanel:
|
class AbstractPanel:
|
||||||
@ -291,6 +296,8 @@ def add_manifest_json_key(key, val):
|
|||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def async_setup(hass, config):
|
def async_setup(hass, config):
|
||||||
"""Set up the serving of the frontend."""
|
"""Set up the serving of the frontend."""
|
||||||
|
hass.components.websocket_api.async_register_command(
|
||||||
|
WS_TYPE_GET_PANELS, websocket_handle_get_panels, SCHEMA_GET_PANELS)
|
||||||
hass.http.register_view(ManifestJSONView)
|
hass.http.register_view(ManifestJSONView)
|
||||||
|
|
||||||
conf = config.get(DOMAIN, {})
|
conf = config.get(DOMAIN, {})
|
||||||
@ -597,3 +604,19 @@ def _is_latest(js_option, request):
|
|||||||
useragent = request.headers.get('User-Agent')
|
useragent = request.headers.get('User-Agent')
|
||||||
|
|
||||||
return useragent and hass_frontend.version(useragent)
|
return useragent and hass_frontend.version(useragent)
|
||||||
|
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def websocket_handle_get_panels(hass, connection, msg):
|
||||||
|
"""Handle get panels command.
|
||||||
|
|
||||||
|
Async friendly.
|
||||||
|
"""
|
||||||
|
panels = {
|
||||||
|
panel:
|
||||||
|
connection.hass.data[DATA_PANELS][panel].to_response(
|
||||||
|
connection.hass, connection.request)
|
||||||
|
for panel in connection.hass.data[DATA_PANELS]}
|
||||||
|
|
||||||
|
connection.to_write.put_nowait(websocket_api.result_message(
|
||||||
|
msg['id'], panels))
|
||||||
|
@ -70,8 +70,7 @@ def request_sync(hass):
|
|||||||
hass.services.call(DOMAIN, SERVICE_REQUEST_SYNC)
|
hass.services.call(DOMAIN, SERVICE_REQUEST_SYNC)
|
||||||
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def async_setup(hass: HomeAssistant, yaml_config: Dict[str, Any]):
|
||||||
def async_setup(hass: HomeAssistant, yaml_config: Dict[str, Any]):
|
|
||||||
"""Activate Google Actions component."""
|
"""Activate Google Actions component."""
|
||||||
config = yaml_config.get(DOMAIN, {})
|
config = yaml_config.get(DOMAIN, {})
|
||||||
agent_user_id = config.get(CONF_AGENT_USER_ID)
|
agent_user_id = config.get(CONF_AGENT_USER_ID)
|
||||||
@ -79,20 +78,19 @@ def async_setup(hass: HomeAssistant, yaml_config: Dict[str, Any]):
|
|||||||
hass.http.register_view(GoogleAssistantAuthView(hass, config))
|
hass.http.register_view(GoogleAssistantAuthView(hass, config))
|
||||||
async_register_http(hass, config)
|
async_register_http(hass, config)
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def request_sync_service_handler(call):
|
||||||
def request_sync_service_handler(call):
|
|
||||||
"""Handle request sync service calls."""
|
"""Handle request sync service calls."""
|
||||||
websession = async_get_clientsession(hass)
|
websession = async_get_clientsession(hass)
|
||||||
try:
|
try:
|
||||||
with async_timeout.timeout(5, loop=hass.loop):
|
with async_timeout.timeout(5, loop=hass.loop):
|
||||||
res = yield from websession.post(
|
res = await websession.post(
|
||||||
REQUEST_SYNC_BASE_URL,
|
REQUEST_SYNC_BASE_URL,
|
||||||
params={'key': api_key},
|
params={'key': api_key},
|
||||||
json={'agent_user_id': agent_user_id})
|
json={'agent_user_id': agent_user_id})
|
||||||
_LOGGER.info("Submitted request_sync request to Google")
|
_LOGGER.info("Submitted request_sync request to Google")
|
||||||
res.raise_for_status()
|
res.raise_for_status()
|
||||||
except aiohttp.ClientResponseError:
|
except aiohttp.ClientResponseError:
|
||||||
body = yield from res.read()
|
body = await res.read()
|
||||||
_LOGGER.error(
|
_LOGGER.error(
|
||||||
'request_sync request failed: %d %s', res.status, body)
|
'request_sync request failed: %d %s', res.status, body)
|
||||||
except (asyncio.TimeoutError, aiohttp.ClientError):
|
except (asyncio.TimeoutError, aiohttp.ClientError):
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
"""Google Assistant OAuth View."""
|
"""Google Assistant OAuth View."""
|
||||||
|
|
||||||
import asyncio
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
# Typing imports
|
# Typing imports
|
||||||
@ -44,8 +43,7 @@ class GoogleAssistantAuthView(HomeAssistantView):
|
|||||||
self.client_id = cfg.get(CONF_CLIENT_ID)
|
self.client_id = cfg.get(CONF_CLIENT_ID)
|
||||||
self.access_token = cfg.get(CONF_ACCESS_TOKEN)
|
self.access_token = cfg.get(CONF_ACCESS_TOKEN)
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def get(self, request: Request) -> Response:
|
||||||
def get(self, request: Request) -> Response:
|
|
||||||
"""Handle oauth token request."""
|
"""Handle oauth token request."""
|
||||||
query = request.query
|
query = request.query
|
||||||
redirect_uri = query.get('redirect_uri')
|
redirect_uri = query.get('redirect_uri')
|
||||||
|
@ -4,7 +4,6 @@ Support for Google Actions Smart Home Control.
|
|||||||
For more details about this component, please refer to the documentation at
|
For more details about this component, please refer to the documentation at
|
||||||
https://home-assistant.io/components/google_assistant/
|
https://home-assistant.io/components/google_assistant/
|
||||||
"""
|
"""
|
||||||
import asyncio
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from aiohttp.hdrs import AUTHORIZATION
|
from aiohttp.hdrs import AUTHORIZATION
|
||||||
@ -77,14 +76,13 @@ class GoogleAssistantView(HomeAssistantView):
|
|||||||
self.access_token = access_token
|
self.access_token = access_token
|
||||||
self.gass_config = gass_config
|
self.gass_config = gass_config
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def post(self, request: Request) -> Response:
|
||||||
def post(self, request: Request) -> Response:
|
|
||||||
"""Handle Google Assistant requests."""
|
"""Handle Google Assistant requests."""
|
||||||
auth = request.headers.get(AUTHORIZATION, None)
|
auth = request.headers.get(AUTHORIZATION, None)
|
||||||
if 'Bearer {}'.format(self.access_token) != auth:
|
if 'Bearer {}'.format(self.access_token) != auth:
|
||||||
return self.json_message("missing authorization", status_code=401)
|
return self.json_message("missing authorization", status_code=401)
|
||||||
|
|
||||||
message = yield from request.json() # type: dict
|
message = await request.json() # type: dict
|
||||||
result = yield from async_handle_message(
|
result = await async_handle_message(
|
||||||
request.app['hass'], self.gass_config, message)
|
request.app['hass'], self.gass_config, message)
|
||||||
return self.json(result)
|
return self.json(result)
|
||||||
|
@ -245,34 +245,31 @@ def get_entity_ids(hass, entity_id, domain_filter=None):
|
|||||||
if ent_id.startswith(domain_filter)]
|
if ent_id.startswith(domain_filter)]
|
||||||
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def async_setup(hass, config):
|
||||||
def async_setup(hass, config):
|
|
||||||
"""Set up all groups found defined in the configuration."""
|
"""Set up all groups found defined in the configuration."""
|
||||||
component = hass.data.get(DOMAIN)
|
component = hass.data.get(DOMAIN)
|
||||||
|
|
||||||
if component is None:
|
if component is None:
|
||||||
component = hass.data[DOMAIN] = EntityComponent(_LOGGER, DOMAIN, hass)
|
component = hass.data[DOMAIN] = EntityComponent(_LOGGER, DOMAIN, hass)
|
||||||
|
|
||||||
yield from _async_process_config(hass, config, component)
|
await _async_process_config(hass, config, component)
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def reload_service_handler(service):
|
||||||
def reload_service_handler(service):
|
|
||||||
"""Remove all user-defined groups and load new ones from config."""
|
"""Remove all user-defined groups and load new ones from config."""
|
||||||
auto = list(filter(lambda e: not e.user_defined, component.entities))
|
auto = list(filter(lambda e: not e.user_defined, component.entities))
|
||||||
|
|
||||||
conf = yield from component.async_prepare_reload()
|
conf = await component.async_prepare_reload()
|
||||||
if conf is None:
|
if conf is None:
|
||||||
return
|
return
|
||||||
yield from _async_process_config(hass, conf, component)
|
await _async_process_config(hass, conf, component)
|
||||||
|
|
||||||
yield from component.async_add_entities(auto)
|
await component.async_add_entities(auto)
|
||||||
|
|
||||||
hass.services.async_register(
|
hass.services.async_register(
|
||||||
DOMAIN, SERVICE_RELOAD, reload_service_handler,
|
DOMAIN, SERVICE_RELOAD, reload_service_handler,
|
||||||
schema=RELOAD_SERVICE_SCHEMA)
|
schema=RELOAD_SERVICE_SCHEMA)
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def groups_service_handler(service):
|
||||||
def groups_service_handler(service):
|
|
||||||
"""Handle dynamic group service functions."""
|
"""Handle dynamic group service functions."""
|
||||||
object_id = service.data[ATTR_OBJECT_ID]
|
object_id = service.data[ATTR_OBJECT_ID]
|
||||||
entity_id = ENTITY_ID_FORMAT.format(object_id)
|
entity_id = ENTITY_ID_FORMAT.format(object_id)
|
||||||
@ -287,7 +284,7 @@ def async_setup(hass, config):
|
|||||||
ATTR_VISIBLE, ATTR_ICON, ATTR_VIEW, ATTR_CONTROL
|
ATTR_VISIBLE, ATTR_ICON, ATTR_VIEW, ATTR_CONTROL
|
||||||
) if service.data.get(attr) is not None}
|
) if service.data.get(attr) is not None}
|
||||||
|
|
||||||
yield from Group.async_create_group(
|
await Group.async_create_group(
|
||||||
hass, service.data.get(ATTR_NAME, object_id),
|
hass, service.data.get(ATTR_NAME, object_id),
|
||||||
object_id=object_id,
|
object_id=object_id,
|
||||||
entity_ids=entity_ids,
|
entity_ids=entity_ids,
|
||||||
@ -308,11 +305,11 @@ def async_setup(hass, config):
|
|||||||
if ATTR_ADD_ENTITIES in service.data:
|
if ATTR_ADD_ENTITIES in service.data:
|
||||||
delta = service.data[ATTR_ADD_ENTITIES]
|
delta = service.data[ATTR_ADD_ENTITIES]
|
||||||
entity_ids = set(group.tracking) | set(delta)
|
entity_ids = set(group.tracking) | set(delta)
|
||||||
yield from group.async_update_tracked_entity_ids(entity_ids)
|
await group.async_update_tracked_entity_ids(entity_ids)
|
||||||
|
|
||||||
if ATTR_ENTITIES in service.data:
|
if ATTR_ENTITIES in service.data:
|
||||||
entity_ids = service.data[ATTR_ENTITIES]
|
entity_ids = service.data[ATTR_ENTITIES]
|
||||||
yield from group.async_update_tracked_entity_ids(entity_ids)
|
await group.async_update_tracked_entity_ids(entity_ids)
|
||||||
|
|
||||||
if ATTR_NAME in service.data:
|
if ATTR_NAME in service.data:
|
||||||
group.name = service.data[ATTR_NAME]
|
group.name = service.data[ATTR_NAME]
|
||||||
@ -335,13 +332,13 @@ def async_setup(hass, config):
|
|||||||
need_update = True
|
need_update = True
|
||||||
|
|
||||||
if need_update:
|
if need_update:
|
||||||
yield from group.async_update_ha_state()
|
await group.async_update_ha_state()
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
# remove group
|
# remove group
|
||||||
if service.service == SERVICE_REMOVE:
|
if service.service == SERVICE_REMOVE:
|
||||||
yield from component.async_remove_entity(entity_id)
|
await component.async_remove_entity(entity_id)
|
||||||
|
|
||||||
hass.services.async_register(
|
hass.services.async_register(
|
||||||
DOMAIN, SERVICE_SET, groups_service_handler,
|
DOMAIN, SERVICE_SET, groups_service_handler,
|
||||||
@ -351,8 +348,7 @@ def async_setup(hass, config):
|
|||||||
DOMAIN, SERVICE_REMOVE, groups_service_handler,
|
DOMAIN, SERVICE_REMOVE, groups_service_handler,
|
||||||
schema=REMOVE_SERVICE_SCHEMA)
|
schema=REMOVE_SERVICE_SCHEMA)
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def visibility_service_handler(service):
|
||||||
def visibility_service_handler(service):
|
|
||||||
"""Change visibility of a group."""
|
"""Change visibility of a group."""
|
||||||
visible = service.data.get(ATTR_VISIBLE)
|
visible = service.data.get(ATTR_VISIBLE)
|
||||||
|
|
||||||
@ -363,7 +359,7 @@ def async_setup(hass, config):
|
|||||||
tasks.append(group.async_update_ha_state())
|
tasks.append(group.async_update_ha_state())
|
||||||
|
|
||||||
if tasks:
|
if tasks:
|
||||||
yield from asyncio.wait(tasks, loop=hass.loop)
|
await asyncio.wait(tasks, loop=hass.loop)
|
||||||
|
|
||||||
hass.services.async_register(
|
hass.services.async_register(
|
||||||
DOMAIN, SERVICE_SET_VISIBILITY, visibility_service_handler,
|
DOMAIN, SERVICE_SET_VISIBILITY, visibility_service_handler,
|
||||||
@ -372,8 +368,7 @@ def async_setup(hass, config):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def _async_process_config(hass, config, component):
|
||||||
def _async_process_config(hass, config, component):
|
|
||||||
"""Process group configuration."""
|
"""Process group configuration."""
|
||||||
for object_id, conf in config.get(DOMAIN, {}).items():
|
for object_id, conf in config.get(DOMAIN, {}).items():
|
||||||
name = conf.get(CONF_NAME, object_id)
|
name = conf.get(CONF_NAME, object_id)
|
||||||
@ -384,7 +379,7 @@ def _async_process_config(hass, config, component):
|
|||||||
|
|
||||||
# Don't create tasks and await them all. The order is important as
|
# Don't create tasks and await them all. The order is important as
|
||||||
# groups get a number based on creation order.
|
# groups get a number based on creation order.
|
||||||
yield from Group.async_create_group(
|
await Group.async_create_group(
|
||||||
hass, name, entity_ids, icon=icon, view=view,
|
hass, name, entity_ids, icon=icon, view=view,
|
||||||
control=control, object_id=object_id)
|
control=control, object_id=object_id)
|
||||||
|
|
||||||
@ -428,10 +423,9 @@ class Group(Entity):
|
|||||||
hass.loop).result()
|
hass.loop).result()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@asyncio.coroutine
|
async def async_create_group(hass, name, entity_ids=None,
|
||||||
def async_create_group(hass, name, entity_ids=None, user_defined=True,
|
user_defined=True, visible=True, icon=None,
|
||||||
visible=True, icon=None, view=False, control=None,
|
view=False, control=None, object_id=None):
|
||||||
object_id=None):
|
|
||||||
"""Initialize a group.
|
"""Initialize a group.
|
||||||
|
|
||||||
This method must be run in the event loop.
|
This method must be run in the event loop.
|
||||||
@ -453,7 +447,7 @@ class Group(Entity):
|
|||||||
component = hass.data[DOMAIN] = \
|
component = hass.data[DOMAIN] = \
|
||||||
EntityComponent(_LOGGER, DOMAIN, hass)
|
EntityComponent(_LOGGER, DOMAIN, hass)
|
||||||
|
|
||||||
yield from component.async_add_entities([group], True)
|
await component.async_add_entities([group], True)
|
||||||
|
|
||||||
return group
|
return group
|
||||||
|
|
||||||
@ -520,17 +514,16 @@ class Group(Entity):
|
|||||||
self.async_update_tracked_entity_ids(entity_ids), self.hass.loop
|
self.async_update_tracked_entity_ids(entity_ids), self.hass.loop
|
||||||
).result()
|
).result()
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def async_update_tracked_entity_ids(self, entity_ids):
|
||||||
def async_update_tracked_entity_ids(self, entity_ids):
|
|
||||||
"""Update the member entity IDs.
|
"""Update the member entity IDs.
|
||||||
|
|
||||||
This method must be run in the event loop.
|
This method must be run in the event loop.
|
||||||
"""
|
"""
|
||||||
yield from self.async_stop()
|
await self.async_stop()
|
||||||
self.tracking = tuple(ent_id.lower() for ent_id in entity_ids)
|
self.tracking = tuple(ent_id.lower() for ent_id in entity_ids)
|
||||||
self.group_on, self.group_off = None, None
|
self.group_on, self.group_off = None, None
|
||||||
|
|
||||||
yield from self.async_update_ha_state(True)
|
await self.async_update_ha_state(True)
|
||||||
self.async_start()
|
self.async_start()
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
@ -544,8 +537,7 @@ class Group(Entity):
|
|||||||
self.hass, self.tracking, self._async_state_changed_listener
|
self.hass, self.tracking, self._async_state_changed_listener
|
||||||
)
|
)
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def async_stop(self):
|
||||||
def async_stop(self):
|
|
||||||
"""Unregister the group from Home Assistant.
|
"""Unregister the group from Home Assistant.
|
||||||
|
|
||||||
This method must be run in the event loop.
|
This method must be run in the event loop.
|
||||||
@ -554,27 +546,24 @@ class Group(Entity):
|
|||||||
self._async_unsub_state_changed()
|
self._async_unsub_state_changed()
|
||||||
self._async_unsub_state_changed = None
|
self._async_unsub_state_changed = None
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def async_update(self):
|
||||||
def async_update(self):
|
|
||||||
"""Query all members and determine current group state."""
|
"""Query all members and determine current group state."""
|
||||||
self._state = STATE_UNKNOWN
|
self._state = STATE_UNKNOWN
|
||||||
self._async_update_group_state()
|
self._async_update_group_state()
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def async_added_to_hass(self):
|
||||||
def async_added_to_hass(self):
|
|
||||||
"""Callback when added to HASS."""
|
"""Callback when added to HASS."""
|
||||||
if self.tracking:
|
if self.tracking:
|
||||||
self.async_start()
|
self.async_start()
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def async_will_remove_from_hass(self):
|
||||||
def async_will_remove_from_hass(self):
|
|
||||||
"""Callback when removed from HASS."""
|
"""Callback when removed from HASS."""
|
||||||
if self._async_unsub_state_changed:
|
if self._async_unsub_state_changed:
|
||||||
self._async_unsub_state_changed()
|
self._async_unsub_state_changed()
|
||||||
self._async_unsub_state_changed = None
|
self._async_unsub_state_changed = None
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def _async_state_changed_listener(self, entity_id, old_state,
|
||||||
def _async_state_changed_listener(self, entity_id, old_state, new_state):
|
new_state):
|
||||||
"""Respond to a member state changing.
|
"""Respond to a member state changing.
|
||||||
|
|
||||||
This method must be run in the event loop.
|
This method must be run in the event loop.
|
||||||
@ -584,7 +573,7 @@ class Group(Entity):
|
|||||||
return
|
return
|
||||||
|
|
||||||
self._async_update_group_state(new_state)
|
self._async_update_group_state(new_state)
|
||||||
yield from self.async_update_ha_state()
|
await self.async_update_ha_state()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _tracking_states(self):
|
def _tracking_states(self):
|
||||||
|
@ -4,7 +4,6 @@ Provide pre-made queries on top of the recorder component.
|
|||||||
For more details about this component, please refer to the documentation at
|
For more details about this component, please refer to the documentation at
|
||||||
https://home-assistant.io/components/history/
|
https://home-assistant.io/components/history/
|
||||||
"""
|
"""
|
||||||
import asyncio
|
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from itertools import groupby
|
from itertools import groupby
|
||||||
@ -259,8 +258,7 @@ def get_state(hass, utc_point_in_time, entity_id, run=None):
|
|||||||
return states[0] if states else None
|
return states[0] if states else None
|
||||||
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def async_setup(hass, config):
|
||||||
def async_setup(hass, config):
|
|
||||||
"""Set up the history hooks."""
|
"""Set up the history hooks."""
|
||||||
filters = Filters()
|
filters = Filters()
|
||||||
conf = config.get(DOMAIN, {})
|
conf = config.get(DOMAIN, {})
|
||||||
@ -275,7 +273,7 @@ def async_setup(hass, config):
|
|||||||
use_include_order = conf.get(CONF_ORDER)
|
use_include_order = conf.get(CONF_ORDER)
|
||||||
|
|
||||||
hass.http.register_view(HistoryPeriodView(filters, use_include_order))
|
hass.http.register_view(HistoryPeriodView(filters, use_include_order))
|
||||||
yield from hass.components.frontend.async_register_built_in_panel(
|
await hass.components.frontend.async_register_built_in_panel(
|
||||||
'history', 'history', 'mdi:poll-box')
|
'history', 'history', 'mdi:poll-box')
|
||||||
|
|
||||||
return True
|
return True
|
||||||
@ -293,8 +291,7 @@ class HistoryPeriodView(HomeAssistantView):
|
|||||||
self.filters = filters
|
self.filters = filters
|
||||||
self.use_include_order = use_include_order
|
self.use_include_order = use_include_order
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def get(self, request, datetime=None):
|
||||||
def get(self, request, datetime=None):
|
|
||||||
"""Return history over a period of time."""
|
"""Return history over a period of time."""
|
||||||
timer_start = time.perf_counter()
|
timer_start = time.perf_counter()
|
||||||
if datetime:
|
if datetime:
|
||||||
@ -330,7 +327,7 @@ class HistoryPeriodView(HomeAssistantView):
|
|||||||
|
|
||||||
hass = request.app['hass']
|
hass = request.app['hass']
|
||||||
|
|
||||||
result = yield from hass.async_add_job(
|
result = await hass.async_add_job(
|
||||||
get_significant_states, hass, start_time, end_time,
|
get_significant_states, hass, start_time, end_time,
|
||||||
entity_ids, self.filters, include_start_time_state)
|
entity_ids, self.filters, include_start_time_state)
|
||||||
result = list(result.values())
|
result = list(result.values())
|
||||||
@ -353,8 +350,7 @@ class HistoryPeriodView(HomeAssistantView):
|
|||||||
sorted_result.extend(result)
|
sorted_result.extend(result)
|
||||||
result = sorted_result
|
result = sorted_result
|
||||||
|
|
||||||
response = yield from hass.async_add_job(self.json, result)
|
return await hass.async_add_job(self.json, result)
|
||||||
return response
|
|
||||||
|
|
||||||
|
|
||||||
class Filters(object):
|
class Filters(object):
|
||||||
|
@ -4,7 +4,6 @@ Support to graphs card in the UI.
|
|||||||
For more details about this component, please refer to the documentation at
|
For more details about this component, please refer to the documentation at
|
||||||
https://home-assistant.io/components/history_graph/
|
https://home-assistant.io/components/history_graph/
|
||||||
"""
|
"""
|
||||||
import asyncio
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
@ -39,8 +38,7 @@ CONFIG_SCHEMA = vol.Schema({
|
|||||||
}, extra=vol.ALLOW_EXTRA)
|
}, extra=vol.ALLOW_EXTRA)
|
||||||
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def async_setup(hass, config):
|
||||||
def async_setup(hass, config):
|
|
||||||
"""Load graph configurations."""
|
"""Load graph configurations."""
|
||||||
component = EntityComponent(
|
component = EntityComponent(
|
||||||
_LOGGER, DOMAIN, hass)
|
_LOGGER, DOMAIN, hass)
|
||||||
@ -51,7 +49,7 @@ def async_setup(hass, config):
|
|||||||
graph = HistoryGraphEntity(name, cfg)
|
graph = HistoryGraphEntity(name, cfg)
|
||||||
graphs.append(graph)
|
graphs.append(graph)
|
||||||
|
|
||||||
yield from component.async_add_entities(graphs)
|
await component.async_add_entities(graphs)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@ -14,7 +14,8 @@ from homeassistant.components.cover import (
|
|||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT,
|
ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT,
|
||||||
ATTR_DEVICE_CLASS, CONF_IP_ADDRESS, CONF_PORT, TEMP_CELSIUS,
|
ATTR_DEVICE_CLASS, CONF_IP_ADDRESS, CONF_PORT, TEMP_CELSIUS,
|
||||||
TEMP_FAHRENHEIT, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP)
|
TEMP_FAHRENHEIT, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP,
|
||||||
|
DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_TEMPERATURE)
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
from homeassistant.helpers.entityfilter import FILTER_SCHEMA
|
from homeassistant.helpers.entityfilter import FILTER_SCHEMA
|
||||||
from homeassistant.util import get_local_ip
|
from homeassistant.util import get_local_ip
|
||||||
@ -22,15 +23,20 @@ from homeassistant.util.decorator import Registry
|
|||||||
from .const import (
|
from .const import (
|
||||||
DOMAIN, HOMEKIT_FILE, CONF_AUTO_START, CONF_ENTITY_CONFIG, CONF_FILTER,
|
DOMAIN, HOMEKIT_FILE, CONF_AUTO_START, CONF_ENTITY_CONFIG, CONF_FILTER,
|
||||||
DEFAULT_PORT, DEFAULT_AUTO_START, SERVICE_HOMEKIT_START,
|
DEFAULT_PORT, DEFAULT_AUTO_START, SERVICE_HOMEKIT_START,
|
||||||
DEVICE_CLASS_CO2, DEVICE_CLASS_LIGHT, DEVICE_CLASS_HUMIDITY,
|
DEVICE_CLASS_CO2, DEVICE_CLASS_PM25)
|
||||||
DEVICE_CLASS_PM25, DEVICE_CLASS_TEMPERATURE)
|
|
||||||
from .util import (
|
from .util import (
|
||||||
validate_entity_config, show_setup_message)
|
validate_entity_config, show_setup_message)
|
||||||
|
|
||||||
TYPES = Registry()
|
TYPES = Registry()
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
REQUIREMENTS = ['HAP-python==1.1.9']
|
REQUIREMENTS = ['HAP-python==2.0.0']
|
||||||
|
|
||||||
|
# #### Driver Status ####
|
||||||
|
STATUS_READY = 0
|
||||||
|
STATUS_RUNNING = 1
|
||||||
|
STATUS_STOPPED = 2
|
||||||
|
STATUS_WAIT = 3
|
||||||
|
|
||||||
|
|
||||||
CONFIG_SCHEMA = vol.Schema({
|
CONFIG_SCHEMA = vol.Schema({
|
||||||
@ -57,7 +63,7 @@ async def async_setup(hass, config):
|
|||||||
entity_config = conf[CONF_ENTITY_CONFIG]
|
entity_config = conf[CONF_ENTITY_CONFIG]
|
||||||
|
|
||||||
homekit = HomeKit(hass, port, ip_address, entity_filter, entity_config)
|
homekit = HomeKit(hass, port, ip_address, entity_filter, entity_config)
|
||||||
homekit.setup()
|
await hass.async_add_job(homekit.setup)
|
||||||
|
|
||||||
if auto_start:
|
if auto_start:
|
||||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, homekit.start)
|
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, homekit.start)
|
||||||
@ -65,8 +71,10 @@ async def async_setup(hass, config):
|
|||||||
|
|
||||||
def handle_homekit_service_start(service):
|
def handle_homekit_service_start(service):
|
||||||
"""Handle start HomeKit service call."""
|
"""Handle start HomeKit service call."""
|
||||||
if homekit.started:
|
if homekit.status != STATUS_READY:
|
||||||
_LOGGER.warning('HomeKit is already running')
|
_LOGGER.warning(
|
||||||
|
'HomeKit is not ready. Either it is already running or has '
|
||||||
|
'been stopped.')
|
||||||
return
|
return
|
||||||
homekit.start()
|
homekit.start()
|
||||||
|
|
||||||
@ -118,10 +126,10 @@ def get_accessory(hass, state, aid, config):
|
|||||||
unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
|
unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
|
||||||
device_class = state.attributes.get(ATTR_DEVICE_CLASS)
|
device_class = state.attributes.get(ATTR_DEVICE_CLASS)
|
||||||
|
|
||||||
if device_class == DEVICE_CLASS_TEMPERATURE or unit == TEMP_CELSIUS \
|
if device_class == DEVICE_CLASS_TEMPERATURE or \
|
||||||
or unit == TEMP_FAHRENHEIT:
|
unit in (TEMP_CELSIUS, TEMP_FAHRENHEIT):
|
||||||
a_type = 'TemperatureSensor'
|
a_type = 'TemperatureSensor'
|
||||||
elif device_class == DEVICE_CLASS_HUMIDITY or unit == '%':
|
elif device_class == DEVICE_CLASS_HUMIDITY and unit == '%':
|
||||||
a_type = 'HumiditySensor'
|
a_type = 'HumiditySensor'
|
||||||
elif device_class == DEVICE_CLASS_PM25 \
|
elif device_class == DEVICE_CLASS_PM25 \
|
||||||
or DEVICE_CLASS_PM25 in state.entity_id:
|
or DEVICE_CLASS_PM25 in state.entity_id:
|
||||||
@ -129,12 +137,10 @@ def get_accessory(hass, state, aid, config):
|
|||||||
elif device_class == DEVICE_CLASS_CO2 \
|
elif device_class == DEVICE_CLASS_CO2 \
|
||||||
or DEVICE_CLASS_CO2 in state.entity_id:
|
or DEVICE_CLASS_CO2 in state.entity_id:
|
||||||
a_type = 'CarbonDioxideSensor'
|
a_type = 'CarbonDioxideSensor'
|
||||||
elif device_class == DEVICE_CLASS_LIGHT or unit == 'lm' or \
|
elif device_class == DEVICE_CLASS_ILLUMINANCE or unit in ('lm', 'lx'):
|
||||||
unit == 'lux':
|
|
||||||
a_type = 'LightSensor'
|
a_type = 'LightSensor'
|
||||||
|
|
||||||
elif state.domain == 'switch' or state.domain == 'remote' \
|
elif state.domain in ('switch', 'remote', 'input_boolean', 'script'):
|
||||||
or state.domain == 'input_boolean' or state.domain == 'script':
|
|
||||||
a_type = 'Switch'
|
a_type = 'Switch'
|
||||||
|
|
||||||
if a_type is None:
|
if a_type is None:
|
||||||
@ -162,7 +168,7 @@ class HomeKit():
|
|||||||
self._ip_address = ip_address
|
self._ip_address = ip_address
|
||||||
self._filter = entity_filter
|
self._filter = entity_filter
|
||||||
self._config = entity_config
|
self._config = entity_config
|
||||||
self.started = False
|
self.status = STATUS_READY
|
||||||
|
|
||||||
self.bridge = None
|
self.bridge = None
|
||||||
self.driver = None
|
self.driver = None
|
||||||
@ -191,9 +197,9 @@ class HomeKit():
|
|||||||
|
|
||||||
def start(self, *args):
|
def start(self, *args):
|
||||||
"""Start the accessory driver."""
|
"""Start the accessory driver."""
|
||||||
if self.started:
|
if self.status != STATUS_READY:
|
||||||
return
|
return
|
||||||
self.started = True
|
self.status = STATUS_WAIT
|
||||||
|
|
||||||
# pylint: disable=unused-variable
|
# pylint: disable=unused-variable
|
||||||
from . import ( # noqa F401
|
from . import ( # noqa F401
|
||||||
@ -202,19 +208,20 @@ class HomeKit():
|
|||||||
|
|
||||||
for state in self.hass.states.all():
|
for state in self.hass.states.all():
|
||||||
self.add_bridge_accessory(state)
|
self.add_bridge_accessory(state)
|
||||||
self.bridge.set_broker(self.driver)
|
self.bridge.set_driver(self.driver)
|
||||||
|
|
||||||
if not self.bridge.paired:
|
if not self.bridge.paired:
|
||||||
show_setup_message(self.hass, self.bridge)
|
show_setup_message(self.hass, self.bridge)
|
||||||
|
|
||||||
_LOGGER.debug('Driver start')
|
_LOGGER.debug('Driver start')
|
||||||
self.driver.start()
|
self.hass.add_job(self.driver.start)
|
||||||
|
self.status = STATUS_RUNNING
|
||||||
|
|
||||||
def stop(self, *args):
|
def stop(self, *args):
|
||||||
"""Stop the accessory driver."""
|
"""Stop the accessory driver."""
|
||||||
if not self.started:
|
if self.status != STATUS_RUNNING:
|
||||||
return
|
return
|
||||||
|
self.status = STATUS_STOPPED
|
||||||
|
|
||||||
_LOGGER.debug('Driver stop')
|
_LOGGER.debug('Driver stop')
|
||||||
if self.driver and self.driver.run_sentinel:
|
self.hass.add_job(self.driver.stop)
|
||||||
self.driver.stop()
|
|
||||||
|
@ -4,18 +4,20 @@ from functools import wraps
|
|||||||
from inspect import getmodule
|
from inspect import getmodule
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from pyhap.accessory import Accessory, Bridge, Category
|
from pyhap.accessory import Accessory, Bridge
|
||||||
from pyhap.accessory_driver import AccessoryDriver
|
from pyhap.accessory_driver import AccessoryDriver
|
||||||
|
from pyhap.const import CATEGORY_OTHER
|
||||||
|
|
||||||
|
from homeassistant.const import __version__
|
||||||
from homeassistant.core import callback as ha_callback
|
from homeassistant.core import callback as ha_callback
|
||||||
|
from homeassistant.core import split_entity_id
|
||||||
from homeassistant.helpers.event import (
|
from homeassistant.helpers.event import (
|
||||||
async_track_state_change, track_point_in_utc_time)
|
async_track_state_change, track_point_in_utc_time)
|
||||||
from homeassistant.util import dt as dt_util
|
from homeassistant.util import dt as dt_util
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
DEBOUNCE_TIMEOUT, BRIDGE_MODEL, BRIDGE_NAME, MANUFACTURER,
|
DEBOUNCE_TIMEOUT, BRIDGE_MODEL, BRIDGE_NAME,
|
||||||
SERV_ACCESSORY_INFO, CHAR_MANUFACTURER,
|
BRIDGE_SERIAL_NUMBER, MANUFACTURER)
|
||||||
CHAR_MODEL, CHAR_NAME, CHAR_SERIAL_NUMBER)
|
|
||||||
from .util import (
|
from .util import (
|
||||||
show_setup_message, dismiss_setup_message)
|
show_setup_message, dismiss_setup_message)
|
||||||
|
|
||||||
@ -59,55 +61,20 @@ def debounce(func):
|
|||||||
return wrapper
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
def add_preload_service(acc, service, chars=None):
|
|
||||||
"""Define and return a service to be available for the accessory."""
|
|
||||||
from pyhap.loader import get_serv_loader, get_char_loader
|
|
||||||
service = get_serv_loader().get(service)
|
|
||||||
if chars:
|
|
||||||
chars = chars if isinstance(chars, list) else [chars]
|
|
||||||
for char_name in chars:
|
|
||||||
char = get_char_loader().get(char_name)
|
|
||||||
service.add_characteristic(char)
|
|
||||||
acc.add_service(service)
|
|
||||||
return service
|
|
||||||
|
|
||||||
|
|
||||||
def setup_char(char_name, service, value=None, properties=None, callback=None):
|
|
||||||
"""Helper function to return fully configured characteristic."""
|
|
||||||
char = service.get_characteristic(char_name)
|
|
||||||
if value:
|
|
||||||
char.value = value
|
|
||||||
if properties:
|
|
||||||
char.override_properties(properties)
|
|
||||||
if callback:
|
|
||||||
char.setter_callback = callback
|
|
||||||
return char
|
|
||||||
|
|
||||||
|
|
||||||
def set_accessory_info(acc, name, model, manufacturer=MANUFACTURER,
|
|
||||||
serial_number='0000'):
|
|
||||||
"""Set the default accessory information."""
|
|
||||||
service = acc.get_service(SERV_ACCESSORY_INFO)
|
|
||||||
service.get_characteristic(CHAR_NAME).set_value(name)
|
|
||||||
service.get_characteristic(CHAR_MODEL).set_value(model)
|
|
||||||
service.get_characteristic(CHAR_MANUFACTURER).set_value(manufacturer)
|
|
||||||
service.get_characteristic(CHAR_SERIAL_NUMBER).set_value(serial_number)
|
|
||||||
|
|
||||||
|
|
||||||
class HomeAccessory(Accessory):
|
class HomeAccessory(Accessory):
|
||||||
"""Adapter class for Accessory."""
|
"""Adapter class for Accessory."""
|
||||||
|
|
||||||
def __init__(self, hass, name, entity_id, aid, category):
|
def __init__(self, hass, name, entity_id, aid, category=CATEGORY_OTHER):
|
||||||
"""Initialize a Accessory object."""
|
"""Initialize a Accessory object."""
|
||||||
super().__init__(name, aid=aid)
|
super().__init__(name, aid=aid)
|
||||||
set_accessory_info(self, name, model=entity_id)
|
domain = split_entity_id(entity_id)[0].replace("_", " ").title()
|
||||||
self.category = getattr(Category, category, Category.OTHER)
|
self.set_info_service(
|
||||||
|
firmware_revision=__version__, manufacturer=MANUFACTURER,
|
||||||
|
model=domain, serial_number=entity_id)
|
||||||
|
self.category = category
|
||||||
self.entity_id = entity_id
|
self.entity_id = entity_id
|
||||||
self.hass = hass
|
self.hass = hass
|
||||||
|
|
||||||
def _set_services(self):
|
|
||||||
add_preload_service(self, SERV_ACCESSORY_INFO)
|
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
"""Method called by accessory after driver is started."""
|
"""Method called by accessory after driver is started."""
|
||||||
state = self.hass.states.get(self.entity_id)
|
state = self.hass.states.get(self.entity_id)
|
||||||
@ -137,12 +104,11 @@ class HomeBridge(Bridge):
|
|||||||
def __init__(self, hass, name=BRIDGE_NAME):
|
def __init__(self, hass, name=BRIDGE_NAME):
|
||||||
"""Initialize a Bridge object."""
|
"""Initialize a Bridge object."""
|
||||||
super().__init__(name)
|
super().__init__(name)
|
||||||
set_accessory_info(self, name, model=BRIDGE_MODEL)
|
self.set_info_service(
|
||||||
|
firmware_revision=__version__, manufacturer=MANUFACTURER,
|
||||||
|
model=BRIDGE_MODEL, serial_number=BRIDGE_SERIAL_NUMBER)
|
||||||
self.hass = hass
|
self.hass = hass
|
||||||
|
|
||||||
def _set_services(self):
|
|
||||||
add_preload_service(self, SERV_ACCESSORY_INFO)
|
|
||||||
|
|
||||||
def setup_message(self):
|
def setup_message(self):
|
||||||
"""Prevent print of pyhap setup message to terminal."""
|
"""Prevent print of pyhap setup message to terminal."""
|
||||||
pass
|
pass
|
||||||
|
@ -18,20 +18,10 @@ DEFAULT_PORT = 51827
|
|||||||
SERVICE_HOMEKIT_START = 'start'
|
SERVICE_HOMEKIT_START = 'start'
|
||||||
|
|
||||||
# #### STRING CONSTANTS ####
|
# #### STRING CONSTANTS ####
|
||||||
BRIDGE_MODEL = 'homekit.bridge'
|
BRIDGE_MODEL = 'Bridge'
|
||||||
BRIDGE_NAME = 'Home Assistant'
|
BRIDGE_NAME = 'Home Assistant Bridge'
|
||||||
MANUFACTURER = 'HomeAssistant'
|
BRIDGE_SERIAL_NUMBER = 'homekit.bridge'
|
||||||
|
MANUFACTURER = 'Home Assistant'
|
||||||
# #### Categories ####
|
|
||||||
CATEGORY_ALARM_SYSTEM = 'ALARM_SYSTEM'
|
|
||||||
CATEGORY_GARAGE_DOOR_OPENER = 'GARAGE_DOOR_OPENER'
|
|
||||||
CATEGORY_LIGHT = 'LIGHTBULB'
|
|
||||||
CATEGORY_LOCK = 'DOOR_LOCK'
|
|
||||||
CATEGORY_SENSOR = 'SENSOR'
|
|
||||||
CATEGORY_SWITCH = 'SWITCH'
|
|
||||||
CATEGORY_THERMOSTAT = 'THERMOSTAT'
|
|
||||||
CATEGORY_WINDOW_COVERING = 'WINDOW_COVERING'
|
|
||||||
|
|
||||||
|
|
||||||
# #### Services ####
|
# #### Services ####
|
||||||
SERV_ACCESSORY_INFO = 'AccessoryInformation'
|
SERV_ACCESSORY_INFO = 'AccessoryInformation'
|
||||||
@ -55,7 +45,6 @@ SERV_THERMOSTAT = 'Thermostat'
|
|||||||
SERV_WINDOW_COVERING = 'WindowCovering'
|
SERV_WINDOW_COVERING = 'WindowCovering'
|
||||||
# CurrentPosition, TargetPosition, PositionState
|
# CurrentPosition, TargetPosition, PositionState
|
||||||
|
|
||||||
|
|
||||||
# #### Characteristics ####
|
# #### Characteristics ####
|
||||||
CHAR_AIR_PARTICULATE_DENSITY = 'AirParticulateDensity'
|
CHAR_AIR_PARTICULATE_DENSITY = 'AirParticulateDensity'
|
||||||
CHAR_AIR_QUALITY = 'AirQuality'
|
CHAR_AIR_QUALITY = 'AirQuality'
|
||||||
@ -74,6 +63,7 @@ CHAR_CURRENT_POSITION = 'CurrentPosition' # Int | [0, 100]
|
|||||||
CHAR_CURRENT_HUMIDITY = 'CurrentRelativeHumidity' # percent
|
CHAR_CURRENT_HUMIDITY = 'CurrentRelativeHumidity' # percent
|
||||||
CHAR_CURRENT_SECURITY_STATE = 'SecuritySystemCurrentState'
|
CHAR_CURRENT_SECURITY_STATE = 'SecuritySystemCurrentState'
|
||||||
CHAR_CURRENT_TEMPERATURE = 'CurrentTemperature'
|
CHAR_CURRENT_TEMPERATURE = 'CurrentTemperature'
|
||||||
|
CHAR_FIRMWARE_REVISION = 'FirmwareRevision'
|
||||||
CHAR_HEATING_THRESHOLD_TEMPERATURE = 'HeatingThresholdTemperature'
|
CHAR_HEATING_THRESHOLD_TEMPERATURE = 'HeatingThresholdTemperature'
|
||||||
CHAR_HUE = 'Hue' # arcdegress | [0, 360]
|
CHAR_HUE = 'Hue' # arcdegress | [0, 360]
|
||||||
CHAR_LEAK_DETECTED = 'LeakDetected'
|
CHAR_LEAK_DETECTED = 'LeakDetected'
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
"""Class to hold all cover accessories."""
|
"""Class to hold all cover accessories."""
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
from pyhap.const import CATEGORY_WINDOW_COVERING, CATEGORY_GARAGE_DOOR_OPENER
|
||||||
|
|
||||||
from homeassistant.components.cover import (
|
from homeassistant.components.cover import (
|
||||||
ATTR_CURRENT_POSITION, ATTR_POSITION, DOMAIN, SUPPORT_STOP)
|
ATTR_CURRENT_POSITION, ATTR_POSITION, DOMAIN, SUPPORT_STOP)
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
@ -9,12 +11,11 @@ from homeassistant.const import (
|
|||||||
ATTR_SUPPORTED_FEATURES)
|
ATTR_SUPPORTED_FEATURES)
|
||||||
|
|
||||||
from . import TYPES
|
from . import TYPES
|
||||||
from .accessories import HomeAccessory, add_preload_service, setup_char
|
from .accessories import HomeAccessory, debounce
|
||||||
from .const import (
|
from .const import (
|
||||||
CATEGORY_WINDOW_COVERING, SERV_WINDOW_COVERING,
|
SERV_WINDOW_COVERING, CHAR_CURRENT_POSITION,
|
||||||
CHAR_CURRENT_POSITION, CHAR_TARGET_POSITION, CHAR_POSITION_STATE,
|
CHAR_TARGET_POSITION, CHAR_POSITION_STATE,
|
||||||
CATEGORY_GARAGE_DOOR_OPENER, SERV_GARAGE_DOOR_OPENER,
|
SERV_GARAGE_DOOR_OPENER, CHAR_CURRENT_DOOR_STATE, CHAR_TARGET_DOOR_STATE)
|
||||||
CHAR_CURRENT_DOOR_STATE, CHAR_TARGET_DOOR_STATE)
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -32,12 +33,11 @@ class GarageDoorOpener(HomeAccessory):
|
|||||||
super().__init__(*args, category=CATEGORY_GARAGE_DOOR_OPENER)
|
super().__init__(*args, category=CATEGORY_GARAGE_DOOR_OPENER)
|
||||||
self.flag_target_state = False
|
self.flag_target_state = False
|
||||||
|
|
||||||
serv_garage_door = add_preload_service(self, SERV_GARAGE_DOOR_OPENER)
|
serv_garage_door = self.add_preload_service(SERV_GARAGE_DOOR_OPENER)
|
||||||
self.char_current_state = setup_char(
|
self.char_current_state = serv_garage_door.configure_char(
|
||||||
CHAR_CURRENT_DOOR_STATE, serv_garage_door, value=0)
|
CHAR_CURRENT_DOOR_STATE, value=0)
|
||||||
self.char_target_state = setup_char(
|
self.char_target_state = serv_garage_door.configure_char(
|
||||||
CHAR_TARGET_DOOR_STATE, serv_garage_door, value=0,
|
CHAR_TARGET_DOOR_STATE, value=0, setter_callback=self.set_state)
|
||||||
callback=self.set_state)
|
|
||||||
|
|
||||||
def set_state(self, value):
|
def set_state(self, value):
|
||||||
"""Change garage state if call came from HomeKit."""
|
"""Change garage state if call came from HomeKit."""
|
||||||
@ -74,13 +74,13 @@ class WindowCovering(HomeAccessory):
|
|||||||
super().__init__(*args, category=CATEGORY_WINDOW_COVERING)
|
super().__init__(*args, category=CATEGORY_WINDOW_COVERING)
|
||||||
self.homekit_target = None
|
self.homekit_target = None
|
||||||
|
|
||||||
serv_cover = add_preload_service(self, SERV_WINDOW_COVERING)
|
serv_cover = self.add_preload_service(SERV_WINDOW_COVERING)
|
||||||
self.char_current_position = setup_char(
|
self.char_current_position = serv_cover.configure_char(
|
||||||
CHAR_CURRENT_POSITION, serv_cover, value=0)
|
CHAR_CURRENT_POSITION, value=0)
|
||||||
self.char_target_position = setup_char(
|
self.char_target_position = serv_cover.configure_char(
|
||||||
CHAR_TARGET_POSITION, serv_cover, value=0,
|
CHAR_TARGET_POSITION, value=0, setter_callback=self.move_cover)
|
||||||
callback=self.move_cover)
|
|
||||||
|
|
||||||
|
@debounce
|
||||||
def move_cover(self, value):
|
def move_cover(self, value):
|
||||||
"""Move cover to value if call came from HomeKit."""
|
"""Move cover to value if call came from HomeKit."""
|
||||||
_LOGGER.debug('%s: Set position to %d', self.entity_id, value)
|
_LOGGER.debug('%s: Set position to %d', self.entity_id, value)
|
||||||
@ -115,15 +115,15 @@ class WindowCoveringBasic(HomeAccessory):
|
|||||||
.attributes.get(ATTR_SUPPORTED_FEATURES)
|
.attributes.get(ATTR_SUPPORTED_FEATURES)
|
||||||
self.supports_stop = features & SUPPORT_STOP
|
self.supports_stop = features & SUPPORT_STOP
|
||||||
|
|
||||||
serv_cover = add_preload_service(self, SERV_WINDOW_COVERING)
|
serv_cover = self.add_preload_service(SERV_WINDOW_COVERING)
|
||||||
self.char_current_position = setup_char(
|
self.char_current_position = serv_cover.configure_char(
|
||||||
CHAR_CURRENT_POSITION, serv_cover, value=0)
|
CHAR_CURRENT_POSITION, value=0)
|
||||||
self.char_target_position = setup_char(
|
self.char_target_position = serv_cover.configure_char(
|
||||||
CHAR_TARGET_POSITION, serv_cover, value=0,
|
CHAR_TARGET_POSITION, value=0, setter_callback=self.move_cover)
|
||||||
callback=self.move_cover)
|
self.char_position_state = serv_cover.configure_char(
|
||||||
self.char_position_state = setup_char(
|
CHAR_POSITION_STATE, value=2)
|
||||||
CHAR_POSITION_STATE, serv_cover, value=2)
|
|
||||||
|
|
||||||
|
@debounce
|
||||||
def move_cover(self, value):
|
def move_cover(self, value):
|
||||||
"""Move cover to value if call came from HomeKit."""
|
"""Move cover to value if call came from HomeKit."""
|
||||||
_LOGGER.debug('%s: Set position to %d', self.entity_id, value)
|
_LOGGER.debug('%s: Set position to %d', self.entity_id, value)
|
||||||
|
@ -1,16 +1,17 @@
|
|||||||
"""Class to hold all light accessories."""
|
"""Class to hold all light accessories."""
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
from pyhap.const import CATEGORY_LIGHTBULB
|
||||||
|
|
||||||
from homeassistant.components.light import (
|
from homeassistant.components.light import (
|
||||||
ATTR_HS_COLOR, ATTR_COLOR_TEMP, ATTR_BRIGHTNESS, ATTR_MIN_MIREDS,
|
ATTR_HS_COLOR, ATTR_COLOR_TEMP, ATTR_BRIGHTNESS, ATTR_MIN_MIREDS,
|
||||||
ATTR_MAX_MIREDS, SUPPORT_COLOR, SUPPORT_COLOR_TEMP, SUPPORT_BRIGHTNESS)
|
ATTR_MAX_MIREDS, SUPPORT_COLOR, SUPPORT_COLOR_TEMP, SUPPORT_BRIGHTNESS)
|
||||||
from homeassistant.const import ATTR_SUPPORTED_FEATURES, STATE_ON, STATE_OFF
|
from homeassistant.const import ATTR_SUPPORTED_FEATURES, STATE_ON, STATE_OFF
|
||||||
|
|
||||||
from . import TYPES
|
from . import TYPES
|
||||||
from .accessories import (
|
from .accessories import HomeAccessory, debounce
|
||||||
HomeAccessory, add_preload_service, debounce, setup_char)
|
|
||||||
from .const import (
|
from .const import (
|
||||||
CATEGORY_LIGHT, SERV_LIGHTBULB, CHAR_COLOR_TEMPERATURE,
|
SERV_LIGHTBULB, CHAR_COLOR_TEMPERATURE,
|
||||||
CHAR_BRIGHTNESS, CHAR_HUE, CHAR_ON, CHAR_SATURATION)
|
CHAR_BRIGHTNESS, CHAR_HUE, CHAR_ON, CHAR_SATURATION)
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
@ -27,7 +28,7 @@ class Light(HomeAccessory):
|
|||||||
|
|
||||||
def __init__(self, *args, config):
|
def __init__(self, *args, config):
|
||||||
"""Initialize a new Light accessory object."""
|
"""Initialize a new Light accessory object."""
|
||||||
super().__init__(*args, category=CATEGORY_LIGHT)
|
super().__init__(*args, category=CATEGORY_LIGHTBULB)
|
||||||
self._flag = {CHAR_ON: False, CHAR_BRIGHTNESS: False,
|
self._flag = {CHAR_ON: False, CHAR_BRIGHTNESS: False,
|
||||||
CHAR_HUE: False, CHAR_SATURATION: False,
|
CHAR_HUE: False, CHAR_SATURATION: False,
|
||||||
CHAR_COLOR_TEMPERATURE: False, RGB_COLOR: False}
|
CHAR_COLOR_TEMPERATURE: False, RGB_COLOR: False}
|
||||||
@ -46,30 +47,28 @@ class Light(HomeAccessory):
|
|||||||
self._hue = None
|
self._hue = None
|
||||||
self._saturation = None
|
self._saturation = None
|
||||||
|
|
||||||
serv_light = add_preload_service(self, SERV_LIGHTBULB, self.chars)
|
serv_light = self.add_preload_service(SERV_LIGHTBULB, self.chars)
|
||||||
self.char_on = setup_char(
|
self.char_on = serv_light.configure_char(
|
||||||
CHAR_ON, serv_light, value=self._state, callback=self.set_state)
|
CHAR_ON, value=self._state, setter_callback=self.set_state)
|
||||||
|
|
||||||
if CHAR_BRIGHTNESS in self.chars:
|
if CHAR_BRIGHTNESS in self.chars:
|
||||||
self.char_brightness = setup_char(
|
self.char_brightness = serv_light.configure_char(
|
||||||
CHAR_BRIGHTNESS, serv_light, value=0,
|
CHAR_BRIGHTNESS, value=0, setter_callback=self.set_brightness)
|
||||||
callback=self.set_brightness)
|
|
||||||
if CHAR_COLOR_TEMPERATURE in self.chars:
|
if CHAR_COLOR_TEMPERATURE in self.chars:
|
||||||
min_mireds = self.hass.states.get(self.entity_id) \
|
min_mireds = self.hass.states.get(self.entity_id) \
|
||||||
.attributes.get(ATTR_MIN_MIREDS, 153)
|
.attributes.get(ATTR_MIN_MIREDS, 153)
|
||||||
max_mireds = self.hass.states.get(self.entity_id) \
|
max_mireds = self.hass.states.get(self.entity_id) \
|
||||||
.attributes.get(ATTR_MAX_MIREDS, 500)
|
.attributes.get(ATTR_MAX_MIREDS, 500)
|
||||||
self.char_color_temperature = setup_char(
|
self.char_color_temperature = serv_light.configure_char(
|
||||||
CHAR_COLOR_TEMPERATURE, serv_light, value=min_mireds,
|
CHAR_COLOR_TEMPERATURE, value=min_mireds,
|
||||||
properties={'minValue': min_mireds, 'maxValue': max_mireds},
|
properties={'minValue': min_mireds, 'maxValue': max_mireds},
|
||||||
callback=self.set_color_temperature)
|
setter_callback=self.set_color_temperature)
|
||||||
if CHAR_HUE in self.chars:
|
if CHAR_HUE in self.chars:
|
||||||
self.char_hue = setup_char(
|
self.char_hue = serv_light.configure_char(
|
||||||
CHAR_HUE, serv_light, value=0, callback=self.set_hue)
|
CHAR_HUE, value=0, setter_callback=self.set_hue)
|
||||||
if CHAR_SATURATION in self.chars:
|
if CHAR_SATURATION in self.chars:
|
||||||
self.char_saturation = setup_char(
|
self.char_saturation = serv_light.configure_char(
|
||||||
CHAR_SATURATION, serv_light, value=75,
|
CHAR_SATURATION, value=75, setter_callback=self.set_saturation)
|
||||||
callback=self.set_saturation)
|
|
||||||
|
|
||||||
def set_state(self, value):
|
def set_state(self, value):
|
||||||
"""Set state if call came from HomeKit."""
|
"""Set state if call came from HomeKit."""
|
||||||
|
@ -1,13 +1,15 @@
|
|||||||
"""Class to hold all lock accessories."""
|
"""Class to hold all lock accessories."""
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
from pyhap.const import CATEGORY_DOOR_LOCK
|
||||||
|
|
||||||
from homeassistant.components.lock import (
|
from homeassistant.components.lock import (
|
||||||
ATTR_ENTITY_ID, STATE_LOCKED, STATE_UNLOCKED, STATE_UNKNOWN)
|
ATTR_ENTITY_ID, STATE_LOCKED, STATE_UNLOCKED, STATE_UNKNOWN)
|
||||||
|
|
||||||
from . import TYPES
|
from . import TYPES
|
||||||
from .accessories import HomeAccessory, add_preload_service, setup_char
|
from .accessories import HomeAccessory
|
||||||
from .const import (
|
from .const import (
|
||||||
CATEGORY_LOCK, SERV_LOCK, CHAR_LOCK_CURRENT_STATE, CHAR_LOCK_TARGET_STATE)
|
SERV_LOCK, CHAR_LOCK_CURRENT_STATE, CHAR_LOCK_TARGET_STATE)
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -29,16 +31,16 @@ class Lock(HomeAccessory):
|
|||||||
|
|
||||||
def __init__(self, *args, config):
|
def __init__(self, *args, config):
|
||||||
"""Initialize a Lock accessory object."""
|
"""Initialize a Lock accessory object."""
|
||||||
super().__init__(*args, category=CATEGORY_LOCK)
|
super().__init__(*args, category=CATEGORY_DOOR_LOCK)
|
||||||
self.flag_target_state = False
|
self.flag_target_state = False
|
||||||
|
|
||||||
serv_lock_mechanism = add_preload_service(self, SERV_LOCK)
|
serv_lock_mechanism = self.add_preload_service(SERV_LOCK)
|
||||||
self.char_current_state = setup_char(
|
self.char_current_state = serv_lock_mechanism.configure_char(
|
||||||
CHAR_LOCK_CURRENT_STATE, serv_lock_mechanism,
|
CHAR_LOCK_CURRENT_STATE,
|
||||||
value=HASS_TO_HOMEKIT[STATE_UNKNOWN])
|
value=HASS_TO_HOMEKIT[STATE_UNKNOWN])
|
||||||
self.char_target_state = setup_char(
|
self.char_target_state = serv_lock_mechanism.configure_char(
|
||||||
CHAR_LOCK_TARGET_STATE, serv_lock_mechanism,
|
CHAR_LOCK_TARGET_STATE, value=HASS_TO_HOMEKIT[STATE_LOCKED],
|
||||||
value=HASS_TO_HOMEKIT[STATE_LOCKED], callback=self.set_state)
|
setter_callback=self.set_state)
|
||||||
|
|
||||||
def set_state(self, value):
|
def set_state(self, value):
|
||||||
"""Set lock state to value if call came from HomeKit."""
|
"""Set lock state to value if call came from HomeKit."""
|
||||||
|
@ -1,26 +1,31 @@
|
|||||||
"""Class to hold all alarm control panel accessories."""
|
"""Class to hold all alarm control panel accessories."""
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
from pyhap.const import CATEGORY_ALARM_SYSTEM
|
||||||
|
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME,
|
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME,
|
||||||
STATE_ALARM_ARMED_NIGHT, STATE_ALARM_DISARMED,
|
STATE_ALARM_ARMED_NIGHT, STATE_ALARM_DISARMED,
|
||||||
ATTR_ENTITY_ID, ATTR_CODE)
|
STATE_ALARM_TRIGGERED, ATTR_ENTITY_ID, ATTR_CODE)
|
||||||
|
|
||||||
from . import TYPES
|
from . import TYPES
|
||||||
from .accessories import HomeAccessory, add_preload_service, setup_char
|
from .accessories import HomeAccessory
|
||||||
from .const import (
|
from .const import (
|
||||||
CATEGORY_ALARM_SYSTEM, SERV_SECURITY_SYSTEM,
|
SERV_SECURITY_SYSTEM, CHAR_CURRENT_SECURITY_STATE,
|
||||||
CHAR_CURRENT_SECURITY_STATE, CHAR_TARGET_SECURITY_STATE)
|
CHAR_TARGET_SECURITY_STATE)
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
HASS_TO_HOMEKIT = {STATE_ALARM_DISARMED: 3, STATE_ALARM_ARMED_HOME: 0,
|
HASS_TO_HOMEKIT = {STATE_ALARM_ARMED_HOME: 0,
|
||||||
STATE_ALARM_ARMED_AWAY: 1, STATE_ALARM_ARMED_NIGHT: 2}
|
STATE_ALARM_ARMED_AWAY: 1,
|
||||||
|
STATE_ALARM_ARMED_NIGHT: 2,
|
||||||
|
STATE_ALARM_DISARMED: 3,
|
||||||
|
STATE_ALARM_TRIGGERED: 4}
|
||||||
HOMEKIT_TO_HASS = {c: s for s, c in HASS_TO_HOMEKIT.items()}
|
HOMEKIT_TO_HASS = {c: s for s, c in HASS_TO_HOMEKIT.items()}
|
||||||
STATE_TO_SERVICE = {STATE_ALARM_DISARMED: 'alarm_disarm',
|
STATE_TO_SERVICE = {STATE_ALARM_ARMED_HOME: 'alarm_arm_home',
|
||||||
STATE_ALARM_ARMED_HOME: 'alarm_arm_home',
|
|
||||||
STATE_ALARM_ARMED_AWAY: 'alarm_arm_away',
|
STATE_ALARM_ARMED_AWAY: 'alarm_arm_away',
|
||||||
STATE_ALARM_ARMED_NIGHT: 'alarm_arm_night'}
|
STATE_ALARM_ARMED_NIGHT: 'alarm_arm_night',
|
||||||
|
STATE_ALARM_DISARMED: 'alarm_disarm'}
|
||||||
|
|
||||||
|
|
||||||
@TYPES.register('SecuritySystem')
|
@TYPES.register('SecuritySystem')
|
||||||
@ -33,12 +38,12 @@ class SecuritySystem(HomeAccessory):
|
|||||||
self._alarm_code = config.get(ATTR_CODE)
|
self._alarm_code = config.get(ATTR_CODE)
|
||||||
self.flag_target_state = False
|
self.flag_target_state = False
|
||||||
|
|
||||||
serv_alarm = add_preload_service(self, SERV_SECURITY_SYSTEM)
|
serv_alarm = self.add_preload_service(SERV_SECURITY_SYSTEM)
|
||||||
self.char_current_state = setup_char(
|
self.char_current_state = serv_alarm.configure_char(
|
||||||
CHAR_CURRENT_SECURITY_STATE, serv_alarm, value=3)
|
CHAR_CURRENT_SECURITY_STATE, value=3)
|
||||||
self.char_target_state = setup_char(
|
self.char_target_state = serv_alarm.configure_char(
|
||||||
CHAR_TARGET_SECURITY_STATE, serv_alarm, value=3,
|
CHAR_TARGET_SECURITY_STATE, value=3,
|
||||||
callback=self.set_security_state)
|
setter_callback=self.set_security_state)
|
||||||
|
|
||||||
def set_security_state(self, value):
|
def set_security_state(self, value):
|
||||||
"""Move security state to value if call came from HomeKit."""
|
"""Move security state to value if call came from HomeKit."""
|
||||||
@ -62,7 +67,8 @@ class SecuritySystem(HomeAccessory):
|
|||||||
_LOGGER.debug('%s: Updated current state to %s (%d)',
|
_LOGGER.debug('%s: Updated current state to %s (%d)',
|
||||||
self.entity_id, hass_state, current_security_state)
|
self.entity_id, hass_state, current_security_state)
|
||||||
|
|
||||||
if not self.flag_target_state:
|
# SecuritySystemTargetState does not support triggered
|
||||||
|
if not self.flag_target_state and \
|
||||||
|
hass_state != STATE_ALARM_TRIGGERED:
|
||||||
self.char_target_state.set_value(current_security_state)
|
self.char_target_state.set_value(current_security_state)
|
||||||
if self.char_target_state.value == self.char_current_state.value:
|
self.flag_target_state = False
|
||||||
self.flag_target_state = False
|
|
||||||
|
@ -1,14 +1,16 @@
|
|||||||
"""Class to hold all sensor accessories."""
|
"""Class to hold all sensor accessories."""
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
from pyhap.const import CATEGORY_SENSOR
|
||||||
|
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS,
|
ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS,
|
||||||
ATTR_DEVICE_CLASS, STATE_ON, STATE_HOME)
|
ATTR_DEVICE_CLASS, STATE_ON, STATE_HOME)
|
||||||
|
|
||||||
from . import TYPES
|
from . import TYPES
|
||||||
from .accessories import HomeAccessory, add_preload_service, setup_char
|
from .accessories import HomeAccessory
|
||||||
from .const import (
|
from .const import (
|
||||||
CATEGORY_SENSOR, SERV_HUMIDITY_SENSOR, SERV_TEMPERATURE_SENSOR,
|
SERV_HUMIDITY_SENSOR, SERV_TEMPERATURE_SENSOR,
|
||||||
CHAR_CURRENT_HUMIDITY, CHAR_CURRENT_TEMPERATURE, PROP_CELSIUS,
|
CHAR_CURRENT_HUMIDITY, CHAR_CURRENT_TEMPERATURE, PROP_CELSIUS,
|
||||||
SERV_AIR_QUALITY_SENSOR, CHAR_AIR_QUALITY, CHAR_AIR_PARTICULATE_DENSITY,
|
SERV_AIR_QUALITY_SENSOR, CHAR_AIR_QUALITY, CHAR_AIR_PARTICULATE_DENSITY,
|
||||||
CHAR_CARBON_DIOXIDE_LEVEL, CHAR_CARBON_DIOXIDE_PEAK_LEVEL,
|
CHAR_CARBON_DIOXIDE_LEVEL, CHAR_CARBON_DIOXIDE_PEAK_LEVEL,
|
||||||
@ -52,10 +54,9 @@ class TemperatureSensor(HomeAccessory):
|
|||||||
def __init__(self, *args, config):
|
def __init__(self, *args, config):
|
||||||
"""Initialize a TemperatureSensor accessory object."""
|
"""Initialize a TemperatureSensor accessory object."""
|
||||||
super().__init__(*args, category=CATEGORY_SENSOR)
|
super().__init__(*args, category=CATEGORY_SENSOR)
|
||||||
serv_temp = add_preload_service(self, SERV_TEMPERATURE_SENSOR)
|
serv_temp = self.add_preload_service(SERV_TEMPERATURE_SENSOR)
|
||||||
self.char_temp = setup_char(
|
self.char_temp = serv_temp.configure_char(
|
||||||
CHAR_CURRENT_TEMPERATURE, serv_temp, value=0,
|
CHAR_CURRENT_TEMPERATURE, value=0, properties=PROP_CELSIUS)
|
||||||
properties=PROP_CELSIUS)
|
|
||||||
self.unit = None
|
self.unit = None
|
||||||
|
|
||||||
def update_state(self, new_state):
|
def update_state(self, new_state):
|
||||||
@ -76,9 +77,9 @@ class HumiditySensor(HomeAccessory):
|
|||||||
def __init__(self, *args, config):
|
def __init__(self, *args, config):
|
||||||
"""Initialize a HumiditySensor accessory object."""
|
"""Initialize a HumiditySensor accessory object."""
|
||||||
super().__init__(*args, category=CATEGORY_SENSOR)
|
super().__init__(*args, category=CATEGORY_SENSOR)
|
||||||
serv_humidity = add_preload_service(self, SERV_HUMIDITY_SENSOR)
|
serv_humidity = self.add_preload_service(SERV_HUMIDITY_SENSOR)
|
||||||
self.char_humidity = setup_char(
|
self.char_humidity = serv_humidity.configure_char(
|
||||||
CHAR_CURRENT_HUMIDITY, serv_humidity, value=0)
|
CHAR_CURRENT_HUMIDITY, value=0)
|
||||||
|
|
||||||
def update_state(self, new_state):
|
def update_state(self, new_state):
|
||||||
"""Update accessory after state change."""
|
"""Update accessory after state change."""
|
||||||
@ -97,12 +98,12 @@ class AirQualitySensor(HomeAccessory):
|
|||||||
"""Initialize a AirQualitySensor accessory object."""
|
"""Initialize a AirQualitySensor accessory object."""
|
||||||
super().__init__(*args, category=CATEGORY_SENSOR)
|
super().__init__(*args, category=CATEGORY_SENSOR)
|
||||||
|
|
||||||
serv_air_quality = add_preload_service(self, SERV_AIR_QUALITY_SENSOR,
|
serv_air_quality = self.add_preload_service(
|
||||||
[CHAR_AIR_PARTICULATE_DENSITY])
|
SERV_AIR_QUALITY_SENSOR, [CHAR_AIR_PARTICULATE_DENSITY])
|
||||||
self.char_quality = setup_char(
|
self.char_quality = serv_air_quality.configure_char(
|
||||||
CHAR_AIR_QUALITY, serv_air_quality, value=0)
|
CHAR_AIR_QUALITY, value=0)
|
||||||
self.char_density = setup_char(
|
self.char_density = serv_air_quality.configure_char(
|
||||||
CHAR_AIR_PARTICULATE_DENSITY, serv_air_quality, value=0)
|
CHAR_AIR_PARTICULATE_DENSITY, value=0)
|
||||||
|
|
||||||
def update_state(self, new_state):
|
def update_state(self, new_state):
|
||||||
"""Update accessory after state change."""
|
"""Update accessory after state change."""
|
||||||
@ -121,14 +122,14 @@ class CarbonDioxideSensor(HomeAccessory):
|
|||||||
"""Initialize a CarbonDioxideSensor accessory object."""
|
"""Initialize a CarbonDioxideSensor accessory object."""
|
||||||
super().__init__(*args, category=CATEGORY_SENSOR)
|
super().__init__(*args, category=CATEGORY_SENSOR)
|
||||||
|
|
||||||
serv_co2 = add_preload_service(self, SERV_CARBON_DIOXIDE_SENSOR, [
|
serv_co2 = self.add_preload_service(SERV_CARBON_DIOXIDE_SENSOR, [
|
||||||
CHAR_CARBON_DIOXIDE_LEVEL, CHAR_CARBON_DIOXIDE_PEAK_LEVEL])
|
CHAR_CARBON_DIOXIDE_LEVEL, CHAR_CARBON_DIOXIDE_PEAK_LEVEL])
|
||||||
self.char_co2 = setup_char(
|
self.char_co2 = serv_co2.configure_char(
|
||||||
CHAR_CARBON_DIOXIDE_LEVEL, serv_co2, value=0)
|
CHAR_CARBON_DIOXIDE_LEVEL, value=0)
|
||||||
self.char_peak = setup_char(
|
self.char_peak = serv_co2.configure_char(
|
||||||
CHAR_CARBON_DIOXIDE_PEAK_LEVEL, serv_co2, value=0)
|
CHAR_CARBON_DIOXIDE_PEAK_LEVEL, value=0)
|
||||||
self.char_detected = setup_char(
|
self.char_detected = serv_co2.configure_char(
|
||||||
CHAR_CARBON_DIOXIDE_DETECTED, serv_co2, value=0)
|
CHAR_CARBON_DIOXIDE_DETECTED, value=0)
|
||||||
|
|
||||||
def update_state(self, new_state):
|
def update_state(self, new_state):
|
||||||
"""Update accessory after state change."""
|
"""Update accessory after state change."""
|
||||||
@ -149,9 +150,9 @@ class LightSensor(HomeAccessory):
|
|||||||
"""Initialize a LightSensor accessory object."""
|
"""Initialize a LightSensor accessory object."""
|
||||||
super().__init__(*args, category=CATEGORY_SENSOR)
|
super().__init__(*args, category=CATEGORY_SENSOR)
|
||||||
|
|
||||||
serv_light = add_preload_service(self, SERV_LIGHT_SENSOR)
|
serv_light = self.add_preload_service(SERV_LIGHT_SENSOR)
|
||||||
self.char_light = setup_char(
|
self.char_light = serv_light.configure_char(
|
||||||
CHAR_CURRENT_AMBIENT_LIGHT_LEVEL, serv_light, value=0)
|
CHAR_CURRENT_AMBIENT_LIGHT_LEVEL, value=0)
|
||||||
|
|
||||||
def update_state(self, new_state):
|
def update_state(self, new_state):
|
||||||
"""Update accessory after state change."""
|
"""Update accessory after state change."""
|
||||||
@ -174,8 +175,8 @@ class BinarySensor(HomeAccessory):
|
|||||||
if device_class in BINARY_SENSOR_SERVICE_MAP \
|
if device_class in BINARY_SENSOR_SERVICE_MAP \
|
||||||
else BINARY_SENSOR_SERVICE_MAP[DEVICE_CLASS_OCCUPANCY]
|
else BINARY_SENSOR_SERVICE_MAP[DEVICE_CLASS_OCCUPANCY]
|
||||||
|
|
||||||
service = add_preload_service(self, service_char[0])
|
service = self.add_preload_service(service_char[0])
|
||||||
self.char_detected = setup_char(service_char[1], service, value=0)
|
self.char_detected = service.configure_char(service_char[1], value=0)
|
||||||
|
|
||||||
def update_state(self, new_state):
|
def update_state(self, new_state):
|
||||||
"""Update accessory after state change."""
|
"""Update accessory after state change."""
|
||||||
|
@ -1,13 +1,15 @@
|
|||||||
"""Class to hold all switch accessories."""
|
"""Class to hold all switch accessories."""
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
from pyhap.const import CATEGORY_SWITCH
|
||||||
|
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF, STATE_ON)
|
ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF, STATE_ON)
|
||||||
from homeassistant.core import split_entity_id
|
from homeassistant.core import split_entity_id
|
||||||
|
|
||||||
from . import TYPES
|
from . import TYPES
|
||||||
from .accessories import HomeAccessory, add_preload_service, setup_char
|
from .accessories import HomeAccessory
|
||||||
from .const import CATEGORY_SWITCH, SERV_SWITCH, CHAR_ON
|
from .const import SERV_SWITCH, CHAR_ON
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -22,9 +24,9 @@ class Switch(HomeAccessory):
|
|||||||
self._domain = split_entity_id(self.entity_id)[0]
|
self._domain = split_entity_id(self.entity_id)[0]
|
||||||
self.flag_target_state = False
|
self.flag_target_state = False
|
||||||
|
|
||||||
serv_switch = add_preload_service(self, SERV_SWITCH)
|
serv_switch = self.add_preload_service(SERV_SWITCH)
|
||||||
self.char_on = setup_char(
|
self.char_on = serv_switch.configure_char(
|
||||||
CHAR_ON, serv_switch, value=False, callback=self.set_state)
|
CHAR_ON, value=False, setter_callback=self.set_state)
|
||||||
|
|
||||||
def set_state(self, value):
|
def set_state(self, value):
|
||||||
"""Move switch state to value if call came from HomeKit."""
|
"""Move switch state to value if call came from HomeKit."""
|
||||||
|
@ -1,21 +1,22 @@
|
|||||||
"""Class to hold all thermostat accessories."""
|
"""Class to hold all thermostat accessories."""
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
from pyhap.const import CATEGORY_THERMOSTAT
|
||||||
|
|
||||||
from homeassistant.components.climate import (
|
from homeassistant.components.climate import (
|
||||||
ATTR_CURRENT_TEMPERATURE, ATTR_TEMPERATURE,
|
ATTR_CURRENT_TEMPERATURE, ATTR_TEMPERATURE,
|
||||||
ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW,
|
ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW,
|
||||||
ATTR_OPERATION_MODE, ATTR_OPERATION_LIST,
|
ATTR_OPERATION_MODE, ATTR_OPERATION_LIST,
|
||||||
STATE_HEAT, STATE_COOL, STATE_AUTO,
|
STATE_HEAT, STATE_COOL, STATE_AUTO, SUPPORT_ON_OFF,
|
||||||
SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_TARGET_TEMPERATURE_LOW)
|
SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_TARGET_TEMPERATURE_LOW)
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT,
|
ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT,
|
||||||
STATE_OFF, TEMP_CELSIUS, TEMP_FAHRENHEIT)
|
STATE_OFF, TEMP_CELSIUS, TEMP_FAHRENHEIT)
|
||||||
|
|
||||||
from . import TYPES
|
from . import TYPES
|
||||||
from .accessories import (
|
from .accessories import HomeAccessory, debounce
|
||||||
HomeAccessory, add_preload_service, debounce, setup_char)
|
|
||||||
from .const import (
|
from .const import (
|
||||||
CATEGORY_THERMOSTAT, SERV_THERMOSTAT, CHAR_CURRENT_HEATING_COOLING,
|
SERV_THERMOSTAT, CHAR_CURRENT_HEATING_COOLING,
|
||||||
CHAR_TARGET_HEATING_COOLING, CHAR_CURRENT_TEMPERATURE,
|
CHAR_TARGET_HEATING_COOLING, CHAR_CURRENT_TEMPERATURE,
|
||||||
CHAR_TARGET_TEMPERATURE, CHAR_TEMP_DISPLAY_UNITS,
|
CHAR_TARGET_TEMPERATURE, CHAR_TEMP_DISPLAY_UNITS,
|
||||||
CHAR_COOLING_THRESHOLD_TEMPERATURE, CHAR_HEATING_THRESHOLD_TEMPERATURE)
|
CHAR_COOLING_THRESHOLD_TEMPERATURE, CHAR_HEATING_THRESHOLD_TEMPERATURE)
|
||||||
@ -41,6 +42,7 @@ class Thermostat(HomeAccessory):
|
|||||||
"""Initialize a Thermostat accessory object."""
|
"""Initialize a Thermostat accessory object."""
|
||||||
super().__init__(*args, category=CATEGORY_THERMOSTAT)
|
super().__init__(*args, category=CATEGORY_THERMOSTAT)
|
||||||
self._unit = TEMP_CELSIUS
|
self._unit = TEMP_CELSIUS
|
||||||
|
self.support_power_state = False
|
||||||
self.heat_cool_flag_target_state = False
|
self.heat_cool_flag_target_state = False
|
||||||
self.temperature_flag_target_state = False
|
self.temperature_flag_target_state = False
|
||||||
self.coolingthresh_flag_target_state = False
|
self.coolingthresh_flag_target_state = False
|
||||||
@ -50,42 +52,43 @@ class Thermostat(HomeAccessory):
|
|||||||
self.chars = []
|
self.chars = []
|
||||||
features = self.hass.states.get(self.entity_id) \
|
features = self.hass.states.get(self.entity_id) \
|
||||||
.attributes.get(ATTR_SUPPORTED_FEATURES)
|
.attributes.get(ATTR_SUPPORTED_FEATURES)
|
||||||
|
if features & SUPPORT_ON_OFF:
|
||||||
|
self.support_power_state = True
|
||||||
if features & SUPPORT_TEMP_RANGE:
|
if features & SUPPORT_TEMP_RANGE:
|
||||||
self.chars.extend((CHAR_COOLING_THRESHOLD_TEMPERATURE,
|
self.chars.extend((CHAR_COOLING_THRESHOLD_TEMPERATURE,
|
||||||
CHAR_HEATING_THRESHOLD_TEMPERATURE))
|
CHAR_HEATING_THRESHOLD_TEMPERATURE))
|
||||||
|
|
||||||
serv_thermostat = add_preload_service(
|
serv_thermostat = self.add_preload_service(SERV_THERMOSTAT, self.chars)
|
||||||
self, SERV_THERMOSTAT, self.chars)
|
|
||||||
|
|
||||||
# Current and target mode characteristics
|
# Current and target mode characteristics
|
||||||
self.char_current_heat_cool = setup_char(
|
self.char_current_heat_cool = serv_thermostat.configure_char(
|
||||||
CHAR_CURRENT_HEATING_COOLING, serv_thermostat, value=0)
|
CHAR_CURRENT_HEATING_COOLING, value=0)
|
||||||
self.char_target_heat_cool = setup_char(
|
self.char_target_heat_cool = serv_thermostat.configure_char(
|
||||||
CHAR_TARGET_HEATING_COOLING, serv_thermostat, value=0,
|
CHAR_TARGET_HEATING_COOLING, value=0,
|
||||||
callback=self.set_heat_cool)
|
setter_callback=self.set_heat_cool)
|
||||||
|
|
||||||
# Current and target temperature characteristics
|
# Current and target temperature characteristics
|
||||||
self.char_current_temp = setup_char(
|
self.char_current_temp = serv_thermostat.configure_char(
|
||||||
CHAR_CURRENT_TEMPERATURE, serv_thermostat, value=21.0)
|
CHAR_CURRENT_TEMPERATURE, value=21.0)
|
||||||
self.char_target_temp = setup_char(
|
self.char_target_temp = serv_thermostat.configure_char(
|
||||||
CHAR_TARGET_TEMPERATURE, serv_thermostat, value=21.0,
|
CHAR_TARGET_TEMPERATURE, value=21.0,
|
||||||
callback=self.set_target_temperature)
|
setter_callback=self.set_target_temperature)
|
||||||
|
|
||||||
# Display units characteristic
|
# Display units characteristic
|
||||||
self.char_display_units = setup_char(
|
self.char_display_units = serv_thermostat.configure_char(
|
||||||
CHAR_TEMP_DISPLAY_UNITS, serv_thermostat, value=0)
|
CHAR_TEMP_DISPLAY_UNITS, value=0)
|
||||||
|
|
||||||
# If the device supports it: high and low temperature characteristics
|
# If the device supports it: high and low temperature characteristics
|
||||||
self.char_cooling_thresh_temp = None
|
self.char_cooling_thresh_temp = None
|
||||||
self.char_heating_thresh_temp = None
|
self.char_heating_thresh_temp = None
|
||||||
if CHAR_COOLING_THRESHOLD_TEMPERATURE in self.chars:
|
if CHAR_COOLING_THRESHOLD_TEMPERATURE in self.chars:
|
||||||
self.char_cooling_thresh_temp = setup_char(
|
self.char_cooling_thresh_temp = serv_thermostat.configure_char(
|
||||||
CHAR_COOLING_THRESHOLD_TEMPERATURE, serv_thermostat,
|
CHAR_COOLING_THRESHOLD_TEMPERATURE, value=23.0,
|
||||||
value=23.0, callback=self.set_cooling_threshold)
|
setter_callback=self.set_cooling_threshold)
|
||||||
if CHAR_HEATING_THRESHOLD_TEMPERATURE in self.chars:
|
if CHAR_HEATING_THRESHOLD_TEMPERATURE in self.chars:
|
||||||
self.char_heating_thresh_temp = setup_char(
|
self.char_heating_thresh_temp = serv_thermostat.configure_char(
|
||||||
CHAR_HEATING_THRESHOLD_TEMPERATURE, serv_thermostat,
|
CHAR_HEATING_THRESHOLD_TEMPERATURE, value=19.0,
|
||||||
value=19.0, callback=self.set_heating_threshold)
|
setter_callback=self.set_heating_threshold)
|
||||||
|
|
||||||
def set_heat_cool(self, value):
|
def set_heat_cool(self, value):
|
||||||
"""Move operation mode to value if call came from HomeKit."""
|
"""Move operation mode to value if call came from HomeKit."""
|
||||||
@ -93,6 +96,13 @@ class Thermostat(HomeAccessory):
|
|||||||
_LOGGER.debug('%s: Set heat-cool to %d', self.entity_id, value)
|
_LOGGER.debug('%s: Set heat-cool to %d', self.entity_id, value)
|
||||||
self.heat_cool_flag_target_state = True
|
self.heat_cool_flag_target_state = True
|
||||||
hass_value = HC_HOMEKIT_TO_HASS[value]
|
hass_value = HC_HOMEKIT_TO_HASS[value]
|
||||||
|
if self.support_power_state is True:
|
||||||
|
params = {ATTR_ENTITY_ID: self.entity_id}
|
||||||
|
if hass_value == STATE_OFF:
|
||||||
|
self.hass.services.call('climate', 'turn_off', params)
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
self.hass.services.call('climate', 'turn_on', params)
|
||||||
self.hass.components.climate.set_operation_mode(
|
self.hass.components.climate.set_operation_mode(
|
||||||
operation_mode=hass_value, entity_id=self.entity_id)
|
operation_mode=hass_value, entity_id=self.entity_id)
|
||||||
|
|
||||||
@ -178,15 +188,19 @@ class Thermostat(HomeAccessory):
|
|||||||
|
|
||||||
# Update target operation mode
|
# Update target operation mode
|
||||||
operation_mode = new_state.attributes.get(ATTR_OPERATION_MODE)
|
operation_mode = new_state.attributes.get(ATTR_OPERATION_MODE)
|
||||||
if operation_mode \
|
if self.support_power_state is True and new_state.state == STATE_OFF:
|
||||||
and operation_mode in HC_HASS_TO_HOMEKIT:
|
self.char_target_heat_cool.set_value(
|
||||||
|
HC_HASS_TO_HOMEKIT[STATE_OFF])
|
||||||
|
elif operation_mode and operation_mode in HC_HASS_TO_HOMEKIT:
|
||||||
if not self.heat_cool_flag_target_state:
|
if not self.heat_cool_flag_target_state:
|
||||||
self.char_target_heat_cool.set_value(
|
self.char_target_heat_cool.set_value(
|
||||||
HC_HASS_TO_HOMEKIT[operation_mode])
|
HC_HASS_TO_HOMEKIT[operation_mode])
|
||||||
self.heat_cool_flag_target_state = False
|
self.heat_cool_flag_target_state = False
|
||||||
|
|
||||||
# Set current operation mode based on temperatures and target mode
|
# Set current operation mode based on temperatures and target mode
|
||||||
if operation_mode == STATE_HEAT:
|
if self.support_power_state is True and new_state.state == STATE_OFF:
|
||||||
|
current_operation_mode = STATE_OFF
|
||||||
|
elif operation_mode == STATE_HEAT:
|
||||||
if isinstance(target_temp, float) and current_temp < target_temp:
|
if isinstance(target_temp, float) and current_temp < target_temp:
|
||||||
current_operation_mode = STATE_HEAT
|
current_operation_mode = STATE_HEAT
|
||||||
else:
|
else:
|
||||||
|
@ -20,7 +20,7 @@ from homeassistant.helpers.entity import Entity
|
|||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
from homeassistant.loader import bind_hass
|
from homeassistant.loader import bind_hass
|
||||||
|
|
||||||
REQUIREMENTS = ['pyhomematic==0.1.41']
|
REQUIREMENTS = ['pyhomematic==0.1.42']
|
||||||
DOMAIN = 'homematic'
|
DOMAIN = 'homematic'
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -5,143 +5,239 @@ For more details about this component, please refer to the documentation at
|
|||||||
https://home-assistant.io/components/homematicip_cloud/
|
https://home-assistant.io/components/homematicip_cloud/
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
from socket import timeout
|
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.core import callback
|
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
from homeassistant.helpers.dispatcher import (dispatcher_send,
|
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
||||||
async_dispatcher_connect)
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
from homeassistant.helpers.discovery import load_platform
|
from homeassistant.helpers.discovery import async_load_platform
|
||||||
from homeassistant.helpers.entity import Entity
|
from homeassistant.helpers.entity import Entity
|
||||||
|
from homeassistant.core import callback
|
||||||
|
|
||||||
REQUIREMENTS = ['homematicip==0.8']
|
REQUIREMENTS = ['homematicip==0.9.2.4']
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
DOMAIN = 'homematicip_cloud'
|
DOMAIN = 'homematicip_cloud'
|
||||||
|
|
||||||
|
COMPONENTS = [
|
||||||
|
'sensor'
|
||||||
|
]
|
||||||
|
|
||||||
CONF_NAME = 'name'
|
CONF_NAME = 'name'
|
||||||
CONF_ACCESSPOINT = 'accesspoint'
|
CONF_ACCESSPOINT = 'accesspoint'
|
||||||
CONF_AUTHTOKEN = 'authtoken'
|
CONF_AUTHTOKEN = 'authtoken'
|
||||||
|
|
||||||
CONFIG_SCHEMA = vol.Schema({
|
CONFIG_SCHEMA = vol.Schema({
|
||||||
vol.Optional(DOMAIN): [vol.Schema({
|
vol.Optional(DOMAIN, default=[]): vol.All(cv.ensure_list, [vol.Schema({
|
||||||
vol.Optional(CONF_NAME, default=''): cv.string,
|
vol.Optional(CONF_NAME): vol.Any(cv.string),
|
||||||
vol.Required(CONF_ACCESSPOINT): cv.string,
|
vol.Required(CONF_ACCESSPOINT): cv.string,
|
||||||
vol.Required(CONF_AUTHTOKEN): cv.string,
|
vol.Required(CONF_AUTHTOKEN): cv.string,
|
||||||
})],
|
})]),
|
||||||
}, extra=vol.ALLOW_EXTRA)
|
}, extra=vol.ALLOW_EXTRA)
|
||||||
|
|
||||||
EVENT_HOME_CHANGED = 'homematicip_home_changed'
|
HMIP_ACCESS_POINT = 'Access Point'
|
||||||
EVENT_DEVICE_CHANGED = 'homematicip_device_changed'
|
HMIP_HUB = 'HmIP-HUB'
|
||||||
EVENT_GROUP_CHANGED = 'homematicip_group_changed'
|
|
||||||
EVENT_SECURITY_CHANGED = 'homematicip_security_changed'
|
|
||||||
EVENT_JOURNAL_CHANGED = 'homematicip_journal_changed'
|
|
||||||
|
|
||||||
ATTR_HOME_ID = 'home_id'
|
ATTR_HOME_ID = 'home_id'
|
||||||
ATTR_HOME_LABEL = 'home_label'
|
ATTR_HOME_NAME = 'home_name'
|
||||||
ATTR_DEVICE_ID = 'device_id'
|
ATTR_DEVICE_ID = 'device_id'
|
||||||
ATTR_DEVICE_LABEL = 'device_label'
|
ATTR_DEVICE_LABEL = 'device_label'
|
||||||
ATTR_STATUS_UPDATE = 'status_update'
|
ATTR_STATUS_UPDATE = 'status_update'
|
||||||
ATTR_FIRMWARE_STATE = 'firmware_state'
|
ATTR_FIRMWARE_STATE = 'firmware_state'
|
||||||
|
ATTR_UNREACHABLE = 'unreachable'
|
||||||
ATTR_LOW_BATTERY = 'low_battery'
|
ATTR_LOW_BATTERY = 'low_battery'
|
||||||
|
ATTR_MODEL_TYPE = 'model_type'
|
||||||
|
ATTR_GROUP_TYPE = 'group_type'
|
||||||
|
ATTR_DEVICE_RSSI = 'device_rssi'
|
||||||
|
ATTR_DUTY_CYCLE = 'duty_cycle'
|
||||||
|
ATTR_CONNECTED = 'connected'
|
||||||
ATTR_SABOTAGE = 'sabotage'
|
ATTR_SABOTAGE = 'sabotage'
|
||||||
ATTR_RSSI = 'rssi'
|
ATTR_OPERATION_LOCK = 'operation_lock'
|
||||||
ATTR_TYPE = 'type'
|
|
||||||
|
|
||||||
|
|
||||||
def setup(hass, config):
|
async def async_setup(hass, config):
|
||||||
"""Set up the HomematicIP component."""
|
"""Set up the HomematicIP component."""
|
||||||
# pylint: disable=import-error, no-name-in-module
|
from homematicip.base.base_connection import HmipConnectionError
|
||||||
from homematicip.home import Home
|
|
||||||
|
|
||||||
hass.data.setdefault(DOMAIN, {})
|
hass.data.setdefault(DOMAIN, {})
|
||||||
homes = hass.data[DOMAIN]
|
|
||||||
accesspoints = config.get(DOMAIN, [])
|
accesspoints = config.get(DOMAIN, [])
|
||||||
|
for conf in accesspoints:
|
||||||
def _update_event(events):
|
_websession = async_get_clientsession(hass)
|
||||||
"""Handle incoming HomeMaticIP events."""
|
_hmip = HomematicipConnector(hass, conf, _websession)
|
||||||
for event in events:
|
|
||||||
etype = event['eventType']
|
|
||||||
edata = event['data']
|
|
||||||
if etype == 'DEVICE_CHANGED':
|
|
||||||
dispatcher_send(hass, EVENT_DEVICE_CHANGED, edata.id)
|
|
||||||
elif etype == 'GROUP_CHANGED':
|
|
||||||
dispatcher_send(hass, EVENT_GROUP_CHANGED, edata.id)
|
|
||||||
elif etype == 'HOME_CHANGED':
|
|
||||||
dispatcher_send(hass, EVENT_HOME_CHANGED, edata.id)
|
|
||||||
elif etype == 'JOURNAL_CHANGED':
|
|
||||||
dispatcher_send(hass, EVENT_SECURITY_CHANGED, edata.id)
|
|
||||||
return True
|
|
||||||
|
|
||||||
for device in accesspoints:
|
|
||||||
name = device.get(CONF_NAME)
|
|
||||||
accesspoint = device.get(CONF_ACCESSPOINT)
|
|
||||||
authtoken = device.get(CONF_AUTHTOKEN)
|
|
||||||
|
|
||||||
home = Home()
|
|
||||||
if name.lower() == 'none':
|
|
||||||
name = ''
|
|
||||||
home.label = name
|
|
||||||
try:
|
try:
|
||||||
home.set_auth_token(authtoken)
|
await _hmip.init()
|
||||||
home.init(accesspoint)
|
except HmipConnectionError:
|
||||||
if home.get_current_state():
|
_LOGGER.error('Failed to connect to the HomematicIP server, %s.',
|
||||||
_LOGGER.info("Connection to HMIP established")
|
conf.get(CONF_ACCESSPOINT))
|
||||||
else:
|
|
||||||
_LOGGER.warning("Connection to HMIP could not be established")
|
|
||||||
return False
|
|
||||||
except timeout:
|
|
||||||
_LOGGER.warning("Connection to HMIP could not be established")
|
|
||||||
return False
|
return False
|
||||||
homes[home.id] = home
|
|
||||||
home.onEvent += _update_event
|
|
||||||
home.enable_events()
|
|
||||||
_LOGGER.info('HUB name: %s, id: %s', home.label, home.id)
|
|
||||||
|
|
||||||
for component in ['sensor']:
|
home = _hmip.home
|
||||||
load_platform(hass, component, DOMAIN, {'homeid': home.id}, config)
|
home.name = conf.get(CONF_NAME)
|
||||||
|
home.label = HMIP_ACCESS_POINT
|
||||||
|
home.modelType = HMIP_HUB
|
||||||
|
|
||||||
|
hass.data[DOMAIN][home.id] = home
|
||||||
|
_LOGGER.info('Connected to the HomematicIP server, %s.',
|
||||||
|
conf.get(CONF_ACCESSPOINT))
|
||||||
|
homeid = {ATTR_HOME_ID: home.id}
|
||||||
|
for component in COMPONENTS:
|
||||||
|
hass.async_add_job(async_load_platform(hass, component, DOMAIN,
|
||||||
|
homeid, config))
|
||||||
|
|
||||||
|
hass.loop.create_task(_hmip.connect())
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class HomematicipConnector:
|
||||||
|
"""Manages HomematicIP http and websocket connection."""
|
||||||
|
|
||||||
|
def __init__(self, hass, config, websession):
|
||||||
|
"""Initialize HomematicIP cloud connection."""
|
||||||
|
from homematicip.async.home import AsyncHome
|
||||||
|
|
||||||
|
self._hass = hass
|
||||||
|
self._ws_close_requested = False
|
||||||
|
self._retry_task = None
|
||||||
|
self._tries = 0
|
||||||
|
self._accesspoint = config.get(CONF_ACCESSPOINT)
|
||||||
|
_authtoken = config.get(CONF_AUTHTOKEN)
|
||||||
|
|
||||||
|
self.home = AsyncHome(hass.loop, websession)
|
||||||
|
self.home.set_auth_token(_authtoken)
|
||||||
|
|
||||||
|
self.home.on_update(self.async_update)
|
||||||
|
self._accesspoint_connected = True
|
||||||
|
|
||||||
|
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.close())
|
||||||
|
|
||||||
|
async def init(self):
|
||||||
|
"""Initialize connection."""
|
||||||
|
await self.home.init(self._accesspoint)
|
||||||
|
await self.home.get_current_state()
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_update(self, *args, **kwargs):
|
||||||
|
"""Async update the home device.
|
||||||
|
|
||||||
|
Triggered when the hmip HOME_CHANGED event has fired.
|
||||||
|
There are several occasions for this event to happen.
|
||||||
|
We are only interested to check whether the access point
|
||||||
|
is still connected. If not, device state changes cannot
|
||||||
|
be forwarded to hass. So if access point is disconnected all devices
|
||||||
|
are set to unavailable.
|
||||||
|
"""
|
||||||
|
if not self.home.connected:
|
||||||
|
_LOGGER.error(
|
||||||
|
"HMIP access point has lost connection with the cloud")
|
||||||
|
self._accesspoint_connected = False
|
||||||
|
self.set_all_to_unavailable()
|
||||||
|
elif not self._accesspoint_connected:
|
||||||
|
# Explicitly getting an update as device states might have
|
||||||
|
# changed during access point disconnect."""
|
||||||
|
|
||||||
|
job = self._hass.async_add_job(self.get_state())
|
||||||
|
job.add_done_callback(self.get_state_finished)
|
||||||
|
|
||||||
|
async def get_state(self):
|
||||||
|
"""Update hmip state and tell hass."""
|
||||||
|
await self.home.get_current_state()
|
||||||
|
self.update_all()
|
||||||
|
|
||||||
|
def get_state_finished(self, future):
|
||||||
|
"""Execute when get_state coroutine has finished."""
|
||||||
|
from homematicip.base.base_connection import HmipConnectionError
|
||||||
|
|
||||||
|
try:
|
||||||
|
future.result()
|
||||||
|
except HmipConnectionError:
|
||||||
|
# Somehow connection could not recover. Will disconnect and
|
||||||
|
# so reconnect loop is taking over.
|
||||||
|
_LOGGER.error(
|
||||||
|
"updating state after himp access point reconnect failed.")
|
||||||
|
self._hass.async_add_job(self.home.disable_events())
|
||||||
|
|
||||||
|
def set_all_to_unavailable(self):
|
||||||
|
"""Set all devices to unavailable and tell Hass."""
|
||||||
|
for device in self.home.devices:
|
||||||
|
device.unreach = True
|
||||||
|
self.update_all()
|
||||||
|
|
||||||
|
def update_all(self):
|
||||||
|
"""Signal all devices to update their state."""
|
||||||
|
for device in self.home.devices:
|
||||||
|
device.fire_update_event()
|
||||||
|
|
||||||
|
async def _handle_connection(self):
|
||||||
|
"""Handle websocket connection."""
|
||||||
|
from homematicip.base.base_connection import HmipConnectionError
|
||||||
|
|
||||||
|
await self.home.get_current_state()
|
||||||
|
hmip_events = await self.home.enable_events()
|
||||||
|
try:
|
||||||
|
await hmip_events
|
||||||
|
except HmipConnectionError:
|
||||||
|
return
|
||||||
|
|
||||||
|
async def connect(self):
|
||||||
|
"""Start websocket connection."""
|
||||||
|
self._tries = 0
|
||||||
|
while True:
|
||||||
|
await self._handle_connection()
|
||||||
|
if self._ws_close_requested:
|
||||||
|
break
|
||||||
|
self._ws_close_requested = False
|
||||||
|
self._tries += 1
|
||||||
|
try:
|
||||||
|
self._retry_task = self._hass.async_add_job(asyncio.sleep(
|
||||||
|
2 ** min(9, self._tries), loop=self._hass.loop))
|
||||||
|
await self._retry_task
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
break
|
||||||
|
_LOGGER.info('Reconnect (%s) to the HomematicIP cloud server.',
|
||||||
|
self._tries)
|
||||||
|
|
||||||
|
async def close(self):
|
||||||
|
"""Close the websocket connection."""
|
||||||
|
self._ws_close_requested = True
|
||||||
|
if self._retry_task is not None:
|
||||||
|
self._retry_task.cancel()
|
||||||
|
await self.home.disable_events()
|
||||||
|
_LOGGER.info("Closed connection to HomematicIP cloud server.")
|
||||||
|
|
||||||
|
|
||||||
class HomematicipGenericDevice(Entity):
|
class HomematicipGenericDevice(Entity):
|
||||||
"""Representation of an HomematicIP generic device."""
|
"""Representation of an HomematicIP generic device."""
|
||||||
|
|
||||||
def __init__(self, home, device):
|
def __init__(self, home, device, post=None):
|
||||||
"""Initialize the generic device."""
|
"""Initialize the generic device."""
|
||||||
self._home = home
|
self._home = home
|
||||||
self._device = device
|
self._device = device
|
||||||
|
self.post = post
|
||||||
|
_LOGGER.info('Setting up %s (%s)', self.name,
|
||||||
|
self._device.modelType)
|
||||||
|
|
||||||
async def async_added_to_hass(self):
|
async def async_added_to_hass(self):
|
||||||
"""Register callbacks."""
|
"""Register callbacks."""
|
||||||
async_dispatcher_connect(
|
self._device.on_update(self._device_changed)
|
||||||
self.hass, EVENT_DEVICE_CHANGED, self._device_changed)
|
|
||||||
|
|
||||||
@callback
|
def _device_changed(self, json, **kwargs):
|
||||||
def _device_changed(self, deviceid):
|
|
||||||
"""Handle device state changes."""
|
"""Handle device state changes."""
|
||||||
if deviceid is None or deviceid == self._device.id:
|
_LOGGER.debug('Event %s (%s)', self.name, self._device.modelType)
|
||||||
_LOGGER.debug('Event device %s', self._device.label)
|
self.async_schedule_update_ha_state()
|
||||||
self.async_schedule_update_ha_state()
|
|
||||||
|
|
||||||
def _name(self, addon=''):
|
|
||||||
"""Return the name of the device."""
|
|
||||||
name = ''
|
|
||||||
if self._home.label != '':
|
|
||||||
name += self._home.label + ' '
|
|
||||||
name += self._device.label
|
|
||||||
if addon != '':
|
|
||||||
name += ' ' + addon
|
|
||||||
return name
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
"""Return the name of the generic device."""
|
"""Return the name of the generic device."""
|
||||||
return self._name()
|
name = self._device.label
|
||||||
|
if self._home.name is not None:
|
||||||
|
name = "{} {}".format(self._home.name, name)
|
||||||
|
if self.post is not None:
|
||||||
|
name = "{} {}".format(name, self.post)
|
||||||
|
return name
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def should_poll(self):
|
def should_poll(self):
|
||||||
@ -153,24 +249,10 @@ class HomematicipGenericDevice(Entity):
|
|||||||
"""Device available."""
|
"""Device available."""
|
||||||
return not self._device.unreach
|
return not self._device.unreach
|
||||||
|
|
||||||
def _generic_state_attributes(self):
|
|
||||||
"""Return the state attributes of the generic device."""
|
|
||||||
laststatus = ''
|
|
||||||
if self._device.lastStatusUpdate is not None:
|
|
||||||
laststatus = self._device.lastStatusUpdate.isoformat()
|
|
||||||
return {
|
|
||||||
ATTR_HOME_LABEL: self._home.label,
|
|
||||||
ATTR_DEVICE_LABEL: self._device.label,
|
|
||||||
ATTR_HOME_ID: self._device.homeId,
|
|
||||||
ATTR_DEVICE_ID: self._device.id.lower(),
|
|
||||||
ATTR_STATUS_UPDATE: laststatus,
|
|
||||||
ATTR_FIRMWARE_STATE: self._device.updateState.lower(),
|
|
||||||
ATTR_LOW_BATTERY: self._device.lowBat,
|
|
||||||
ATTR_RSSI: self._device.rssiDeviceValue,
|
|
||||||
ATTR_TYPE: self._device.modelType
|
|
||||||
}
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def device_state_attributes(self):
|
def device_state_attributes(self):
|
||||||
"""Return the state attributes of the generic device."""
|
"""Return the state attributes of the generic device."""
|
||||||
return self._generic_state_attributes()
|
return {
|
||||||
|
ATTR_LOW_BATTERY: self._device.lowBat,
|
||||||
|
ATTR_MODEL_TYPE: self._device.modelType
|
||||||
|
}
|
||||||
|
@ -32,17 +32,19 @@ def setup_auth(app, trusted_networks, api_password):
|
|||||||
|
|
||||||
if (HTTP_HEADER_HA_AUTH in request.headers and
|
if (HTTP_HEADER_HA_AUTH in request.headers and
|
||||||
hmac.compare_digest(
|
hmac.compare_digest(
|
||||||
api_password, request.headers[HTTP_HEADER_HA_AUTH])):
|
api_password.encode('utf-8'),
|
||||||
|
request.headers[HTTP_HEADER_HA_AUTH].encode('utf-8'))):
|
||||||
# A valid auth header has been set
|
# A valid auth header has been set
|
||||||
authenticated = True
|
authenticated = True
|
||||||
|
|
||||||
elif (DATA_API_PASSWORD in request.query and
|
elif (DATA_API_PASSWORD in request.query and
|
||||||
hmac.compare_digest(api_password,
|
hmac.compare_digest(
|
||||||
request.query[DATA_API_PASSWORD])):
|
api_password.encode('utf-8'),
|
||||||
|
request.query[DATA_API_PASSWORD].encode('utf-8'))):
|
||||||
authenticated = True
|
authenticated = True
|
||||||
|
|
||||||
elif (hdrs.AUTHORIZATION in request.headers and
|
elif (hdrs.AUTHORIZATION in request.headers and
|
||||||
validate_authorization_header(api_password, request)):
|
await async_validate_auth_header(api_password, request)):
|
||||||
authenticated = True
|
authenticated = True
|
||||||
|
|
||||||
elif _is_trusted_ip(request, trusted_networks):
|
elif _is_trusted_ip(request, trusted_networks):
|
||||||
@ -70,23 +72,38 @@ def _is_trusted_ip(request, trusted_networks):
|
|||||||
def validate_password(request, api_password):
|
def validate_password(request, api_password):
|
||||||
"""Test if password is valid."""
|
"""Test if password is valid."""
|
||||||
return hmac.compare_digest(
|
return hmac.compare_digest(
|
||||||
api_password, request.app['hass'].http.api_password)
|
api_password.encode('utf-8'),
|
||||||
|
request.app['hass'].http.api_password.encode('utf-8'))
|
||||||
|
|
||||||
|
|
||||||
def validate_authorization_header(api_password, request):
|
async def async_validate_auth_header(api_password, request):
|
||||||
"""Test an authorization header if valid password."""
|
"""Test an authorization header if valid password."""
|
||||||
if hdrs.AUTHORIZATION not in request.headers:
|
if hdrs.AUTHORIZATION not in request.headers:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
auth_type, auth = request.headers.get(hdrs.AUTHORIZATION).split(' ', 1)
|
auth_type, auth_val = request.headers.get(hdrs.AUTHORIZATION).split(' ', 1)
|
||||||
|
|
||||||
if auth_type != 'Basic':
|
if auth_type == 'Basic':
|
||||||
|
decoded = base64.b64decode(auth_val).decode('utf-8')
|
||||||
|
try:
|
||||||
|
username, password = decoded.split(':', 1)
|
||||||
|
except ValueError:
|
||||||
|
# If no ':' in decoded
|
||||||
|
return False
|
||||||
|
|
||||||
|
if username != 'homeassistant':
|
||||||
|
return False
|
||||||
|
|
||||||
|
return hmac.compare_digest(api_password.encode('utf-8'),
|
||||||
|
password.encode('utf-8'))
|
||||||
|
|
||||||
|
if auth_type != 'Bearer':
|
||||||
return False
|
return False
|
||||||
|
|
||||||
decoded = base64.b64decode(auth).decode('utf-8')
|
hass = request.app['hass']
|
||||||
username, password = decoded.split(':', 1)
|
access_token = hass.auth.async_get_access_token(auth_val)
|
||||||
|
if access_token is None:
|
||||||
if username != 'homeassistant':
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return hmac.compare_digest(api_password, password)
|
request['hass_user'] = access_token.refresh_token.user
|
||||||
|
return True
|
||||||
|
29
homeassistant/components/hue/.translations/bg.json
Normal file
29
homeassistant/components/hue/.translations/bg.json
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"abort": {
|
||||||
|
"all_configured": "\u0412\u0441\u0438\u0447\u043a\u0438 \u0431\u0430\u0437\u043e\u0432\u0438 \u0441\u0442\u0430\u043d\u0446\u0438\u0438 Philips Hue \u0441\u0430 \u0432\u0435\u0447\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0438",
|
||||||
|
"already_configured": "\u0411\u0430\u0437\u043e\u0432\u0430\u0442\u0430 \u0441\u0442\u0430\u043d\u0446\u0438\u044f \u0435 \u0432\u0435\u0447\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0430",
|
||||||
|
"cannot_connect": "\u041d\u0435 \u043c\u043e\u0436\u0430 \u0434\u0430 \u0441\u0435 \u0441\u0432\u044a\u0440\u0436\u0435 \u0441 \u0431\u0430\u0437\u043e\u0432\u0430\u0442\u0430 \u0441\u0442\u0430\u043d\u0446\u0438\u044f",
|
||||||
|
"discover_timeout": "\u041d\u0435\u0432\u044a\u0437\u043c\u043e\u0436\u043d\u043e \u0435 \u0434\u0430 \u0431\u044a\u0434\u0430\u0442 \u043e\u0442\u043a\u0440\u0438\u0442\u0438 \u0431\u0430\u0437\u043e\u0432\u0438 \u0441\u0442\u0430\u043d\u0446\u0438\u0438 \u043d\u0430 Philips Hue",
|
||||||
|
"no_bridges": "\u041d\u0435 \u0441\u0430 \u043e\u0442\u043a\u0440\u0438\u0442\u0438 \u0431\u0430\u0437\u043e\u0432\u0438 \u0441\u0442\u0430\u043d\u0446\u0438\u0438 \u043d\u0430 Philips Hue",
|
||||||
|
"unknown": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"linking": "\u041f\u043e\u044f\u0432\u0438 \u0441\u0435 \u043d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435\u0442\u043e.",
|
||||||
|
"register_failed": "\u0420\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u044f\u0442\u0430 \u043d\u0435 \u0431\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430, \u043c\u043e\u043b\u044f \u043e\u043f\u0438\u0442\u0430\u0439\u0442\u0435 \u043e\u0442\u043d\u043e\u0432\u043e"
|
||||||
|
},
|
||||||
|
"step": {
|
||||||
|
"init": {
|
||||||
|
"data": {
|
||||||
|
"host": "\u0425\u043e\u0441\u0442"
|
||||||
|
},
|
||||||
|
"title": "\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u0431\u0430\u0437\u043e\u0432\u0430 \u0441\u0442\u0430\u043d\u0446\u0438\u044f Philips Hue"
|
||||||
|
},
|
||||||
|
"link": {
|
||||||
|
"description": "\u041d\u0430\u0442\u0438\u0441\u043d\u0435\u0442\u0435 \u0431\u0443\u0442\u043e\u043d\u0430 \u043d\u0430 \u0431\u0430\u0437\u043e\u0432\u0430\u0442\u0430 \u0441\u0442\u0430\u043d\u0446\u0438\u044f, \u0437\u0430 \u0434\u0430 \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0430\u0442\u0435 Philips Hue \u0441 Home Assistant. \n\n",
|
||||||
|
"title": "\u0421\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u043d\u0430 \u0445\u044a\u0431"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"title": "\u0411\u0430\u0437\u043e\u0432\u0430 \u0441\u0442\u0430\u043d\u0446\u0438\u044f Philips Hue"
|
||||||
|
}
|
||||||
|
}
|
29
homeassistant/components/hue/.translations/cy.json
Normal file
29
homeassistant/components/hue/.translations/cy.json
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"abort": {
|
||||||
|
"all_configured": "Mae holl bontydd Philips Hue eisoes wedi eu ffurfweddu",
|
||||||
|
"already_configured": "Pont eisoes wedi'i ffurfweddu",
|
||||||
|
"cannot_connect": "Methu cysylltu i'r bont",
|
||||||
|
"discover_timeout": "Methu darganfod pontydd Hue",
|
||||||
|
"no_bridges": "Dim pontydd Philips Hue wedi'i ddarganfod",
|
||||||
|
"unknown": "Digwyddodd gwall anhysbys"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"linking": "Digwyddodd gwall cysylltu anhysbys.",
|
||||||
|
"register_failed": "Wedi methu \u00e2 chofrestru, pl\u00eds ceisiwch eto"
|
||||||
|
},
|
||||||
|
"step": {
|
||||||
|
"init": {
|
||||||
|
"data": {
|
||||||
|
"host": "Gwesteiwr"
|
||||||
|
},
|
||||||
|
"title": "Dewiswch bont Hue"
|
||||||
|
},
|
||||||
|
"link": {
|
||||||
|
"description": "Pwyswch y botwm ar y bont i gofrestru Philips Hue gyda Cynorthwydd Cartref.\n\n",
|
||||||
|
"title": "Hwb cyswllt"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"title": "Pont Phillips Hue"
|
||||||
|
}
|
||||||
|
}
|
19
homeassistant/components/hue/.translations/da.json
Normal file
19
homeassistant/components/hue/.translations/da.json
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"abort": {
|
||||||
|
"no_bridges": "Ingen Philips Hue bridge fundet"
|
||||||
|
},
|
||||||
|
"step": {
|
||||||
|
"init": {
|
||||||
|
"data": {
|
||||||
|
"host": "V\u00e6rt"
|
||||||
|
},
|
||||||
|
"title": "V\u00e6lg Hue bridge"
|
||||||
|
},
|
||||||
|
"link": {
|
||||||
|
"title": "Link Hub"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"title": "Philips Hue Bridge"
|
||||||
|
}
|
||||||
|
}
|
@ -2,8 +2,11 @@
|
|||||||
"config": {
|
"config": {
|
||||||
"abort": {
|
"abort": {
|
||||||
"all_configured": "Alle Philips Hue Bridges sind bereits konfiguriert",
|
"all_configured": "Alle Philips Hue Bridges sind bereits konfiguriert",
|
||||||
|
"already_configured": "Bridge ist bereits konfiguriert",
|
||||||
|
"cannot_connect": "Verbindung zur Bridge nicht m\u00f6glich",
|
||||||
"discover_timeout": "Nicht in der Lage Hue Bridges zu entdecken",
|
"discover_timeout": "Nicht in der Lage Hue Bridges zu entdecken",
|
||||||
"no_bridges": "Philips Hue Bridges entdeckt"
|
"no_bridges": "Keine Philips Hue Bridges entdeckt",
|
||||||
|
"unknown": "Unbekannter Fehler ist aufgetreten"
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"linking": "Unbekannter Link-Fehler aufgetreten.",
|
"linking": "Unbekannter Link-Fehler aufgetreten.",
|
||||||
|
@ -2,8 +2,11 @@
|
|||||||
"config": {
|
"config": {
|
||||||
"abort": {
|
"abort": {
|
||||||
"all_configured": "All Philips Hue bridges are already configured",
|
"all_configured": "All Philips Hue bridges are already configured",
|
||||||
|
"already_configured": "Bridge is already configured",
|
||||||
|
"cannot_connect": "Unable to connect to the bridge",
|
||||||
"discover_timeout": "Unable to discover Hue bridges",
|
"discover_timeout": "Unable to discover Hue bridges",
|
||||||
"no_bridges": "No Philips Hue bridges discovered"
|
"no_bridges": "No Philips Hue bridges discovered",
|
||||||
|
"unknown": "Unknown error occurred"
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"linking": "Unknown linking error occurred.",
|
"linking": "Unknown linking error occurred.",
|
||||||
|
11
homeassistant/components/hue/.translations/es.json
Normal file
11
homeassistant/components/hue/.translations/es.json
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"abort": {
|
||||||
|
"unknown": "Se produjo un error desconocido"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"linking": "Se produjo un error de enlace desconocido.",
|
||||||
|
"register_failed": "No se pudo registrar, intente de nuevo"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
28
homeassistant/components/hue/.translations/hu.json
Normal file
28
homeassistant/components/hue/.translations/hu.json
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"abort": {
|
||||||
|
"all_configured": "M\u00e1r minden Philips Hue bridge konfigur\u00e1lt",
|
||||||
|
"already_configured": "A bridge m\u00e1r konfigur\u00e1lt",
|
||||||
|
"cannot_connect": "Nem siker\u00fclt csatlakozni a bridge-hez.",
|
||||||
|
"discover_timeout": "Nem tal\u00e1ltam a Hue bridget",
|
||||||
|
"no_bridges": "Nem tal\u00e1ltam Philips Hue bridget",
|
||||||
|
"unknown": "Ismeretlen hiba t\u00f6rt\u00e9nt"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"linking": "Ismeretlen \u00f6sszekapcsol\u00e1si hiba t\u00f6rt\u00e9nt.",
|
||||||
|
"register_failed": "Regisztr\u00e1ci\u00f3 nem siker\u00fclt, k\u00e9rem pr\u00f3b\u00e1lja \u00fajra"
|
||||||
|
},
|
||||||
|
"step": {
|
||||||
|
"init": {
|
||||||
|
"data": {
|
||||||
|
"host": "H\u00e1zigazda (Host)"
|
||||||
|
},
|
||||||
|
"title": "V\u00e1lassz Hue bridge-t"
|
||||||
|
},
|
||||||
|
"link": {
|
||||||
|
"title": "Kapcsol\u00f3d\u00e1s a hubhoz"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"title": "Philips Hue Bridge"
|
||||||
|
}
|
||||||
|
}
|
10
homeassistant/components/hue/.translations/it.json
Normal file
10
homeassistant/components/hue/.translations/it.json
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"abort": {
|
||||||
|
"all_configured": "Tutti i bridge Philips Hue sono gi\u00e0 configurati",
|
||||||
|
"discover_timeout": "Impossibile trovare i bridge Hue",
|
||||||
|
"no_bridges": "Nessun bridge Hue di Philips trovato"
|
||||||
|
},
|
||||||
|
"title": "Philips Hue Bridge"
|
||||||
|
}
|
||||||
|
}
|
@ -2,8 +2,11 @@
|
|||||||
"config": {
|
"config": {
|
||||||
"abort": {
|
"abort": {
|
||||||
"all_configured": "\ubaa8\ub4e0 \ud544\ub9bd\uc2a4 Hue \ube0c\ub9bf\uc9c0\uac00 \uc774\ubbf8 \uc124\uc815\ub41c \uc0c1\ud0dc\uc785\ub2c8\ub2e4",
|
"all_configured": "\ubaa8\ub4e0 \ud544\ub9bd\uc2a4 Hue \ube0c\ub9bf\uc9c0\uac00 \uc774\ubbf8 \uc124\uc815\ub41c \uc0c1\ud0dc\uc785\ub2c8\ub2e4",
|
||||||
|
"already_configured": "\ube0c\ub9bf\uc9c0\uac00 \uc774\ubbf8 \uc124\uc815\ub41c \uc0c1\ud0dc\uc785\ub2c8\ub2e4",
|
||||||
|
"cannot_connect": "\ube0c\ub9ac\uc9c0\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4",
|
||||||
"discover_timeout": "Hue \ube0c\ub9bf\uc9c0\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4",
|
"discover_timeout": "Hue \ube0c\ub9bf\uc9c0\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4",
|
||||||
"no_bridges": "\ubc1c\uacac\ub41c \ud544\ub9bd\uc2a4 Hue \ube0c\ub9bf\uc9c0\uac00 \uc5c6\uc2b5\ub2c8\ub2e4"
|
"no_bridges": "\ubc1c\uacac\ub41c \ud544\ub9bd\uc2a4 Hue \ube0c\ub9bf\uc9c0\uac00 \uc5c6\uc2b5\ub2c8\ub2e4",
|
||||||
|
"unknown": "\uc54c \uc218\uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4"
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"linking": "\uc54c \uc218\uc5c6\ub294 \uc5f0\uacb0 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.",
|
"linking": "\uc54c \uc218\uc5c6\ub294 \uc5f0\uacb0 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.",
|
||||||
|
29
homeassistant/components/hue/.translations/lb.json
Normal file
29
homeassistant/components/hue/.translations/lb.json
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"abort": {
|
||||||
|
"all_configured": "All Philips Hue Bridge si scho\u00a0konfigur\u00e9iert",
|
||||||
|
"already_configured": "Bridge ass scho konfigur\u00e9iert",
|
||||||
|
"cannot_connect": "Keng Verbindung mat der bridge m\u00e9iglech",
|
||||||
|
"discover_timeout": "Keng Hue bridge fonnt",
|
||||||
|
"no_bridges": "Keng Philips Hue Bridge fonnt",
|
||||||
|
"unknown": "Onbekannten Feeler opgetrueden"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"linking": "Onbekannte Liaisoun's Feeler opgetrueden",
|
||||||
|
"register_failed": "Feeler beim registr\u00e9ieren, prob\u00e9iert w.e.g. nach emol"
|
||||||
|
},
|
||||||
|
"step": {
|
||||||
|
"init": {
|
||||||
|
"data": {
|
||||||
|
"host": "Host"
|
||||||
|
},
|
||||||
|
"title": "Hue Bridge auswielen"
|
||||||
|
},
|
||||||
|
"link": {
|
||||||
|
"description": "Dr\u00e9ckt de Kn\u00e4ppchen un der Bridge fir den Philips Hue mam Home Assistant ze registr\u00e9ieren.\n\n",
|
||||||
|
"title": "Link Hub"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"title": "Philips Hue Bridge"
|
||||||
|
}
|
||||||
|
}
|
@ -2,8 +2,11 @@
|
|||||||
"config": {
|
"config": {
|
||||||
"abort": {
|
"abort": {
|
||||||
"all_configured": "Alle Philips Hue bridges zijn al geconfigureerd",
|
"all_configured": "Alle Philips Hue bridges zijn al geconfigureerd",
|
||||||
|
"already_configured": "Bridge is al geconfigureerd",
|
||||||
|
"cannot_connect": "Kan geen verbinding maken met bridge",
|
||||||
"discover_timeout": "Hue bridges kunnen niet worden gevonden",
|
"discover_timeout": "Hue bridges kunnen niet worden gevonden",
|
||||||
"no_bridges": "Geen Philips Hue bridges ontdekt"
|
"no_bridges": "Geen Philips Hue bridges ontdekt",
|
||||||
|
"unknown": "Onbekende fout opgetreden"
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"linking": "Er is een onbekende verbindingsfout opgetreden.",
|
"linking": "Er is een onbekende verbindingsfout opgetreden.",
|
||||||
@ -17,7 +20,7 @@
|
|||||||
"title": "Kies Hue bridge"
|
"title": "Kies Hue bridge"
|
||||||
},
|
},
|
||||||
"link": {
|
"link": {
|
||||||
"description": "Druk op de knop van de bridge om Philips Hue te registreren met de Home Assistant. ![Locatie van de knop op bridge] (/static/images/config_philips_hue.jpg)",
|
"description": "Druk op de knop van de bridge om Philips Hue te registreren met Home Assistant. ![Locatie van de knop op bridge] (/static/images/config_philips_hue.jpg)",
|
||||||
"title": "Link Hub"
|
"title": "Link Hub"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -2,8 +2,11 @@
|
|||||||
"config": {
|
"config": {
|
||||||
"abort": {
|
"abort": {
|
||||||
"all_configured": "Alle Philips Hue Bridger er allerede konfigurert",
|
"all_configured": "Alle Philips Hue Bridger er allerede konfigurert",
|
||||||
|
"already_configured": "Bridge er allerede konfigurert",
|
||||||
|
"cannot_connect": "Kan ikke koble til Bridge",
|
||||||
"discover_timeout": "Kunne ikke oppdage Hue Bridger",
|
"discover_timeout": "Kunne ikke oppdage Hue Bridger",
|
||||||
"no_bridges": "Ingen Philips Hue Bridger oppdaget"
|
"no_bridges": "Ingen Philips Hue Bridger oppdaget",
|
||||||
|
"unknown": "Ukjent feil oppstod"
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"linking": "Ukjent koblingsfeil oppstod.",
|
"linking": "Ukjent koblingsfeil oppstod.",
|
||||||
|
@ -2,8 +2,11 @@
|
|||||||
"config": {
|
"config": {
|
||||||
"abort": {
|
"abort": {
|
||||||
"all_configured": "Wszystkie mostki Hue s\u0105 ju\u017c skonfigurowane",
|
"all_configured": "Wszystkie mostki Hue s\u0105 ju\u017c skonfigurowane",
|
||||||
|
"already_configured": "Mostek jest ju\u017c skonfigurowany",
|
||||||
|
"cannot_connect": "Nie mo\u017cna po\u0142\u0105czy\u0107 si\u0119 z mostkiem",
|
||||||
"discover_timeout": "Nie mo\u017cna wykry\u0107 \u017cadnych mostk\u00f3w Hue",
|
"discover_timeout": "Nie mo\u017cna wykry\u0107 \u017cadnych mostk\u00f3w Hue",
|
||||||
"no_bridges": "Nie wykryto \u017cadnych mostk\u00f3w Hue"
|
"no_bridges": "Nie wykryto \u017cadnych mostk\u00f3w Hue",
|
||||||
|
"unknown": "Wyst\u0105pi\u0142 nieznany b\u0142\u0105d"
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"linking": "Wyst\u0105pi\u0142 nieznany b\u0142\u0105d w trakcie \u0142\u0105czenia.",
|
"linking": "Wyst\u0105pi\u0142 nieznany b\u0142\u0105d w trakcie \u0142\u0105czenia.",
|
||||||
|
5
homeassistant/components/hue/.translations/pt.json
Normal file
5
homeassistant/components/hue/.translations/pt.json
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"title": ""
|
||||||
|
}
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user